# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Model extensions common to both the server and client rdb modules.
"""
from django.core import exceptions as django_exceptions
from django.db import models as dbmodels
from autotest_lib.client.common_lib import host_protections
from autotest_lib.client.common_lib import host_states
from autotest_lib.frontend import settings
class ModelValidators(object):
"""Convenience functions for model validation.
This model is duplicated both on the client and server rdb. Any method
added to this class must only be capable of class level validation of model
fields, since anything else is meaningless on the client side.
"""
# TODO: at least some of these functions really belong in a custom
# Manager class.
field_dict = None
# subclasses should override if they want to support smart_get() by name
name_field = None
@classmethod
def get_field_dict(cls):
if cls.field_dict is None:
cls.field_dict = {}
for field in cls._meta.fields:
cls.field_dict[field.name] = field
return cls.field_dict
@classmethod
def clean_foreign_keys(cls, data):
"""\
-Convert foreign key fields in data from <field>_id to just
<field>.
-replace foreign key objects with their IDs
This method modifies data in-place.
"""
for field in cls._meta.fields:
if not field.rel:
continue
if (field.attname != field.name and
field.attname in data):
data[field.name] = data[field.attname]
del data[field.attname]
if field.name not in data:
continue
value = data[field.name]
if isinstance(value, dbmodels.Model):
data[field.name] = value._get_pk_val()
@classmethod
def _convert_booleans(cls, data):
"""
Ensure BooleanFields actually get bool values. The Django MySQL
backend returns ints for BooleanFields, which is almost always not
a problem, but it can be annoying in certain situations.
"""
for field in cls._meta.fields:
if type(field) == dbmodels.BooleanField and field.name in data:
data[field.name] = bool(data[field.name])
# TODO(showard) - is there a way to not have to do this?
@classmethod
def provide_default_values(cls, data):
"""\
Provide default values for fields with default values which have
nothing passed in.
For CharField and TextField fields with "blank=True", if nothing
is passed, we fill in an empty string value, even if there's no
:retab default set.
"""
new_data = dict(data)
field_dict = cls.get_field_dict()
for name, obj in field_dict.iteritems():
if data.get(name) is not None:
continue
if obj.default is not dbmodels.fields.NOT_PROVIDED:
new_data[name] = obj.default
elif (isinstance(obj, dbmodels.CharField) or
isinstance(obj, dbmodels.TextField)):
new_data[name] = ''
return new_data
@classmethod
def validate_field_names(cls, data):
'Checks for extraneous fields in data.'
errors = {}
field_dict = cls.get_field_dict()
for field_name in data:
if field_name not in field_dict:
errors[field_name] = 'No field of this name'
return errors
@classmethod
def prepare_data_args(cls, data):
'Common preparation for add_object and update_object'
# must check for extraneous field names here, while we have the
# data in a dict
errors = cls.validate_field_names(data)
if errors:
raise django_exceptions.ValidationError(errors)
return data
@classmethod
def _get_required_field_names(cls):
"""Get the fields without which we cannot create a host.
@return: A list of field names that cannot be blank on host creation.
"""
return [field.name for field in cls._meta.fields if not field.blank]
@classmethod
def get_basic_field_names(cls):
"""Get all basic fields of the Model.
This method returns the names of all fields that the client can provide
a value for during host creation. The fields not included in this list
are those that we can leave blank. Specifying non-null values for such
fields only makes sense as an update to the host.
@return A list of basic fields.
Eg: set([hostname, locked, leased, status, invalid,
protection, lock_time, dirty])
"""
return [field.name for field in cls._meta.fields
if field.has_default()] + cls._get_required_field_names()
@classmethod
def validate_model_fields(cls, data):
"""Validate parameters needed to create a host.
Check that all required fields are specified, that specified fields
are actual model values, and provide defaults for the unspecified
but unrequired fields.
@param dict: A dictionary with the args to create the model.
@raises dajngo_exceptions.ValidationError: If either an invalid field
is specified or a required field is missing.
"""
missing_fields = set(cls._get_required_field_names()) - set(data.keys())
if missing_fields:
raise django_exceptions.ValidationError('%s required to create %s, '
'supplied %s ' % (missing_fields, cls.__name__, data))
data = cls.prepare_data_args(data)
data = cls.provide_default_values(data)
return data
class AbstractHostModel(dbmodels.Model, ModelValidators):
"""Abstract model specifying all fields one can use to create a host.
This model enforces consistency between the host models of the rdb and
their representation on the client side.
Internal fields:
status: string describing status of host
invalid: true if the host has been deleted
protection: indicates what can be done to this host during repair
lock_time: DateTime at which the host was locked
dirty: true if the host has been used without being rebooted
lock_reason: The reason for locking the host.
"""
Status = host_states.Status
hostname = dbmodels.CharField(max_length=255, unique=True)
locked = dbmodels.BooleanField(default=False)
leased = dbmodels.BooleanField(default=True)
# TODO(ayatane): This is needed until synch_id is removed from Host._fields
synch_id = dbmodels.IntegerField(blank=True, null=True,
editable=settings.FULL_ADMIN)
status = dbmodels.CharField(max_length=255, default=Status.READY,
choices=Status.choices(),
editable=settings.FULL_ADMIN)
invalid = dbmodels.BooleanField(default=False,
editable=settings.FULL_ADMIN)
protection = dbmodels.SmallIntegerField(null=False, blank=True,
choices=host_protections.choices,
default=host_protections.default)
lock_time = dbmodels.DateTimeField(null=True, blank=True, editable=False)
dirty = dbmodels.BooleanField(default=True, editable=settings.FULL_ADMIN)
lock_reason = dbmodels.CharField(null=True, max_length=255, blank=True,
default='')
class Meta:
abstract = True