1"""Django 1.0 admin interface declarations.""" 2 3from django import forms 4from django.contrib import admin, messages 5from django.db import models as dbmodels 6from django.forms.util import flatatt 7from django.utils.encoding import smart_str 8from django.utils.safestring import mark_safe 9 10from autotest_lib.cli import rpc, site_host 11from autotest_lib.frontend import settings 12from autotest_lib.frontend.afe import model_logic, models 13 14 15class 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 26class 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 42class AtomicGroupForm(ModelWithInvalidForm): 43 class Meta: 44 model = models.AtomicGroup 45 46 47class AtomicGroupAdmin(SiteAdmin): 48 list_display = ('name', 'description', 'max_number_of_machines') 49 50 form = AtomicGroupForm 51 52 def queryset(self, request): 53 return models.AtomicGroup.valid_objects 54 55admin.site.register(models.AtomicGroup, AtomicGroupAdmin) 56 57 58class LabelForm(ModelWithInvalidForm): 59 class Meta: 60 model = models.Label 61 62 63class LabelAdmin(SiteAdmin): 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',) 69 70 form = LabelForm 71 72 def queryset(self, request): 73 return models.Label.valid_objects 74 75admin.site.register(models.Label, LabelAdmin) 76 77 78class UserAdmin(SiteAdmin): 79 list_display = ('login', 'access_level') 80 search_fields = ('login',) 81 82admin.site.register(models.User, UserAdmin) 83 84 85class 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 128class HostForm(ModelWithInvalidForm): 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 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 151 class Meta: 152 model = models.Host 153 154 155class HostAttributeInline(admin.TabularInline): 156 model = models.HostAttribute 157 extra = 1 158 159 160class HostAdmin(SiteAdmin): 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') 165 list_filter = ('locked', 'protection', 'status') 166 search_fields = ('hostname',) 167 168 form = HostForm 169 170 def __init__(self, model, admin_site): 171 self.successful_hosts = [] 172 super(HostAdmin, self).__init__(model, admin_site) 173 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 188 def change_view(self, request, obj_id, form_url='', extra_context=None): 189 # Hide labels_autodetection when editing a host. 190 self.fields = ('hostname', 'locked', 'lock_reason', 191 'leased', 'protection', 'labels') 192 # Only allow editing host attributes when a host has been created. 193 self.inlines = [ 194 HostAttributeInline, 195 ] 196 return super(HostAdmin, self).change_view(request, 197 obj_id, 198 form_url, 199 extra_context) 200 201 def queryset(self, request): 202 return models.Host.valid_objects 203 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'] 224 lock_reason = form.cleaned_data['lock_reason'] 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. 230 host_create_obj = site_host.site_host_create.construct_without_parse( 231 web_server, hosts, platform, 232 locked, lock_reason, labels, acls, 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 248 def save_related(self, request, form, formsets, change): 249 """Save many-to-many relations between host and labels.""" 250 # Skip save_related if autodetection succeeded, since cli has already 251 # handled many-to-many relations. 252 if not self.successful_hosts: 253 super(HostAdmin, self).save_related(request, 254 form, 255 formsets, 256 change) 257 258admin.site.register(models.Host, HostAdmin) 259 260 261class TestAdmin(SiteAdmin): 262 fields = ('name', 'author', 'test_category', 'test_class', 263 'test_time', 'sync_count', 'test_type', 'path', 264 'dependencies', 'experimental', 'run_verify', 265 'description') 266 list_display = ('name', 'test_type', 'admin_description', 'sync_count') 267 search_fields = ('name',) 268 filter_horizontal = ('dependency_labels',) 269 270admin.site.register(models.Test, TestAdmin) 271 272 273class ProfilerAdmin(SiteAdmin): 274 list_display = ('name', 'description') 275 search_fields = ('name',) 276 277admin.site.register(models.Profiler, ProfilerAdmin) 278 279 280class AclGroupAdmin(SiteAdmin): 281 list_display = ('name', 'description') 282 search_fields = ('name',) 283 filter_horizontal = ('users', 'hosts') 284 285 def queryset(self, request): 286 return models.AclGroup.objects.exclude(name='Everyone') 287 288 def save_model(self, request, obj, form, change): 289 super(AclGroupAdmin, self).save_model(request, obj, form, change) 290 _orig_save_m2m = form.save_m2m 291 292 def save_m2m(): 293 _orig_save_m2m() 294 obj.perform_after_save(change) 295 296 form.save_m2m = save_m2m 297 298admin.site.register(models.AclGroup, AclGroupAdmin) 299 300 301class DroneSetForm(forms.ModelForm): 302 def __init__(self, *args, **kwargs): 303 super(DroneSetForm, self).__init__(*args, **kwargs) 304 drone_ids_used = set() 305 for drone_set in models.DroneSet.objects.exclude(id=self.instance.id): 306 drone_ids_used.update(drone_set.drones.values_list('id', flat=True)) 307 available_drones = models.Drone.objects.exclude(id__in=drone_ids_used) 308 309 self.fields['drones'].widget.choices = [(drone.id, drone.hostname) 310 for drone in available_drones] 311 312 313class DroneSetAdmin(SiteAdmin): 314 filter_horizontal = ('drones',) 315 form = DroneSetForm 316 317admin.site.register(models.DroneSet, DroneSetAdmin) 318 319admin.site.register(models.Drone) 320 321 322if settings.FULL_ADMIN: 323 class JobAdmin(SiteAdmin): 324 list_display = ('id', 'owner', 'name', 'control_type') 325 filter_horizontal = ('dependency_labels',) 326 327 admin.site.register(models.Job, JobAdmin) 328 329 330 class IneligibleHostQueueAdmin(SiteAdmin): 331 list_display = ('id', 'job', 'host') 332 333 admin.site.register(models.IneligibleHostQueue, IneligibleHostQueueAdmin) 334 335 336 class HostQueueEntryAdmin(SiteAdmin): 337 list_display = ('id', 'job', 'host', 'status', 338 'meta_host') 339 340 admin.site.register(models.HostQueueEntry, HostQueueEntryAdmin) 341 342 admin.site.register(models.AbortedHostQueueEntry) 343