-
Notifications
You must be signed in to change notification settings - Fork 5
/
resource_data.py
274 lines (209 loc) · 9.6 KB
/
resource_data.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
"""Define resources model classes.
Defines the basic attributes & interface of any resource type class,
responsible for the resource static & dynamic information.
"""
# pylint: disable=no-self-use,too-many-public-methods,too-few-public-methods
# pylint: disable=attribute-defined-outside-init,invalid-name,old-style-class
# pylint: disable=access-member-before-definition,property-on-old-class,no-init
from datetime import datetime
from django.db import models
from django.utils import six
from django.db.models.base import ModelBase
from django.contrib.auth import models as auth_models
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from rotest.common.django_utils.fields import NameField
from rotest.common.django_utils.common import get_fields
from rotest.common.django_utils import get_sub_model, linked_unicode
class DataPointer(object):
"""Pointer to a field in the resource's data."""
def __init__(self, field_name):
self.field_name = field_name
class DataBase(ModelBase):
"""Metaclass that creates data pointers for django fields."""
def __getattr__(cls, key):
if hasattr(cls, '_meta') and \
key in (field.name for field in cls._meta.fields):
return DataPointer(key)
raise AttributeError(key)
class ResourceData(six.with_metaclass(DataBase, models.Model)):
"""Represent a container for a resource's global data.
Inheriting resource datas may add more fields, specific to the resource.
Note that fields pointing to objects inheriting from ResourceData are
considered 'sub resources', and will be considered when checking
availability, reserving or releasing the resource, etc.
Warning:
ResourceData subclasses cannot contain any unique fields.
Attributes:
name (str): name of the test containing the data.
group (auth_models.Group): group to associate the resource with.
owner (str): name of the locking user.
reserved (str): name of the user allow to lock the resource.
Empty string means available to all.
is_usable (bool): a flag to indicate if the resource is a duplication.
comment (str): general comment for the resource.
owner_time (datetime): timestamp of the last ownership event.
reserved_time (datetime): timestamp of the last reserve event.
"""
NAME_SEPERATOR = '_'
MAX_COMMENT_LENGTH = 200
# Fields that shouldn't be transmitted to the client:
IGNORED_FIELDS = ["group", "owner_time", "reserved_time"]
name = NameField(unique=True)
is_usable = models.BooleanField(default=True)
group = models.ForeignKey(auth_models.Group, blank=True, null=True)
comment = models.CharField(default='', blank=True,
max_length=MAX_COMMENT_LENGTH)
owner = NameField(blank=True)
reserved = NameField(blank=True)
owner_time = models.DateTimeField(null=True, blank=True)
reserved_time = models.DateTimeField(null=True, blank=True)
class Meta:
"""Define the Django application for this model."""
app_label = 'management'
def __eq__(self, obj):
return (self.__class__ == obj.__class__ and
self.name == obj.name)
def get_sub_resources(self):
"""Return an iterable to the resource's sub-resources."""
return (field_value for field_value in self.get_fields().itervalues()
if isinstance(field_value, ResourceData))
def _is_sub_resources_available(self, user_name=""):
"""Check if all the sub resources are available.
Args:
user_name (str): user name to be checked. Empty string means
available to all.
Returns:
bool. whether the sub resources are available.
"""
return all(sub_resource.is_available(user_name)
for sub_resource in self.get_sub_resources())
def is_available(self, user_name=""):
"""Return whether resource is available for the given user.
Args:
user_name (str): user name to be checked. Empty string means
available to all.
Returns:
bool. determine whether resource is available for the given user.
Note:
If this method is called from code then leaf is equal to 'self'.
If it is being called by BaseResources table in DB then leaf's
'is_available' method will be called.
"""
leaf = self.leaf # 'leaf' is a property.
if leaf == self:
leaf_available = (self.reserved in [user_name, ""] and
self.owner == "")
else:
leaf_available = leaf.is_available(user_name)
return leaf_available and self._is_sub_resources_available(user_name)
def __unicode__(self):
"""Django version of __str__"""
return self.name
@property
def admin_link(self):
"""Return a link to the resource admin page.
Returns:
str. link to the resource's admin page.
"""
return linked_unicode(self.leaf)
@property
def leaf(self):
"""Return the leaf resource-data inheriting from self."""
sub_model = get_sub_model(self)
if sub_model is None:
return self
return sub_model.leaf
def get_fields(self):
"""Extract the fields of the resource.
Returns:
dict. dictionary contain resource's fields.
"""
return get_fields(self, self.IGNORED_FIELDS)
def duplicate(self):
"""Create a copy of the resource, save it to DB and return it.
Create an exact copy of the resource instance, rename it to the
original name + current date-time and return it.
Note:
The duplication is performed by shallow copy.
Returns:
rotest.common.models.base_resource.BaseResource. a copy of the
instance.
"""
# A simple copy does'nt work so we had to hack it.
# Clone the class instance.
resource_properties = self.get_fields()
for key, value in resource_properties.items():
if isinstance(value, ResourceData):
resource_properties[key] = value.duplicate()
list_field_names = [key for key, value in resource_properties.items()
if isinstance(value, list)]
list_fields = [(field_name, resource_properties.pop(field_name))
for field_name in list_field_names]
resource_copy = self.__class__(**resource_properties)
resource_copy.id = None
resource_copy.name = '%s%s%s' % (self.name, self.NAME_SEPERATOR,
datetime.now())
resource_copy.is_usable = False
resource_copy.save()
if len(list_fields) > 0:
list_fields = [value.duplicate() if isinstance(value, ResourceData)
else value for value in list_fields]
for field_name, field_values in list_fields:
setattr(resource_copy, field_name, field_values)
resource_copy.save()
return resource_copy
def _was_reserved_changed(self):
"""Check if the user is trying to change the value of 'Reserved'.
Returns:
bool. whether 'Reserved' was changed.
"""
try:
pre_reserved = ResourceData.objects.get(pk=self.pk).reserved
except ObjectDoesNotExist:
pre_reserved = ''
return pre_reserved != self.reserved
def clean(self):
"""Block reserving and releasing if sub-resources are not available.
Disable only if the resource is not available for neither the previous
and the current user.
"""
try:
pre_reserved = ResourceData.objects.get(pk=self.pk).reserved
if pre_reserved != self.reserved:
cur_available = self._is_sub_resources_available(self.reserved)
pre_available = self._is_sub_resources_available(pre_reserved)
if (self.reserved != "" and
not cur_available and not pre_available):
raise ValidationError('Cannot reserve a resource if its '
'sub-resources are not available')
except ObjectDoesNotExist:
pass
super(ResourceData, self).clean()
def _unreserve_sub_resources(self, user):
"""Unreserve all sub-resources that are available to the given user.
Args:
user (str): the user to remove from reserved.
"""
for sub_resource in self.get_sub_resources():
if sub_resource.is_available(user):
sub_resource.reserved = ''
sub_resource.save()
def _reserve_sub_resources(self, reserved_text):
"""Set the 'Reserved' field of all the sub resources.
Args:
reserved_text (str): the text to put in 'Reserved' field.
"""
for sub_resource in self.get_sub_resources():
sub_resource.reserved = reserved_text
sub_resource.save()
def save(self, *args, **kwargs):
"""Propagate reservation change to sub-resources of the resource."""
if self._was_reserved_changed():
if self.reserved == '':
self.reserved_time = None
pre_reserved = ResourceData.objects.get(pk=self.pk).reserved
self._unreserve_sub_resources(pre_reserved)
else:
self.reserved_time = datetime.now()
self._reserve_sub_resources(self.reserved)
return super(ResourceData, self).save(*args, **kwargs)