showard | a5288b4 | 2009-07-28 20:06:08 +0000 | [diff] [blame] | 1 | """Django 1.0 admin interface declarations.""" |
| 2 | |
showard | 3370e75 | 2009-08-31 18:32:30 +0000 | [diff] [blame] | 3 | from django import forms |
Jiaxi Luo | c342f9f | 2014-05-19 16:22:03 -0700 | [diff] [blame] | 4 | from django.contrib import admin, messages |
showard | 65974e1 | 2009-08-20 23:34:38 +0000 | [diff] [blame] | 5 | from django.db import models as dbmodels |
Jiaxi Luo | c342f9f | 2014-05-19 16:22:03 -0700 | [diff] [blame] | 6 | from django.forms.util import flatatt |
| 7 | from django.utils.encoding import smart_str |
| 8 | from django.utils.safestring import mark_safe |
showard | a5288b4 | 2009-07-28 20:06:08 +0000 | [diff] [blame] | 9 | |
Justin Giorgi | 16bba56 | 2016-06-22 10:42:13 -0700 | [diff] [blame] | 10 | from autotest_lib.cli import rpc, host |
showard | a5288b4 | 2009-07-28 20:06:08 +0000 | [diff] [blame] | 11 | from autotest_lib.frontend import settings |
| 12 | from autotest_lib.frontend.afe import model_logic, models |
| 13 | |
| 14 | |
showard | 65974e1 | 2009-08-20 23:34:38 +0000 | [diff] [blame] | 15 | class SiteAdmin(admin.ModelAdmin): |
| 16 | def formfield_for_dbfield(self, db_field, **kwargs): |
| 17 | field = super(SiteAdmin, self).formfield_for_dbfield(db_field, **kwargs) |
| 18 | if (db_field.rel and |
| 19 | issubclass(db_field.rel.to, model_logic.ModelWithInvalid)): |
| 20 | model = db_field.rel.to |
| 21 | field.choices = model.valid_objects.all().values_list( |
| 22 | 'id', model.name_field) |
| 23 | return field |
| 24 | |
| 25 | |
showard | 3370e75 | 2009-08-31 18:32:30 +0000 | [diff] [blame] | 26 | class ModelWithInvalidForm(forms.ModelForm): |
| 27 | def validate_unique(self): |
| 28 | # Don't validate name uniqueness if the duplicate model is invalid |
| 29 | model = self.Meta.model |
| 30 | filter_data = { |
| 31 | model.name_field : self.cleaned_data[model.name_field], |
| 32 | 'invalid' : True |
| 33 | } |
| 34 | needs_remove = bool(self.Meta.model.objects.filter(**filter_data)) |
| 35 | if needs_remove: |
| 36 | name_field = self.fields.pop(model.name_field) |
| 37 | super(ModelWithInvalidForm, self).validate_unique() |
| 38 | if needs_remove: |
| 39 | self.fields[model.name_field] = name_field |
| 40 | |
| 41 | |
| 42 | class AtomicGroupForm(ModelWithInvalidForm): |
| 43 | class Meta: |
| 44 | model = models.AtomicGroup |
| 45 | |
| 46 | |
showard | 65974e1 | 2009-08-20 23:34:38 +0000 | [diff] [blame] | 47 | class AtomicGroupAdmin(SiteAdmin): |
showard | a5288b4 | 2009-07-28 20:06:08 +0000 | [diff] [blame] | 48 | list_display = ('name', 'description', 'max_number_of_machines') |
| 49 | |
showard | 3370e75 | 2009-08-31 18:32:30 +0000 | [diff] [blame] | 50 | form = AtomicGroupForm |
| 51 | |
showard | a5288b4 | 2009-07-28 20:06:08 +0000 | [diff] [blame] | 52 | def queryset(self, request): |
| 53 | return models.AtomicGroup.valid_objects |
| 54 | |
| 55 | admin.site.register(models.AtomicGroup, AtomicGroupAdmin) |
| 56 | |
| 57 | |
showard | 3370e75 | 2009-08-31 18:32:30 +0000 | [diff] [blame] | 58 | class LabelForm(ModelWithInvalidForm): |
| 59 | class Meta: |
| 60 | model = models.Label |
| 61 | |
| 62 | |
showard | 65974e1 | 2009-08-20 23:34:38 +0000 | [diff] [blame] | 63 | class LabelAdmin(SiteAdmin): |
Dale Curtis | 74a314b | 2011-06-23 14:55:46 -0700 | [diff] [blame] | 64 | list_display = ('name', 'atomic_group', 'kernel_config') |
| 65 | # Avoid a bug with the admin interface showing a select box pointed at an |
| 66 | # AtomicGroup when this field is intentionally NULL such that editing a |
| 67 | # label via the admin UI unintentionally sets an atomicgroup. |
| 68 | raw_id_fields = ('atomic_group',) |
showard | a5288b4 | 2009-07-28 20:06:08 +0000 | [diff] [blame] | 69 | |
showard | 3370e75 | 2009-08-31 18:32:30 +0000 | [diff] [blame] | 70 | form = LabelForm |
| 71 | |
showard | a5288b4 | 2009-07-28 20:06:08 +0000 | [diff] [blame] | 72 | def queryset(self, request): |
| 73 | return models.Label.valid_objects |
| 74 | |
| 75 | admin.site.register(models.Label, LabelAdmin) |
| 76 | |
| 77 | |
showard | 65974e1 | 2009-08-20 23:34:38 +0000 | [diff] [blame] | 78 | class UserAdmin(SiteAdmin): |
showard | a5288b4 | 2009-07-28 20:06:08 +0000 | [diff] [blame] | 79 | list_display = ('login', 'access_level') |
| 80 | search_fields = ('login',) |
| 81 | |
| 82 | admin.site.register(models.User, UserAdmin) |
| 83 | |
| 84 | |
Jiaxi Luo | c342f9f | 2014-05-19 16:22:03 -0700 | [diff] [blame] | 85 | class LabelsCommaSpacedWidget(forms.Widget): |
| 86 | """A widget that renders the labels in a comman separated text field.""" |
| 87 | |
| 88 | def render(self, name, value, attrs=None): |
| 89 | """Convert label ids to names and render them in HTML. |
| 90 | |
| 91 | @param name: Name attribute of the HTML tag. |
| 92 | @param value: A list of label ids to be rendered. |
| 93 | @param attrs: A dict of extra attributes rendered in the HTML tag. |
| 94 | @return: A Unicode string in HTML format. |
| 95 | """ |
| 96 | final_attrs = self.build_attrs(attrs, type='text', name=name) |
| 97 | |
| 98 | if value: |
| 99 | label_names =(models.Label.objects.filter(id__in=value) |
| 100 | .values_list('name', flat=True)) |
| 101 | value = ', '.join(label_names) |
| 102 | else: |
| 103 | value = '' |
| 104 | final_attrs['value'] = smart_str(value) |
| 105 | return mark_safe(u'<input%s />' % flatatt(final_attrs)) |
| 106 | |
| 107 | def value_from_datadict(self, data, files, name): |
| 108 | """Convert input string to a list of label ids. |
| 109 | |
| 110 | @param data: A dict of input data from HTML form. The keys are name |
| 111 | attrs of HTML tags. |
| 112 | @param files: A dict of input file names from HTML form. The keys are |
| 113 | name attrs of HTML tags. |
| 114 | @param name: The name attr of the HTML tag of labels. |
| 115 | @return: A list of label ids in string. Return None if no label is |
| 116 | specified. |
| 117 | """ |
| 118 | label_names = data.get(name) |
| 119 | if label_names: |
| 120 | label_names = label_names.split(',') |
| 121 | label_names = filter(None, |
| 122 | [name.strip(', ') for name in label_names]) |
| 123 | label_ids = (models.Label.objects.filter(name__in=label_names) |
| 124 | .values_list('id', flat=True)) |
| 125 | return [str(label_id) for label_id in label_ids] |
| 126 | |
| 127 | |
showard | 3370e75 | 2009-08-31 18:32:30 +0000 | [diff] [blame] | 128 | class HostForm(ModelWithInvalidForm): |
Jiaxi Luo | c342f9f | 2014-05-19 16:22:03 -0700 | [diff] [blame] | 129 | # A checkbox triggers label autodetection. |
| 130 | labels_autodetection = forms.BooleanField(initial=True, required=False) |
| 131 | |
| 132 | def __init__(self, *args, **kwargs): |
| 133 | super(HostForm, self).__init__(*args, **kwargs) |
| 134 | self.fields['labels'].widget = LabelsCommaSpacedWidget() |
| 135 | self.fields['labels'].help_text = ('Please enter a comma seperated ' |
| 136 | 'list of labels.') |
| 137 | |
Matthew Sartori | 6818633 | 2015-04-27 17:19:53 -0700 | [diff] [blame] | 138 | def clean(self): |
| 139 | """ ModelForm validation |
| 140 | |
| 141 | Ensure that a lock_reason is provided when locking a device. |
| 142 | """ |
| 143 | cleaned_data = super(HostForm, self).clean() |
| 144 | locked = cleaned_data.get('locked') |
| 145 | lock_reason = cleaned_data.get('lock_reason') |
| 146 | if locked and not lock_reason: |
| 147 | raise forms.ValidationError( |
| 148 | 'Please provide a lock reason when locking a device.') |
| 149 | return cleaned_data |
| 150 | |
showard | 3370e75 | 2009-08-31 18:32:30 +0000 | [diff] [blame] | 151 | class Meta: |
| 152 | model = models.Host |
| 153 | |
| 154 | |
Jiaxi Luo | 020cd19 | 2014-06-30 12:45:45 -0700 | [diff] [blame] | 155 | class HostAttributeInline(admin.TabularInline): |
| 156 | model = models.HostAttribute |
| 157 | extra = 1 |
| 158 | |
| 159 | |
showard | 65974e1 | 2009-08-20 23:34:38 +0000 | [diff] [blame] | 160 | class HostAdmin(SiteAdmin): |
showard | a5288b4 | 2009-07-28 20:06:08 +0000 | [diff] [blame] | 161 | # TODO(showard) - showing platform requires a SQL query for |
| 162 | # each row (since labels are many-to-many) - should we remove |
| 163 | # it? |
| 164 | list_display = ('hostname', 'platform', 'locked', 'status') |
Jiaxi Luo | c342f9f | 2014-05-19 16:22:03 -0700 | [diff] [blame] | 165 | list_filter = ('locked', 'protection', 'status') |
| 166 | search_fields = ('hostname',) |
showard | a5288b4 | 2009-07-28 20:06:08 +0000 | [diff] [blame] | 167 | |
showard | 3370e75 | 2009-08-31 18:32:30 +0000 | [diff] [blame] | 168 | form = HostForm |
| 169 | |
Jiaxi Luo | c342f9f | 2014-05-19 16:22:03 -0700 | [diff] [blame] | 170 | def __init__(self, model, admin_site): |
| 171 | self.successful_hosts = [] |
| 172 | super(HostAdmin, self).__init__(model, admin_site) |
| 173 | |
Matthew Sartori | 6818633 | 2015-04-27 17:19:53 -0700 | [diff] [blame] | 174 | def add_view(self, request, form_url='', extra_context=None): |
| 175 | """ Field layout for admin page. |
| 176 | |
| 177 | fields specifies the visibility and order of HostAdmin attributes |
| 178 | displayed on the device addition page. |
| 179 | |
| 180 | @param request: django request |
| 181 | @param form_url: url |
| 182 | @param extra_context: A dict used to alter the page view |
| 183 | """ |
| 184 | self.fields = ('hostname', 'locked', 'lock_reason', 'leased', |
| 185 | 'protection', 'labels', 'shard', 'labels_autodetection') |
| 186 | return super(HostAdmin, self).add_view(request, form_url, extra_context) |
| 187 | |
Jiaxi Luo | c342f9f | 2014-05-19 16:22:03 -0700 | [diff] [blame] | 188 | def change_view(self, request, obj_id, form_url='', extra_context=None): |
| 189 | # Hide labels_autodetection when editing a host. |
Matthew Sartori | 6818633 | 2015-04-27 17:19:53 -0700 | [diff] [blame] | 190 | self.fields = ('hostname', 'locked', 'lock_reason', |
| 191 | 'leased', 'protection', 'labels') |
Jiaxi Luo | 020cd19 | 2014-06-30 12:45:45 -0700 | [diff] [blame] | 192 | # Only allow editing host attributes when a host has been created. |
| 193 | self.inlines = [ |
| 194 | HostAttributeInline, |
| 195 | ] |
Jiaxi Luo | c342f9f | 2014-05-19 16:22:03 -0700 | [diff] [blame] | 196 | return super(HostAdmin, self).change_view(request, |
| 197 | obj_id, |
| 198 | form_url, |
| 199 | extra_context) |
| 200 | |
showard | a5288b4 | 2009-07-28 20:06:08 +0000 | [diff] [blame] | 201 | def queryset(self, request): |
| 202 | return models.Host.valid_objects |
| 203 | |
Jiaxi Luo | c342f9f | 2014-05-19 16:22:03 -0700 | [diff] [blame] | 204 | def response_add(self, request, obj, post_url_continue=None): |
| 205 | # Disable the 'save and continue editing option' when adding a host. |
| 206 | if "_continue" in request.POST: |
| 207 | request.POST = request.POST.copy() |
| 208 | del request.POST['_continue'] |
| 209 | return super(HostAdmin, self).response_add(request, |
| 210 | obj, |
| 211 | post_url_continue) |
| 212 | |
| 213 | def save_model(self, request, obj, form, change): |
| 214 | if not form.cleaned_data.get('labels_autodetection'): |
| 215 | return super(HostAdmin, self).save_model(request, obj, |
| 216 | form, change) |
| 217 | |
| 218 | # Get submitted info from form. |
| 219 | web_server = rpc.get_autotest_server() |
| 220 | hostname = form.cleaned_data['hostname'] |
| 221 | hosts = [str(hostname)] |
| 222 | platform = None |
| 223 | locked = form.cleaned_data['locked'] |
Matthew Sartori | 6818633 | 2015-04-27 17:19:53 -0700 | [diff] [blame] | 224 | lock_reason = form.cleaned_data['lock_reason'] |
Jiaxi Luo | c342f9f | 2014-05-19 16:22:03 -0700 | [diff] [blame] | 225 | labels = [label.name for label in form.cleaned_data['labels']] |
| 226 | protection = form.cleaned_data['protection'] |
| 227 | acls = [] |
| 228 | |
| 229 | # Pipe to cli to perform autodetection and create host. |
Justin Giorgi | 971b324 | 2016-07-20 15:44:21 -0700 | [diff] [blame] | 230 | host_create_obj = host.host_create.construct_without_parse( |
Jiaxi Luo | c342f9f | 2014-05-19 16:22:03 -0700 | [diff] [blame] | 231 | web_server, hosts, platform, |
Matthew Sartori | 6818633 | 2015-04-27 17:19:53 -0700 | [diff] [blame] | 232 | locked, lock_reason, labels, acls, |
Jiaxi Luo | c342f9f | 2014-05-19 16:22:03 -0700 | [diff] [blame] | 233 | protection) |
| 234 | try: |
| 235 | self.successful_hosts = host_create_obj.execute() |
| 236 | except SystemExit: |
| 237 | # Invalid server name. |
| 238 | messages.error(request, 'Invalid server name %s.' % web_server) |
| 239 | |
| 240 | # Successful_hosts is an empty list if there's time out, |
| 241 | # server error, or JSON error. |
| 242 | if not self.successful_hosts: |
| 243 | messages.error(request, |
| 244 | 'Label autodetection failed. ' |
| 245 | 'Host created with selected labels.') |
| 246 | super(HostAdmin, self).save_model(request, obj, form, change) |
| 247 | |
Justin Giorgi | 971b324 | 2016-07-20 15:44:21 -0700 | [diff] [blame] | 248 | |
Jiaxi Luo | c342f9f | 2014-05-19 16:22:03 -0700 | [diff] [blame] | 249 | def save_related(self, request, form, formsets, change): |
| 250 | """Save many-to-many relations between host and labels.""" |
Jiaxi Luo | 020cd19 | 2014-06-30 12:45:45 -0700 | [diff] [blame] | 251 | # Skip save_related if autodetection succeeded, since cli has already |
Jiaxi Luo | c342f9f | 2014-05-19 16:22:03 -0700 | [diff] [blame] | 252 | # handled many-to-many relations. |
| 253 | if not self.successful_hosts: |
| 254 | super(HostAdmin, self).save_related(request, |
| 255 | form, |
| 256 | formsets, |
| 257 | change) |
| 258 | |
showard | a5288b4 | 2009-07-28 20:06:08 +0000 | [diff] [blame] | 259 | admin.site.register(models.Host, HostAdmin) |
| 260 | |
| 261 | |
showard | 65974e1 | 2009-08-20 23:34:38 +0000 | [diff] [blame] | 262 | class TestAdmin(SiteAdmin): |
showard | a5288b4 | 2009-07-28 20:06:08 +0000 | [diff] [blame] | 263 | fields = ('name', 'author', 'test_category', 'test_class', |
| 264 | 'test_time', 'sync_count', 'test_type', 'path', |
| 265 | 'dependencies', 'experimental', 'run_verify', |
| 266 | 'description') |
jamesren | 35a7022 | 2010-02-16 19:30:46 +0000 | [diff] [blame] | 267 | list_display = ('name', 'test_type', 'admin_description', 'sync_count') |
showard | a5288b4 | 2009-07-28 20:06:08 +0000 | [diff] [blame] | 268 | search_fields = ('name',) |
| 269 | filter_horizontal = ('dependency_labels',) |
| 270 | |
| 271 | admin.site.register(models.Test, TestAdmin) |
| 272 | |
| 273 | |
showard | 65974e1 | 2009-08-20 23:34:38 +0000 | [diff] [blame] | 274 | class ProfilerAdmin(SiteAdmin): |
showard | a5288b4 | 2009-07-28 20:06:08 +0000 | [diff] [blame] | 275 | list_display = ('name', 'description') |
| 276 | search_fields = ('name',) |
| 277 | |
| 278 | admin.site.register(models.Profiler, ProfilerAdmin) |
| 279 | |
| 280 | |
showard | 65974e1 | 2009-08-20 23:34:38 +0000 | [diff] [blame] | 281 | class AclGroupAdmin(SiteAdmin): |
showard | a5288b4 | 2009-07-28 20:06:08 +0000 | [diff] [blame] | 282 | list_display = ('name', 'description') |
| 283 | search_fields = ('name',) |
| 284 | filter_horizontal = ('users', 'hosts') |
| 285 | |
showard | 8cbaf1e | 2009-09-08 16:27:04 +0000 | [diff] [blame] | 286 | def queryset(self, request): |
| 287 | return models.AclGroup.objects.exclude(name='Everyone') |
| 288 | |
| 289 | def save_model(self, request, obj, form, change): |
| 290 | super(AclGroupAdmin, self).save_model(request, obj, form, change) |
| 291 | _orig_save_m2m = form.save_m2m |
| 292 | |
| 293 | def save_m2m(): |
| 294 | _orig_save_m2m() |
| 295 | obj.perform_after_save(change) |
| 296 | |
| 297 | form.save_m2m = save_m2m |
| 298 | |
showard | a5288b4 | 2009-07-28 20:06:08 +0000 | [diff] [blame] | 299 | admin.site.register(models.AclGroup, AclGroupAdmin) |
| 300 | |
| 301 | |
jamesren | 543d9fb | 2010-06-07 20:45:31 +0000 | [diff] [blame] | 302 | class DroneSetForm(forms.ModelForm): |
| 303 | def __init__(self, *args, **kwargs): |
| 304 | super(DroneSetForm, self).__init__(*args, **kwargs) |
| 305 | drone_ids_used = set() |
| 306 | for drone_set in models.DroneSet.objects.exclude(id=self.instance.id): |
| 307 | drone_ids_used.update(drone_set.drones.values_list('id', flat=True)) |
| 308 | available_drones = models.Drone.objects.exclude(id__in=drone_ids_used) |
| 309 | |
| 310 | self.fields['drones'].widget.choices = [(drone.id, drone.hostname) |
| 311 | for drone in available_drones] |
| 312 | |
| 313 | |
jamesren | 76fcf19 | 2010-04-21 20:39:50 +0000 | [diff] [blame] | 314 | class DroneSetAdmin(SiteAdmin): |
| 315 | filter_horizontal = ('drones',) |
jamesren | 543d9fb | 2010-06-07 20:45:31 +0000 | [diff] [blame] | 316 | form = DroneSetForm |
jamesren | 76fcf19 | 2010-04-21 20:39:50 +0000 | [diff] [blame] | 317 | |
| 318 | admin.site.register(models.DroneSet, DroneSetAdmin) |
| 319 | |
| 320 | admin.site.register(models.Drone) |
| 321 | |
| 322 | |
showard | a5288b4 | 2009-07-28 20:06:08 +0000 | [diff] [blame] | 323 | if settings.FULL_ADMIN: |
showard | 65974e1 | 2009-08-20 23:34:38 +0000 | [diff] [blame] | 324 | class JobAdmin(SiteAdmin): |
showard | a5288b4 | 2009-07-28 20:06:08 +0000 | [diff] [blame] | 325 | list_display = ('id', 'owner', 'name', 'control_type') |
| 326 | filter_horizontal = ('dependency_labels',) |
| 327 | |
| 328 | admin.site.register(models.Job, JobAdmin) |
| 329 | |
showard | 3370e75 | 2009-08-31 18:32:30 +0000 | [diff] [blame] | 330 | |
showard | 65974e1 | 2009-08-20 23:34:38 +0000 | [diff] [blame] | 331 | class IneligibleHostQueueAdmin(SiteAdmin): |
showard | a5288b4 | 2009-07-28 20:06:08 +0000 | [diff] [blame] | 332 | list_display = ('id', 'job', 'host') |
| 333 | |
| 334 | admin.site.register(models.IneligibleHostQueue, IneligibleHostQueueAdmin) |
| 335 | |
showard | 3370e75 | 2009-08-31 18:32:30 +0000 | [diff] [blame] | 336 | |
showard | 65974e1 | 2009-08-20 23:34:38 +0000 | [diff] [blame] | 337 | class HostQueueEntryAdmin(SiteAdmin): |
showard | a5288b4 | 2009-07-28 20:06:08 +0000 | [diff] [blame] | 338 | list_display = ('id', 'job', 'host', 'status', |
| 339 | 'meta_host') |
| 340 | |
| 341 | admin.site.register(models.HostQueueEntry, HostQueueEntryAdmin) |
| 342 | |
| 343 | admin.site.register(models.AbortedHostQueueEntry) |