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