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