# 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 _id to just . -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