1# Copyright (c) 2014 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5"""Model extensions common to both the server and client rdb modules. 6""" 7 8import six 9from autotest_lib.client.common_lib import host_protections, host_states 10from autotest_lib.frontend import settings 11from django.core import exceptions as django_exceptions 12from django.db import models as dbmodels 13 14 15class ModelValidators(object): 16 """Convenience functions for model validation. 17 18 This model is duplicated both on the client and server rdb. Any method 19 added to this class must only be capable of class level validation of model 20 fields, since anything else is meaningless on the client side. 21 """ 22 # TODO: at least some of these functions really belong in a custom 23 # Manager class. 24 25 field_dict = None 26 # subclasses should override if they want to support smart_get() by name 27 name_field = None 28 29 @classmethod 30 def get_field_dict(cls): 31 if cls.field_dict is None: 32 cls.field_dict = {} 33 for field in cls._meta.fields: 34 cls.field_dict[field.name] = field 35 return cls.field_dict 36 37 38 @classmethod 39 def clean_foreign_keys(cls, data): 40 """\ 41 -Convert foreign key fields in data from <field>_id to just 42 <field>. 43 -replace foreign key objects with their IDs 44 This method modifies data in-place. 45 """ 46 for field in cls._meta.fields: 47 if not field.rel: 48 continue 49 if (field.attname != field.name and 50 field.attname in data): 51 data[field.name] = data[field.attname] 52 del data[field.attname] 53 if field.name not in data: 54 continue 55 value = data[field.name] 56 if isinstance(value, dbmodels.Model): 57 data[field.name] = value._get_pk_val() 58 59 60 @classmethod 61 def _convert_booleans(cls, data): 62 """ 63 Ensure BooleanFields actually get bool values. The Django MySQL 64 backend returns ints for BooleanFields, which is almost always not 65 a problem, but it can be annoying in certain situations. 66 """ 67 for field in cls._meta.fields: 68 if type(field) == dbmodels.BooleanField and field.name in data: 69 data[field.name] = bool(data[field.name]) 70 71 72 # TODO(showard) - is there a way to not have to do this? 73 @classmethod 74 def provide_default_values(cls, data): 75 """\ 76 Provide default values for fields with default values which have 77 nothing passed in. 78 79 For CharField and TextField fields with "blank=True", if nothing 80 is passed, we fill in an empty string value, even if there's no 81 :retab default set. 82 """ 83 new_data = dict(data) 84 field_dict = cls.get_field_dict() 85 for name, obj in six.iteritems(field_dict): 86 if data.get(name) is not None: 87 continue 88 if obj.default is not dbmodels.fields.NOT_PROVIDED: 89 new_data[name] = obj.default 90 elif (isinstance(obj, dbmodels.CharField) or 91 isinstance(obj, dbmodels.TextField)): 92 new_data[name] = '' 93 return new_data 94 95 96 @classmethod 97 def validate_field_names(cls, data): 98 'Checks for extraneous fields in data.' 99 errors = {} 100 field_dict = cls.get_field_dict() 101 for field_name in data: 102 if field_name not in field_dict: 103 errors[field_name] = 'No field of this name' 104 return errors 105 106 107 @classmethod 108 def prepare_data_args(cls, data): 109 'Common preparation for add_object and update_object' 110 # must check for extraneous field names here, while we have the 111 # data in a dict 112 errors = cls.validate_field_names(data) 113 if errors: 114 raise django_exceptions.ValidationError(errors) 115 return data 116 117 118 @classmethod 119 def _get_required_field_names(cls): 120 """Get the fields without which we cannot create a host. 121 122 @return: A list of field names that cannot be blank on host creation. 123 """ 124 return [field.name for field in cls._meta.fields if not field.blank] 125 126 127 @classmethod 128 def get_basic_field_names(cls): 129 """Get all basic fields of the Model. 130 131 This method returns the names of all fields that the client can provide 132 a value for during host creation. The fields not included in this list 133 are those that we can leave blank. Specifying non-null values for such 134 fields only makes sense as an update to the host. 135 136 @return A list of basic fields. 137 Eg: set([hostname, locked, leased, status, invalid, 138 protection, lock_time, dirty]) 139 """ 140 return [field.name for field in cls._meta.fields 141 if field.has_default()] + cls._get_required_field_names() 142 143 144 @classmethod 145 def validate_model_fields(cls, data): 146 """Validate parameters needed to create a host. 147 148 Check that all required fields are specified, that specified fields 149 are actual model values, and provide defaults for the unspecified 150 but unrequired fields. 151 152 @param dict: A dictionary with the args to create the model. 153 154 @raises dajngo_exceptions.ValidationError: If either an invalid field 155 is specified or a required field is missing. 156 """ 157 missing_fields = set(cls._get_required_field_names()) - set(data.keys()) 158 if missing_fields: 159 raise django_exceptions.ValidationError('%s required to create %s, ' 160 'supplied %s ' % (missing_fields, cls.__name__, data)) 161 data = cls.prepare_data_args(data) 162 data = cls.provide_default_values(data) 163 return data 164 165 166class AbstractHostModel(dbmodels.Model, ModelValidators): 167 """Abstract model specifying all fields one can use to create a host. 168 169 This model enforces consistency between the host models of the rdb and 170 their representation on the client side. 171 172 Internal fields: 173 status: string describing status of host 174 invalid: true if the host has been deleted 175 protection: indicates what can be done to this host during repair 176 lock_time: DateTime at which the host was locked 177 dirty: true if the host has been used without being rebooted 178 lock_reason: The reason for locking the host. 179 """ 180 Status = host_states.Status 181 hostname = dbmodels.CharField(max_length=255, unique=True) 182 locked = dbmodels.BooleanField(default=False) 183 leased = dbmodels.BooleanField(default=True) 184 # TODO(ayatane): This is needed until synch_id is removed from Host._fields 185 synch_id = dbmodels.IntegerField(blank=True, null=True, 186 editable=settings.FULL_ADMIN) 187 status = dbmodels.CharField(max_length=255, default=Status.READY, 188 choices=Status.choices(), 189 editable=settings.FULL_ADMIN) 190 invalid = dbmodels.BooleanField(default=False, 191 editable=settings.FULL_ADMIN) 192 protection = dbmodels.SmallIntegerField(null=False, blank=True, 193 choices=host_protections.choices, 194 default=host_protections.default) 195 lock_time = dbmodels.DateTimeField(null=True, blank=True, editable=False) 196 dirty = dbmodels.BooleanField(default=True, editable=settings.FULL_ADMIN) 197 lock_reason = dbmodels.CharField(null=True, max_length=255, blank=True, 198 default='') 199 200 201 class Meta: 202 """Extends dbmodels.Model.Meta""" 203 abstract = True 204