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