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