bug fixes
[plstackapi.git] / planetstack / core / admin.py
1 from core.models import Site
2 from core.models import *
3 from openstack.manager import OpenStackManager
4
5 from django.contrib import admin
6 from django.contrib.auth.models import Group
7 from django import forms
8 from django.utils.safestring import mark_safe
9 from django.contrib.auth.admin import UserAdmin
10 from django.contrib.admin.widgets import FilteredSelectMultiple, AdminTextareaWidget
11 from django.contrib.auth.forms import ReadOnlyPasswordHashField, AdminPasswordChangeForm
12 from django.contrib.auth.signals import user_logged_in
13 from django.utils import timezone
14 from django.contrib.contenttypes import generic
15 from suit.widgets import LinkedSelect
16 from django.core.exceptions import PermissionDenied
17 from django.core.urlresolvers import reverse, NoReverseMatch
18 from django.utils.encoding import force_text, python_2_unicode_compatible
19 from django.utils.html import conditional_escape, format_html
20 from django.forms.utils import flatatt, to_current_timezone
21 from cgi import escape as html_escape
22
23 import django_evolution
24 import threading
25
26 # thread locals necessary to work around a django-suit issue
27 _thread_locals = threading.local()
28
29 def backend_icon(obj): # backend_status, enacted, updated):
30     #return "%s %s %s" % (str(obj.updated), str(obj.enacted), str(obj.backend_status))
31     if (obj.enacted is not None) and obj.enacted >= obj.updated:
32         return '<span style="min-width:16px;"><img src="/static/admin/img/icon_success.gif"></span>'
33     else:
34         if obj.backend_status == "Provisioning in progress" or obj.backend_status=="":
35             return '<span style="min-width:16px;" title="%s"><img src="/static/admin/img/icon_clock.gif"></span>' % obj.backend_status
36         else:
37             return '<span style="min-width:16px;" title="%s"><img src="/static/admin/img/icon_error.gif"></span>' % html_escape(obj.backend_status, quote=True)
38
39 def backend_text(obj):
40     icon = backend_icon(obj)
41     if (obj.enacted is not None) and obj.enacted >= obj.updated:
42         return "%s %s" % (icon, "successfully enacted")
43     else:
44         return "%s %s" % (icon, html_escape(obj.backend_status, quote=True))
45
46 class UploadTextareaWidget(AdminTextareaWidget):
47     def render(self, name, value, attrs=None):
48         if value is None:
49             value = ''\r
50         final_attrs = self.build_attrs(attrs, name=name)\r
51         return format_html('<input type="file" style="width: 0; height: 0" id="btn_upload_%s" onChange="uploadTextarea(event,\'%s\');">' \\r
52                            '<button onClick="$(\'#btn_upload_%s\').click(); return false;">Upload</button>' \\r
53                            '<br><textarea{0}>\r\n{1}</textarea>' % (attrs["id"], attrs["id"], attrs["id"]),\r
54                            flatatt(final_attrs),\r
55                            force_text(value))
56
57 class PlainTextWidget(forms.HiddenInput):
58     input_type = 'hidden'
59
60     def render(self, name, value, attrs=None):
61         if value is None:
62             value = ''
63         return mark_safe(str(value) + super(PlainTextWidget, self).render(name, value, attrs))
64
65 class PermissionCheckingAdminMixin(object):
66     # call save_by_user and delete_by_user instead of save and delete
67
68     def has_add_permission(self, request, obj=None):
69         return (not self.__user_is_readonly(request))
70
71     def has_delete_permission(self, request, obj=None):
72         return (not self.__user_is_readonly(request))
73
74     def save_model(self, request, obj, form, change):
75         if self.__user_is_readonly(request):
76             # this 'if' might be redundant if save_by_user is implemented right
77             raise PermissionDenied
78
79         obj.caller = request.user
80         # update openstack connection to use this site/tenant
81         obj.save_by_user(request.user)
82
83     def delete_model(self, request, obj):
84         obj.delete_by_user(request.user)
85
86     def save_formset(self, request, form, formset, change):
87         instances = formset.save(commit=False)
88         for instance in instances:
89             instance.save_by_user(request.user)
90
91         # BUG in django 1.7? Objects are not deleted by formset.save if
92         # commit is False. So let's delete them ourselves.
93         #
94         # code from forms/models.py save_existing_objects()
95         try:
96             forms_to_delete = formset.deleted_forms\r
97         except AttributeError:\r
98             forms_to_delete = []
99         if formset.initial_forms:
100             for form in formset.initial_forms:
101                 obj = form.instance
102                 if form in forms_to_delete:
103                     if obj.pk is None:
104                         continue
105                     formset.deleted_objects.append(obj)
106                     obj.delete()
107
108         formset.save_m2m()
109
110     def get_actions(self,request):
111         actions = super(PermissionCheckingAdminMixin,self).get_actions(request)
112
113         if self.__user_is_readonly(request):
114             if 'delete_selected' in actions:
115                 del actions['delete_selected']
116
117         return actions
118
119     def change_view(self,request,object_id, extra_context=None):
120         if self.__user_is_readonly(request):
121             if not hasattr(self, "readonly_save"):\r
122                 # save the original readonly fields\r
123                 self.readonly_save = self.readonly_fields\r
124                 self.inlines_save = self.inlines\r
125             if hasattr(self, "user_readonly_fields"):\r
126                 self.readonly_fields=self.user_readonly_fields\r
127             if hasattr(self, "user_readonly_inlines"):\r
128                 self.inlines = self.user_readonly_inlines\r
129         else:\r
130             if hasattr(self, "readonly_save"):\r
131                 # restore the original readonly fields\r
132                 self.readonly_fields = self.readonly_save\r
133             if hasattr(self, "inlines_save"):\r
134                 self.inlines = self.inlines_save
135
136         try:
137             return super(PermissionCheckingAdminMixin, self).change_view(request, object_id, extra_context=extra_context)
138         except PermissionDenied:
139             pass
140         if request.method == 'POST':
141             raise PermissionDenied
142         request.readonly = True
143         return super(PermissionCheckingAdminMixin, self).change_view(request, object_id, extra_context=extra_context)
144
145     def __user_is_readonly(self, request):
146         return request.user.isReadOnlyUser()
147
148     def backend_status_text(self, obj):
149         return mark_safe(backend_text(obj))
150
151     def backend_status_icon(self, obj):
152         return mark_safe(backend_icon(obj))
153     backend_status_icon.short_description = ""
154
155     def get_form(self, request, obj=None, **kwargs):
156         # Save obj and request in thread-local storage, so suit_form_tabs can
157         # use it to determine whether we're in edit or add mode, and can
158         # determine whether the user is an admin.
159         _thread_locals.request = request
160         _thread_locals.obj = obj
161         return super(PermissionCheckingAdminMixin, self).get_form(request, obj, **kwargs)
162
163     def get_inline_instances(self, request, obj=None):
164         inlines = super(PermissionCheckingAdminMixin, self).get_inline_instances(request, obj)
165
166         # inlines that should only be shown to an admin user
167         if request.user.is_admin:
168             for inline_class in getattr(self, "admin_inlines", []):
169                 inlines.append(inline_class(self.model, self.admin_site))
170
171         return inlines
172
173 class ReadOnlyAwareAdmin(PermissionCheckingAdminMixin, admin.ModelAdmin):
174     # Note: Make sure PermissionCheckingAdminMixin is listed before
175     # admin.ModelAdmin in the class declaration.
176
177     pass
178
179 class PlanetStackBaseAdmin(ReadOnlyAwareAdmin):
180     save_on_top = False
181
182 class SingletonAdmin (ReadOnlyAwareAdmin):
183     def has_add_permission(self, request):
184         if not super(SingletonAdmin, self).has_add_permission(request):
185             return False
186
187         num_objects = self.model.objects.count()
188         if num_objects >= 1:
189             return False
190         else:
191             return True
192
193 class PlStackTabularInline(admin.TabularInline):
194     def __init__(self, *args, **kwargs):
195         super(PlStackTabularInline, self).__init__(*args, **kwargs)
196
197         # InlineModelAdmin as no get_fields() method, so in order to add
198         # the selflink field, we override __init__ to modify self.fields and
199         # self.readonly_fields.
200
201         self.setup_selflink()
202
203     def get_change_url(self, model, id):
204         """ Get the URL to a change form in the admin for this model """
205         reverse_path = "admin:%s_change" % (model._meta.db_table)
206         try:
207             url = reverse(reverse_path, args=(id,))
208         except NoReverseMatch:
209             return None
210
211         return url
212
213     def setup_selflink(self):
214         if hasattr(self, "selflink_fieldname"):
215             """ self.selflink_model can be defined to punch through a relation
216                 to its target object. For example, in SliceNetworkInline, set
217                 selflink_model = "network", and the URL will lead to the Network
218                 object instead of trying to bring up a change view of the
219                 SliceNetwork object.
220             """
221             self.selflink_model = getattr(self.model,self.selflink_fieldname).field.rel.to
222         else:
223             self.selflink_model = self.model
224
225         url = self.get_change_url(self.selflink_model, 0)
226
227         # We don't have an admin for this object, so don't create the
228         # selflink.
229         if (url == None):
230             return
231
232         # Since we need to add "selflink" to the field list, we need to create
233         # self.fields if it is None.
234         if (self.fields is None):
235             self.fields = []
236             for f in self.model._meta.fields:
237                 if f.editable and f.name != "id":
238                     self.fields.append(f.name)
239
240         self.fields = tuple(self.fields) + ("selflink", )
241
242         if self.readonly_fields is None:
243             self.readonly_fields = ()
244
245         self.readonly_fields = tuple(self.readonly_fields) + ("selflink", )
246
247     def selflink(self, obj):
248         if hasattr(self, "selflink_fieldname"):
249             obj = getattr(obj, self.selflink_fieldname)
250
251         if obj.id:
252             url = self.get_change_url(self.selflink_model, obj.id)
253             return "<a href='%s'>Details</a>" % str(url)
254         else:\r
255             return "Not present"\r
256
257     selflink.allow_tags = True
258     selflink.short_description = "Details"
259
260     def has_add_permission(self, request):
261         return not request.user.isReadOnlyUser()
262
263     def get_readonly_fields(self, request, obj=None):
264         readonly_fields = list(self.readonly_fields)[:]
265         if request.user.isReadOnlyUser():
266             for field in self.fields:
267                 if not field in readonly_fields:
268                     readonly_fields.append(field)
269         return readonly_fields
270
271     def backend_status_icon(self, obj):
272         return mark_safe(backend_icon(obj))
273     backend_status_icon.short_description = ""
274
275 class PlStackGenericTabularInline(generic.GenericTabularInline):
276     def has_add_permission(self, request):
277         return not request.user.isReadOnlyUser()
278
279     def get_readonly_fields(self, request, obj=None):
280         readonly_fields = list(self.readonly_fields)[:]
281         if request.user.isReadOnlyUser():
282             for field in self.fields:
283                 if not field in readonly_fields:
284                     readonly_fields.append(field)
285         return readonly_fields
286
287     def backend_status_icon(self, obj):
288         return mark_safe(backend_icon(obj))
289     backend_status_icon.short_description = ""
290
291 class ReservationInline(PlStackTabularInline):
292     model = Reservation
293     extra = 0
294     suit_classes = 'suit-tab suit-tab-reservations'
295
296     def queryset(self, request):
297         return Reservation.select_by_user(request.user)
298
299 class TagInline(PlStackGenericTabularInline):
300     model = Tag
301     extra = 0
302     suit_classes = 'suit-tab suit-tab-tags'
303     fields = ['service', 'name', 'value']
304
305     def queryset(self, request):
306         return Tag.select_by_user(request.user)
307
308 class NetworkLookerUpper:
309     """ This is a callable that looks up a network name in a sliver and returns
310         the ip address for that network.
311     """
312
313     byNetworkName = {}    # class variable
314
315     def __init__(self, name):
316         self.short_description = name
317         self.__name__ = name
318         self.network_name = name
319
320     def __call__(self, obj):
321         if obj is not None:
322             for nbs in obj.networksliver_set.all():
323                 if (nbs.network.name == self.network_name):
324                     return nbs.ip
325         return ""
326
327     def __str__(self):
328         return self.network_name
329
330     @staticmethod
331     def get(network_name):
332         """ We want to make sure we alwars return the same NetworkLookerUpper
333             because sometimes django will cause them to be instantiated multiple
334             times (and we don't want different ones in form.fields vs
335             SliverInline.readonly_fields).
336         """
337         if network_name not in NetworkLookerUpper.byNetworkName:
338             NetworkLookerUpper.byNetworkName[network_name] = NetworkLookerUpper(network_name)
339         return NetworkLookerUpper.byNetworkName[network_name]
340
341 class SliverInline(PlStackTabularInline):
342     model = Sliver
343     fields = ['backend_status_icon', 'all_ips_string', 'instance_name', 'slice', 'deployment', 'flavor', 'image', 'node']
344     extra = 0
345     readonly_fields = ['backend_status_icon', 'all_ips_string', 'instance_name']
346     suit_classes = 'suit-tab suit-tab-slivers'
347
348     def queryset(self, request):
349         return Sliver.select_by_user(request.user)
350
351     def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
352         if db_field.name == 'deployment':
353            kwargs['queryset'] = Deployment.select_by_acl(request.user)
354            kwargs['widget'] = forms.Select(attrs={'onChange': "sliver_deployment_changed(this);"})
355         if db_field.name == 'flavor':
356            kwargs['widget'] = forms.Select(attrs={'onChange': "sliver_flavor_changed(this);"})
357
358         field = super(SliverInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
359
360         return field
361
362 class SiteInline(PlStackTabularInline):
363     model = Site
364     extra = 0
365     suit_classes = 'suit-tab suit-tab-sites'
366
367     def queryset(self, request):
368         return Site.select_by_user(request.user)
369
370 class UserInline(PlStackTabularInline):
371     model = User
372     fields = ['backend_status_icon', 'email', 'firstname', 'lastname']
373     readonly_fields = ('backend_status_icon', )
374     extra = 0
375     suit_classes = 'suit-tab suit-tab-users'
376
377     def queryset(self, request):
378         return User.select_by_user(request.user)
379
380 class SliceInline(PlStackTabularInline):
381     model = Slice
382     fields = ['backend_status_icon', 'name', 'site', 'serviceClass', 'service']
383     readonly_fields = ('backend_status_icon', )
384     extra = 0
385     suit_classes = 'suit-tab suit-tab-slices'
386
387     def queryset(self, request):
388         return Slice.select_by_user(request.user)
389
390 class NodeInline(PlStackTabularInline):
391     model = Node
392     extra = 0
393     suit_classes = 'suit-tab suit-tab-nodes'
394     fields = ['backend_status_icon', 'name', 'site_deployment']
395     readonly_fields = ('backend_status_icon', )
396
397 class DeploymentPrivilegeInline(PlStackTabularInline):
398     model = DeploymentPrivilege
399     extra = 0
400     suit_classes = 'suit-tab suit-tab-admin-only'
401     fields = ['backend_status_icon', 'user','role','deployment']
402     readonly_fields = ('backend_status_icon', )
403
404     def queryset(self, request):
405         return DeploymentPrivilege.select_by_user(request.user)
406
407 class ControllerPrivilegeInline(PlStackTabularInline):
408     model = ControllerPrivilege
409     extra = 0
410     suit_classes = 'suit-tab suit-tab-admin-only'
411     fields = ['backend_status_icon', 'user','role','controller']
412     readonly_fields = ('backend_status_icon', )
413
414     def queryset(self, request):
415         return ControllerPrivilege.select_by_user(request.user)
416
417 class ControllerSiteInline(PlStackTabularInline):
418     model = ControllerSite
419     extra = 0
420     suit_classes = 'suit-tab suit-tab-admin-only'
421     fields = ['controller', 'site', 'tenant_id']
422
423
424 class SitePrivilegeInline(PlStackTabularInline):
425     model = SitePrivilege
426     extra = 0
427     suit_classes = 'suit-tab suit-tab-siteprivileges'
428     fields = ['backend_status_icon', 'user','site', 'role']
429     readonly_fields = ('backend_status_icon', )
430
431     def formfield_for_foreignkey(self, db_field, request, **kwargs):
432         if db_field.name == 'site':
433             kwargs['queryset'] = Site.select_by_user(request.user)
434
435         if db_field.name == 'user':
436             kwargs['queryset'] = User.select_by_user(request.user)
437         return super(SitePrivilegeInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
438
439     def queryset(self, request):
440         return SitePrivilege.select_by_user(request.user)
441
442 class SiteDeploymentInline(PlStackTabularInline):
443     model = SiteDeployment
444     extra = 0
445     suit_classes = 'suit-tab suit-tab-deployments'
446     fields = ['backend_status_icon', 'deployment','site', 'controller']
447     readonly_fields = ('backend_status_icon', )
448
449     def formfield_for_foreignkey(self, db_field, request, **kwargs):
450         if db_field.name == 'site':
451             kwargs['queryset'] = Site.select_by_user(request.user)
452
453         if db_field.name == 'deployment':
454             kwargs['queryset'] = Deployment.select_by_user(request.user)
455
456         if db_field.name == 'controller':
457             kwargs['queryset'] = Controller.select_by_user(request.user)
458
459         return super(SiteDeploymentInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
460
461     def queryset(self, request):
462         return SiteDeployment.select_by_user(request.user)
463
464
465 class SlicePrivilegeInline(PlStackTabularInline):
466     model = SlicePrivilege
467     suit_classes = 'suit-tab suit-tab-sliceprivileges'
468     extra = 0
469     fields = ('backend_status_icon', 'user', 'slice', 'role')
470     readonly_fields = ('backend_status_icon', )
471
472     def formfield_for_foreignkey(self, db_field, request, **kwargs):
473         if db_field.name == 'slice':
474            kwargs['queryset'] = Slice.select_by_user(request.user)
475         if db_field.name == 'user':
476            kwargs['queryset'] = User.select_by_user(request.user)
477
478         return super(SlicePrivilegeInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
479
480     def queryset(self, request):
481         return SlicePrivilege.select_by_user(request.user)
482
483 class SliceNetworkInline(PlStackTabularInline):
484     model = Network.slices.through
485     selflink_fieldname = "network"
486     extra = 0
487     verbose_name = "Network Connection"
488     verbose_name_plural = "Network Connections"
489     suit_classes = 'suit-tab suit-tab-slicenetworks'
490     fields = ['backend_status_icon', 'network']
491     readonly_fields = ('backend_status_icon', )
492
493 class ImageDeploymentsInline(PlStackTabularInline):
494     model = ImageDeployments
495     extra = 0
496     verbose_name = "Image Deployments"
497     verbose_name_plural = "Image Deployments"
498     suit_classes = 'suit-tab suit-tab-imagedeployments'
499     fields = ['backend_status_icon', 'image', 'deployment']
500     readonly_fields = ['backend_status_icon']
501
502 class ControllerImagesInline(PlStackTabularInline):
503     model = ControllerImages
504     extra = 0
505     verbose_name = "Controller Images"
506     verbose_name_plural = "Controller Images"
507     suit_classes = 'suit-tab suit-tab-admin-only'
508     fields = ['backend_status_icon', 'image', 'controller', 'glance_image_id']
509     readonly_fields = ['backend_status_icon', 'glance_image_id']
510
511 class SliceRoleAdmin(PlanetStackBaseAdmin):
512     model = SliceRole
513     pass
514
515 class SiteRoleAdmin(PlanetStackBaseAdmin):
516     model = SiteRole
517     pass
518
519 class DeploymentAdminForm(forms.ModelForm):
520     sites = forms.ModelMultipleChoiceField(
521         queryset=Site.objects.all(),
522         required=False,
523         help_text="Select which sites are allowed to host nodes in this deployment",
524         widget=FilteredSelectMultiple(
525             verbose_name=('Sites'), is_stacked=False
526         )
527     )
528     images = forms.ModelMultipleChoiceField(
529         queryset=Image.objects.all(),
530         required=False,
531         help_text="Select which images should be deployed on this deployment",
532         widget=FilteredSelectMultiple(
533             verbose_name=('Images'), is_stacked=False
534         )
535     )
536     flavors = forms.ModelMultipleChoiceField(
537         queryset=Flavor.objects.all(),
538         required=False,
539         help_text="Select which flavors should be usable on this deployment",
540         widget=FilteredSelectMultiple(
541             verbose_name=('Flavors'), is_stacked=False
542         )
543     )
544     class Meta:
545         model = Deployment
546         many_to_many = ["flavors",]
547
548     def __init__(self, *args, **kwargs):
549       request = kwargs.pop('request', None)
550       super(DeploymentAdminForm, self).__init__(*args, **kwargs)
551
552       self.fields['accessControl'].initial = "allow site " + request.user.site.name
553
554       if self.instance and self.instance.pk:
555         self.fields['sites'].initial = [x.site for x in self.instance.sitedeployments.all()]
556         self.fields['images'].initial = [x.image for x in self.instance.imagedeployments.all()]
557         self.fields['flavors'].initial = self.instance.flavors.all()
558
559     def manipulate_m2m_objs(self, this_obj, selected_objs, all_relations, relation_class, local_attrname, foreign_attrname):
560         """ helper function for handling m2m relations from the MultipleChoiceField
561
562             this_obj: the source object we want to link from
563
564             selected_objs: a list of destination objects we want to link to
565
566             all_relations: the full set of relations involving this_obj, including ones we don't want
567
568             relation_class: the class that implements the relation from source to dest
569
570             local_attrname: field name representing this_obj in relation_class
571
572             foreign_attrname: field name representing selected_objs in relation_class
573
574             This function will remove all newobjclass relations from this_obj
575             that are not contained in selected_objs, and add any relations that
576             are in selected_objs but don't exist in the data model yet.
577         """
578
579         existing_dest_objs = []
580         for relation in list(all_relations):
581             if getattr(relation, foreign_attrname) not in selected_objs:
582                 #print "deleting site", sdp.site
583                 relation.delete()
584             else:
585                 existing_dest_objs.append(getattr(relation, foreign_attrname))
586
587         for dest_obj in selected_objs:
588             if dest_obj not in existing_dest_objs:
589                 #print "adding site", site
590                 kwargs = {foreign_attrname: dest_obj, local_attrname: this_obj}
591                 relation = relation_class(**kwargs)
592                 relation.save()
593
594     def save(self, commit=True):
595       deployment = super(DeploymentAdminForm, self).save(commit=False)
596
597       if commit:
598         deployment.save()
599         # this has to be done after save() if/when a deployment is first created
600         deployment.flavors = self.cleaned_data['flavors']
601
602       if deployment.pk:
603         # save_m2m() doesn't seem to work with 'through' relations. So we
604         #    create/destroy the through models ourselves. There has to be
605         #    a better way...
606
607         self.manipulate_m2m_objs(deployment, self.cleaned_data['sites'], deployment.sitedeployments.all(), SiteDeployment, "deployment", "site")
608         self.manipulate_m2m_objs(deployment, self.cleaned_data['images'], deployment.imagedeployments.all(), ImageDeployments, "deployment", "image")
609         # manipulate_m2m_objs doesn't work for Flavor/Deployment relationship
610         # so well handle that manually here
611         for flavor in deployment.flavors.all():
612             if getattr(flavor, 'name') not in self.cleaned_data['flavors']:
613                 deployment.flavors.remove(flavor)
614         for flavor in self.cleaned_data['flavors']:
615             if flavor not in deployment.flavors.all():
616                 flavor.deployments.add(deployment)
617
618       self.save_m2m()
619
620       return deployment
621
622 class DeploymentAdminROForm(DeploymentAdminForm):
623     def save(self, commit=True):
624         raise PermissionDenied
625
626 class SiteAssocInline(PlStackTabularInline):
627     model = Site.deployments.through
628     extra = 0
629     suit_classes = 'suit-tab suit-tab-sites'
630
631 class DeploymentAdmin(PlanetStackBaseAdmin):
632     model = Deployment
633     fieldList = ['backend_status_text', 'name', 'sites', 'images', 'flavors', 'accessControl']
634     fieldsets = [(None, {'fields': fieldList, 'classes':['suit-tab suit-tab-sites']})]
635     # node no longer directly connected to deployment
636     #inlines = [DeploymentPrivilegeInline,NodeInline,TagInline,ImageDeploymentsInline]
637     inlines = [DeploymentPrivilegeInline,TagInline,ImageDeploymentsInline]
638     list_display = ['backend_status_icon', 'name']
639     list_display_links = ('backend_status_icon', 'name', )
640     readonly_fields = ('backend_status_text', )
641
642     user_readonly_fields = ['name']
643
644     # nodes no longer direclty connected to deployments
645     #suit_form_tabs =(('sites','Deployment Details'),('nodes','Nodes'),('deploymentprivileges','Privileges'),('tags','Tags'),('imagedeployments','Images'))
646     suit_form_tabs =(('sites','Deployment Details'),('deploymentprivileges','Privileges'),('tags','Tags'),('imagedeployments','Images'))
647
648     def get_form(self, request, obj=None, **kwargs):
649         if request.user.isReadOnlyUser():
650             kwargs["form"] = DeploymentAdminROForm
651         else:
652             kwargs["form"] = DeploymentAdminForm
653         adminForm = super(DeploymentAdmin,self).get_form(request, obj, **kwargs)
654
655         # from stackexchange: pass the request object into the form
656
657         class AdminFormMetaClass(adminForm):
658            def __new__(cls, *args, **kwargs):
659                kwargs['request'] = request
660                return adminForm(*args, **kwargs)
661
662         return AdminFormMetaClass
663
664 class ControllerAdminForm(forms.ModelForm):
665     sites = forms.ModelMultipleChoiceField(
666         queryset=Site.objects.all(),
667         required=False,
668         help_text="Select which sites are managed by this controller",
669         widget=FilteredSelectMultiple(
670             verbose_name=('Sites'), is_stacked=False
671         )
672     )
673
674     class Meta: 
675         model = Controller
676         
677     def __init__(self, *args, **kwargs):
678         request = kwargs.pop('request', None)
679         super(ControllerAdminForm, self).__init__(*args, **kwargs)  
680
681         if self.instance and self.instance.pk:
682             self.fields['sites'].initial = [x.site_deployment for x in self.instance.controllersite.all()]
683
684     def manipulate_m2m_objs(self, this_obj, selected_objs, all_relations, relation_class, local_attrname, foreign_attrname):
685         """ helper function for handling m2m relations from the MultipleChoiceField
686             this_obj: the source object we want to link from
687             selected_objs: a list of destination objects we want to link to
688             all_relations: the full set of relations involving this_obj, including ones we don't want
689             relation_class: the class that implements the relation from source to dest
690             local_attrname: field name representing this_obj in relation_class
691             foreign_attrname: field name representing selected_objs in relation_class
692             This function will remove all newobjclass relations from this_obj
693             that are not contained in selected_objs, and add any relations that
694             are in selected_objs but don't exist in the data model yet.
695         """
696         existing_dest_objs = []
697         for relation in list(all_relations):
698             if getattr(relation, foreign_attrname) not in selected_objs:
699                 #print "deleting site", sdp.site
700                 relation.delete()
701             else:
702                 existing_dest_objs.append(getattr(relation, foreign_attrname))
703
704         for dest_obj in selected_objs:
705             if dest_obj not in existing_dest_objs:
706                 #print "adding site", site
707                 kwargs = {foreign_attrname: dest_obj, local_attrname: this_obj}
708                 relation = relation_class(**kwargs)
709                 relation.save()
710
711     def save(self, commit=True):
712         controller = super(ControllerAdminForm, self).save(commit=False)
713         if commit:
714             controller.save()
715
716         if controller.pk:
717             # save_m2m() doesn't seem to work with 'through' relations. So we
718             #    create/destroy the through models ourselves. There has to be
719             #    a better way...
720             self.manipulate_m2m_objs(controller, self.cleaned_data['sites'], controller.controllersite.all(), ControllerSite, "controller", "site")
721             pass
722         
723         self.save_m2m()
724
725         return controller 
726       
727 class ControllerAdmin(PlanetStackBaseAdmin):
728     model = Controller 
729     fieldList = ['name', 'version', 'backend_type', 'auth_url', 'admin_user', 'admin_tenant','admin_password']
730     #fieldsets = [(None, {'fields': fieldList, 'classes':['suit-tab suit-tab-general']})]
731     inlines = [ControllerSiteInline] # ,ControllerImagesInline]
732     list_display = ['backend_status_icon', 'name', 'version', 'backend_type']
733     list_display_links = ('backend_status_icon', 'name', )
734     readonly_fields = ('backend_status_text',)
735
736     user_readonly_fields = []
737
738     def get_form(self, request, obj=None, **kwargs):
739         print self.fieldsets
740         if request.user.isReadOnlyUser():
741             kwargs["form"] = ControllerAdminROForm
742         else:
743             kwargs["form"] = ControllerAdminForm
744         adminForm = super(ControllerAdmin,self).get_form(request, obj, **kwargs)
745
746         # from stackexchange: pass the request object into the form
747
748         class AdminFormMetaClass(adminForm):
749            def __new__(cls, *args, **kwargs):
750                kwargs['request'] = request
751                return adminForm(*args, **kwargs)
752
753         return AdminFormMetaClass
754
755 class ServiceAttrAsTabInline(PlStackTabularInline):
756     model = ServiceAttribute
757     fields = ['name','value']
758     extra = 0
759     suit_classes = 'suit-tab suit-tab-serviceattrs'
760
761 class ServiceAdmin(PlanetStackBaseAdmin):
762     list_display = ("backend_status_icon","name","description","versionNumber","enabled","published")
763     list_display_links = ('backend_status_icon', 'name', )
764     fieldList = ["backend_status_text","name","description","versionNumber","enabled","published"]
765     fieldsets = [(None, {'fields': fieldList, 'classes':['suit-tab suit-tab-general']})]
766     inlines = [ServiceAttrAsTabInline,SliceInline]
767     readonly_fields = ('backend_status_text', )
768
769     user_readonly_fields = fieldList
770
771     suit_form_tabs =(('general', 'Service Details'),
772         ('slices','Slices'),
773         ('serviceattrs','Additional Attributes'),
774     )
775
776 class SiteAdmin(PlanetStackBaseAdmin):
777     fieldList = ['backend_status_text', 'name', 'site_url', 'enabled', 'is_public', 'login_base', 'accountLink','location']
778     fieldsets = [
779         (None, {'fields': fieldList, 'classes':['suit-tab suit-tab-general']}),
780         #('Deployment Networks', {'fields': ['deployments'], 'classes':['suit-tab suit-tab-deployments']}),
781     ]
782     suit_form_tabs =(('general', 'Site Details'),
783         ('users','Users'),
784         ('siteprivileges','Privileges'),
785         ('deployments','Deployments'),
786         ('slices','Slices'),
787         #('nodes','Nodes'),
788         ('tags','Tags'),
789     )
790     readonly_fields = ['backend_status_text', 'accountLink']
791
792     user_readonly_fields = ['name', 'deployments','site_url', 'enabled', 'is_public', 'login_base', 'accountLink']
793
794     list_display = ('backend_status_icon', 'name', 'login_base','site_url', 'enabled')
795     list_display_links = ('backend_status_icon', 'name', )
796     filter_horizontal = ('deployments',)
797     inlines = [SliceInline,UserInline,TagInline, SitePrivilegeInline, SiteDeploymentInline]
798     search_fields = ['name']
799
800     def queryset(self, request):
801         return Site.select_by_user(request.user)
802
803     def get_formsets(self, request, obj=None):
804         for inline in self.get_inline_instances(request, obj):
805             # hide MyInline in the add view
806             if obj is None:
807                 continue
808             if isinstance(inline, SliverInline):
809                 inline.model.caller = request.user
810             yield inline.get_formset(request, obj)
811
812     def accountLink(self, obj):
813         link_obj = obj.accounts.all()
814         if link_obj:
815             reverse_path = "admin:core_account_change"
816             url = reverse(reverse_path, args =(link_obj[0].id,))
817             return "<a href='%s'>%s</a>" % (url, "view billing details")
818         else:
819             return "no billing data for this site"
820     accountLink.allow_tags = True
821     accountLink.short_description = "Billing"
822
823     def save_model(self, request, obj, form, change):
824         # update openstack connection to use this site/tenant
825         obj.save_by_user(request.user) 
826
827     def delete_model(self, request, obj):
828         obj.delete_by_user(request.user)
829         
830
831 class SitePrivilegeAdmin(PlanetStackBaseAdmin):
832     fieldList = ['backend_status_text', 'user', 'site', 'role']
833     fieldsets = [
834         (None, {'fields': fieldList, 'classes':['collapse']})
835     ]
836     readonly_fields = ('backend_status_text', )
837     list_display = ('backend_status_icon', 'user', 'site', 'role')
838     list_display_links = list_display
839     user_readonly_fields = fieldList
840     user_readonly_inlines = []
841
842     def formfield_for_foreignkey(self, db_field, request, **kwargs):
843         if db_field.name == 'site':
844             if not request.user.is_admin:
845                 # only show sites where user is an admin or pi
846                 sites = set()
847                 for site_privilege in SitePrivilege.objects.filer(user=request.user):
848                     if site_privilege.role.role_type in ['admin', 'pi']:
849                         sites.add(site_privilege.site)
850                 kwargs['queryset'] = Site.objects.filter(site__in=list(sites))
851
852         if db_field.name == 'user':
853             if not request.user.is_admin:
854                 # only show users from sites where caller has admin or pi role
855                 roles = Role.objects.filter(role_type__in=['admin', 'pi'])
856                 site_privileges = SitePrivilege.objects.filter(user=request.user).filter(role__in=roles)
857                 sites = [site_privilege.site for site_privilege in site_privileges]
858                 site_privileges = SitePrivilege.objects.filter(site__in=sites)
859                 emails = [site_privilege.user.email for site_privilege in site_privileges]
860                 users = User.objects.filter(email__in=emails)
861                 kwargs['queryset'] = users
862
863         return super(SitePrivilegeAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
864
865     def queryset(self, request):
866         # admins can see all privileges. Users can only see privileges at sites
867         # where they have the admin role or pi role.
868         qs = super(SitePrivilegeAdmin, self).queryset(request)
869         #if not request.user.is_admin:
870         #    roles = Role.objects.filter(role_type__in=['admin', 'pi'])
871         #    site_privileges = SitePrivilege.objects.filter(user=request.user).filter(role__in=roles)
872         #    login_bases = [site_privilege.site.login_base for site_privilege in site_privileges]
873         #    sites = Site.objects.filter(login_base__in=login_bases)
874         #    qs = qs.filter(site__in=sites)
875         return qs
876
877 class SliceForm(forms.ModelForm):
878     class Meta:
879         model = Slice
880         widgets = {
881             'service': LinkedSelect
882         }
883
884     def clean(self):
885         cleaned_data = super(SliceForm, self).clean()
886         name = cleaned_data.get('name')
887         site = cleaned_data.get('site')
888         slice_id = self.instance.id
889         if not site and slice_id:
890             site = Slice.objects.get(id=slice_id).site
891         if (not isinstance(site,Site)):
892             # previous code indicates 'site' could be a site_id and not a site?
893             site = Slice.objects.get(id=site.id)
894         if not name.startswith(site.login_base):
895             raise forms.ValidationError('slice name must begin with %s' % site.login_base)
896         return cleaned_data
897
898 class ControllerSliceInline(PlStackTabularInline):
899     model = ControllerSlice
900     extra = 0
901     verbose_name = "Controller Slices"
902     verbose_name_plural = "Controller Slices"
903     suit_classes = 'suit-tab suit-tab-admin-only'
904     fields = ['backend_status_icon', 'controller', 'tenant_id']
905     readonly_fields = ('backend_status_icon', )
906
907 class SliceAdmin(PlanetStackBaseAdmin):
908     form = SliceForm
909     fieldList = ['backend_status_text', 'site', 'name', 'serviceClass', 'enabled','description', 'service', 'slice_url', 'max_slivers']
910     fieldsets = [('Slice Details', {'fields': fieldList, 'classes':['suit-tab suit-tab-general']}),]
911     readonly_fields = ('backend_status_text', )
912     list_display = ('backend_status_icon', 'name', 'site','serviceClass', 'slice_url', 'max_slivers')
913     list_display_links = ('backend_status_icon', 'name', )
914     inlines = [SlicePrivilegeInline,SliverInline, TagInline, ReservationInline,SliceNetworkInline]
915     admin_inlines = [ControllerSliceInline]
916
917     user_readonly_fields = fieldList
918
919     @property
920     def suit_form_tabs(self):
921         tabs =[('general', 'Slice Details'),
922           ('slicenetworks','Networks'),
923           ('sliceprivileges','Privileges'),
924           ('slivers','Slivers'),
925           ('tags','Tags'),
926           ('reservations','Reservations'),
927           ]
928
929         request=getattr(_thread_locals, "request", None)
930         if request and request.user.is_admin:
931             tabs.append( ('admin-only', 'Admin-Only') )
932
933         return tabs
934     
935     def add_view(self, request, form_url='', extra_context=None):
936         # revert to default read-only fields
937         self.readonly_fields = ('backend_status_text',)
938         return super(SliceAdmin, self).add_view(request, form_url, extra_context=extra_context)
939
940     def change_view(self, request, object_id, form_url='', extra_context=None):
941         # cannot change the site of an existing slice so make the site field read only
942         if object_id:
943             self.readonly_fields = ('backend_status_text','site')
944         return super(SliceAdmin, self).change_view(request, object_id, form_url)
945
946     def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
947         deployment_nodes = []
948         for node in Node.objects.all():
949             deployment_nodes.append( (node.site_deployment.id, node.id, node.name) )
950
951         deployment_flavors = []
952         for flavor in Flavor.objects.all():
953             for deployment in flavor.deployments.all():
954                 deployment_flavors.append( (deployment.id, flavor.id, flavor.name) )
955
956         deployment_images = []
957         for image in Image.objects.all():
958             for deployment_image in image.imagedeployments.all():
959                 deployment_images.append( (deployment_image.deployment.id, image.id, image.name) )
960
961         site_login_bases = []
962         for site in Site.objects.all():
963             site_login_bases.append((site.id, site.login_base))
964
965         context["deployment_nodes"] = deployment_nodes
966         context["deployment_flavors"] = deployment_flavors
967         context["deployment_images"] = deployment_images
968         context["site_login_bases"] = site_login_bases
969         return super(SliceAdmin, self).render_change_form(request, context, add, change, form_url, obj)
970
971     def formfield_for_foreignkey(self, db_field, request, **kwargs):
972         if db_field.name == 'site':
973             kwargs['queryset'] = Site.select_by_user(request.user)
974             kwargs['widget'] = forms.Select(attrs={'onChange': "update_slice_prefix(this, $($(this).closest('fieldset')[0]).find('.field-name input')[0].id)"})
975
976         return super(SliceAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
977
978     def queryset(self, request):
979         # admins can see all keys. Users can only see slices they belong to.
980         return Slice.select_by_user(request.user)
981
982     def get_formsets(self, request, obj=None):
983         for inline in self.get_inline_instances(request, obj):
984             # hide MyInline in the add view
985             if obj is None:
986                 continue
987             if isinstance(inline, SliverInline):
988                 inline.model.caller = request.user
989             yield inline.get_formset(request, obj)
990
991 class SlicePrivilegeAdmin(PlanetStackBaseAdmin):
992     fieldsets = [
993         (None, {'fields': ['backend_status_text', 'user', 'slice', 'role']})
994     ]
995     readonly_fields = ('backend_status_text', )
996     list_display = ('backend_status_icon', 'user', 'slice', 'role')
997     list_display_links = list_display
998
999     user_readonly_fields = ['user', 'slice', 'role']
1000     user_readonly_inlines = []
1001
1002     def formfield_for_foreignkey(self, db_field, request, **kwargs):
1003         if db_field.name == 'slice':
1004             kwargs['queryset'] = Slice.select_by_user(request.user)
1005         
1006         if db_field.name == 'user':
1007             kwargs['queryset'] = User.select_by_user(request.user)
1008
1009         return super(SlicePrivilegeAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
1010
1011     def queryset(self, request):
1012         # admins can see all memberships. Users can only see memberships of
1013         # slices where they have the admin role.
1014         return SlicePrivilege.select_by_user(request.user)
1015
1016     def save_model(self, request, obj, form, change):
1017         # update openstack connection to use this site/tenant
1018         auth = request.session.get('auth', {})
1019         auth['tenant'] = obj.slice.slicename
1020         obj.os_manager = OpenStackManager(auth=auth, caller=request.user)
1021         obj.save()
1022
1023     def delete_model(self, request, obj):
1024         # update openstack connection to use this site/tenant
1025         auth = request.session.get('auth', {})
1026         auth['tenant'] = obj.slice.slicename
1027         obj.os_manager = OpenStackManager(auth=auth, caller=request.user)
1028         obj.delete()
1029
1030
1031 class ImageAdmin(PlanetStackBaseAdmin):
1032
1033     fieldsets = [('Image Details',
1034                    {'fields': ['backend_status_text', 'name', 'disk_format', 'container_format'],
1035                     'classes': ['suit-tab suit-tab-general']})
1036                ]
1037     readonly_fields = ('backend_status_text', )
1038
1039     suit_form_tabs =(('general','Image Details'),('slivers','Slivers'),('imagedeployments','Deployments'), ('controllerimages', 'Controllers'))
1040
1041     inlines = [SliverInline, ControllerImagesInline]
1042
1043     user_readonly_fields = ['name', 'disk_format', 'container_format']
1044
1045     list_display = ['backend_status_icon', 'name']
1046     list_display_links = ('backend_status_icon', 'name', )
1047
1048 class NodeForm(forms.ModelForm):
1049     class Meta:
1050         widgets = {
1051             'site': LinkedSelect,
1052             'deployment': LinkedSelect
1053         }
1054
1055 class NodeAdmin(PlanetStackBaseAdmin):
1056     form = NodeForm
1057     list_display = ('backend_status_icon', 'name', 'site_deployment')
1058     list_display_links = ('backend_status_icon', 'name', )
1059     list_filter = ('site_deployment',)
1060
1061     inlines = [TagInline,SliverInline]
1062     fieldsets = [('Node Details', {'fields': ['backend_status_text', 'name','site_deployment'], 'classes':['suit-tab suit-tab-details']})]
1063     readonly_fields = ('backend_status_text', )
1064
1065     user_readonly_fields = ['name','site_deployment']
1066     user_readonly_inlines = [TagInline,SliverInline]
1067
1068     suit_form_tabs =(('details','Node Details'),('slivers','Slivers'),('tags','Tags'))
1069
1070
1071 class SliverForm(forms.ModelForm):
1072     class Meta:
1073         model = Sliver
1074         ip = forms.CharField(widget=PlainTextWidget)
1075         instance_name = forms.CharField(widget=PlainTextWidget)
1076         widgets = {
1077             'ip': PlainTextWidget(),
1078             'instance_name': PlainTextWidget(),
1079             'slice': LinkedSelect,
1080             'deployment': LinkedSelect,
1081             'node': LinkedSelect,
1082             'image': LinkedSelect
1083         }
1084
1085 class TagAdmin(PlanetStackBaseAdmin):
1086     list_display = ['backend_status_icon', 'service', 'name', 'value', 'content_type', 'content_object',]
1087     list_display_links = list_display
1088     user_readonly_fields = ['service', 'name', 'value', 'content_type', 'content_object',]
1089     user_readonly_inlines = []
1090
1091 class SliverAdmin(PlanetStackBaseAdmin):
1092     form = SliverForm
1093     fieldsets = [
1094         ('Sliver Details', {'fields': ['backend_status_text', 'slice', 'deployment', 'node', 'ip', 'instance_name', 'flavor', 'image', ], 'classes': ['suit-tab suit-tab-general'], })
1095     ]
1096     readonly_fields = ('backend_status_text', )
1097     list_display = ['backend_status_icon', 'ip', 'instance_name', 'slice', 'flavor', 'image', 'node', 'deployment']
1098     list_display_links = ('backend_status_icon', 'ip',)
1099
1100     suit_form_tabs =(('general', 'Sliver Details'),
1101         ('tags','Tags'),
1102     )
1103
1104     inlines = [TagInline]
1105
1106     user_readonly_fields = ['slice', 'deployment', 'node', 'ip', 'instance_name', 'flavor', 'image']
1107
1108     def formfield_for_foreignkey(self, db_field, request, **kwargs):
1109         if db_field.name == 'slice':
1110             kwargs['queryset'] = Slice.select_by_user(request.user)
1111
1112         return super(SliverAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
1113
1114     def queryset(self, request):
1115         # admins can see all slivers. Users can only see slivers of
1116         # the slices they belong to.
1117         return Sliver.select_by_user(request.user)
1118
1119
1120     def get_formsets(self, request, obj=None):
1121         # make some fields read only if we are updating an existing record
1122         if obj == None:
1123             #self.readonly_fields = ('ip', 'instance_name')
1124             self.readonly_fields = ('backend_status_text',)
1125         else:
1126             self.readonly_fields = ('backend_status_text',)
1127             #self.readonly_fields = ('ip', 'instance_name', 'slice', 'image', 'key')
1128
1129         for inline in self.get_inline_instances(request, obj):
1130             # hide MyInline in the add view
1131             if obj is None:
1132                 continue
1133             if isinstance(inline, SliverInline):
1134                 inline.model.caller = request.user
1135             yield inline.get_formset(request, obj)
1136
1137     #def save_model(self, request, obj, form, change):
1138     #    # update openstack connection to use this site/tenant
1139     #    auth = request.session.get('auth', {})
1140     #    auth['tenant'] = obj.slice.name
1141     #    obj.os_manager = OpenStackManager(auth=auth, caller=request.user)
1142     #    obj.creator = request.user
1143     #    obj.save()
1144
1145     #def delete_model(self, request, obj):
1146     #    # update openstack connection to use this site/tenant
1147     #    auth = request.session.get('auth', {})
1148     #    auth['tenant'] = obj.slice.name
1149     #    obj.os_manager = OpenStackManager(auth=auth, caller=request.user)
1150     #    obj.delete()
1151
1152 class UserCreationForm(forms.ModelForm):
1153     """A form for creating new users. Includes all the required
1154     fields, plus a repeated password."""
1155     password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
1156     password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput)
1157
1158     class Meta:
1159         model = User
1160         fields = ('email', 'firstname', 'lastname', 'phone', 'public_key')
1161
1162     def clean_password2(self):
1163         # Check that the two password entries match
1164         password1 = self.cleaned_data.get("password1")
1165         password2 = self.cleaned_data.get("password2")
1166         if password1 and password2 and password1 != password2:
1167             raise forms.ValidationError("Passwords don't match")
1168         return password2
1169
1170     def save(self, commit=True):
1171         # Save the provided password in hashed format
1172         user = super(UserCreationForm, self).save(commit=False)
1173         user.password = self.cleaned_data["password1"]
1174         #user.set_password(self.cleaned_data["password1"])
1175         if commit:
1176             user.save()
1177         return user
1178
1179
1180 class UserChangeForm(forms.ModelForm):
1181     """A form for updating users. Includes all the fields on
1182     the user, but replaces the password field with admin's
1183     password hash display field.
1184     """
1185     password = ReadOnlyPasswordHashField(label='Password',
1186                    help_text= '<a href=\"password/\">Change Password</a>.')
1187
1188     class Meta:
1189         model = User
1190         widgets = { 'public_key': UploadTextareaWidget, }
1191
1192     def clean_password(self):
1193         # Regardless of what the user provides, return the initial value.
1194         # This is done here, rather than on the field, because the
1195         # field does not have access to the initial value
1196         return self.initial["password"]
1197
1198 class UserDashboardViewInline(PlStackTabularInline):
1199     model = UserDashboardView
1200     extra = 0
1201     suit_classes = 'suit-tab suit-tab-dashboards'
1202     fields = ['user', 'dashboardView', 'order']
1203
1204 class UserAdmin(PermissionCheckingAdminMixin, UserAdmin):
1205     # Note: Make sure PermissionCheckingAdminMixin is listed before
1206     # admin.ModelAdmin in the class declaration.
1207
1208     class Meta:
1209         app_label = "core"
1210
1211     # The forms to add and change user instances
1212     form = UserChangeForm
1213     add_form = UserCreationForm
1214
1215     # The fields to be used in displaying the User model.
1216     # These override the definitions on the base UserAdmin
1217     # that reference specific fields on auth.User.
1218     list_display = ('email', 'firstname', 'lastname', 'site', 'last_login')
1219     list_filter = ('site',)
1220     inlines = [SlicePrivilegeInline,SitePrivilegeInline,ControllerPrivilegeInline,UserDashboardViewInline]
1221
1222     fieldListLoginDetails = ['backend_status_text', 'email','site','password','is_active','is_readonly','is_admin','public_key']
1223     fieldListContactInfo = ['firstname','lastname','phone','timezone']
1224
1225     fieldsets = (
1226         ('Login Details', {'fields': ['backend_status_text', 'email', 'site','password', 'is_active', 'is_readonly', 'is_admin', 'public_key'], 'classes':['suit-tab suit-tab-general']}),
1227         ('Contact Information', {'fields': ('firstname','lastname','phone', 'timezone'), 'classes':['suit-tab suit-tab-contact']}),
1228         #('Dashboard Views', {'fields': ('dashboards',), 'classes':['suit-tab suit-tab-dashboards']}),
1229         #('Important dates', {'fields': ('last_login',)}),
1230     )
1231     add_fieldsets = (
1232         (None, {
1233             'classes': ('wide',),
1234             'fields': ('email', 'firstname', 'lastname', 'is_readonly', 'phone', 'public_key','password1', 'password2')},
1235         ),
1236     )
1237     readonly_fields = ('backend_status_text', )
1238     search_fields = ('email',)
1239     ordering = ('email',)
1240     filter_horizontal = ()
1241
1242     user_readonly_fields = fieldListLoginDetails + fieldListContactInfo
1243
1244     @property
1245     def suit_form_tabs(self):
1246         if getattr(_thread_locals, "obj", None) is None:
1247             return []
1248         else:
1249             return (('general','Login Details'),
1250                          ('contact','Contact Information'),
1251                          ('sliceprivileges','Slice Privileges'),
1252                          ('siteprivileges','Site Privileges'),
1253                          ('controllerprivileges','Controller Privileges'),
1254                          ('dashboards','Dashboard Views'))
1255
1256     def formfield_for_foreignkey(self, db_field, request, **kwargs):
1257         if db_field.name == 'site':
1258             kwargs['queryset'] = Site.select_by_user(request.user)
1259
1260         return super(UserAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
1261
1262     def queryset(self, request):
1263         return User.select_by_user(request.user)
1264
1265 class ControllerDashboardViewInline(PlStackTabularInline):
1266     model = ControllerDashboardView
1267     extra = 0
1268     fields = ["controller", "url"]
1269     suit_classes = 'suit-tab suit-tab-controllers'
1270
1271 class DashboardViewAdmin(PlanetStackBaseAdmin):
1272     fieldsets = [('Dashboard View Details',
1273                    {'fields': ['backend_status_text', 'name', 'url'],
1274                     'classes': ['suit-tab suit-tab-general']})
1275                ]
1276     readonly_fields = ('backend_status_text', )
1277     inlines = [ControllerDashboardViewInline]
1278
1279     suit_form_tabs =(('general','Dashboard View Details'),
1280                      ('controllers', 'Per-controller Dashboard Details'))
1281
1282 class ServiceResourceInline(PlStackTabularInline):
1283     model = ServiceResource
1284     extra = 0
1285
1286 class ServiceClassAdmin(PlanetStackBaseAdmin):
1287     list_display = ('backend_status_icon', 'name', 'commitment', 'membershipFee')
1288     list_display_links = ('backend_status_icon', 'name', )
1289     inlines = [ServiceResourceInline]
1290
1291     user_readonly_fields = ['name', 'commitment', 'membershipFee']
1292     user_readonly_inlines = []
1293
1294 class ReservedResourceInline(PlStackTabularInline):
1295     model = ReservedResource
1296     extra = 0
1297     suit_classes = 'suit-tab suit-tab-reservedresources'
1298
1299     def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
1300         field = super(ReservedResourceInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
1301
1302         if db_field.name == 'resource':
1303             # restrict resources to those that the slice's service class allows
1304             if request._slice is not None:
1305                 field.queryset = field.queryset.filter(serviceClass = request._slice.serviceClass, calendarReservable=True)
1306                 if len(field.queryset) > 0:
1307                     field.initial = field.queryset.all()[0]
1308             else:\r
1309                 field.queryset = field.queryset.none()\r
1310         elif db_field.name == 'sliver':\r
1311             # restrict slivers to those that belong to the slice\r
1312             if request._slice is not None:\r
1313                 field.queryset = field.queryset.filter(slice = request._slice)
1314             else:
1315                 field.queryset = field.queryset.none()\r
1316 \r
1317         return field
1318
1319     def queryset(self, request):
1320         return ReservedResource.select_by_user(request.user)
1321
1322 class ReservationChangeForm(forms.ModelForm):
1323     class Meta:
1324         model = Reservation
1325         widgets = {
1326             'slice' : LinkedSelect
1327         }
1328
1329 class ReservationAddForm(forms.ModelForm):
1330     slice = forms.ModelChoiceField(queryset=Slice.objects.all(), widget=forms.Select(attrs={"onChange":"document.getElementById('id_refresh').value=1; submit()"}))
1331     refresh = forms.CharField(widget=forms.HiddenInput())
1332
1333     class Media:
1334        css = {'all': ('planetstack.css',)}   # .field-refresh { display: none; }
1335
1336     def clean_slice(self):
1337         slice = self.cleaned_data.get("slice")
1338         x = ServiceResource.objects.filter(serviceClass = slice.serviceClass, calendarReservable=True)
1339         if len(x) == 0:
1340             raise forms.ValidationError("The slice you selected does not have a service class that allows reservations")
1341         return slice
1342
1343     class Meta:
1344         model = Reservation
1345         widgets = {
1346             'slice' : LinkedSelect
1347         }
1348
1349
1350 class ReservationAddRefreshForm(ReservationAddForm):
1351     """ This form is displayed when the Reservation Form receives an update
1352         from the Slice dropdown onChange handler. It doesn't validate the
1353         data and doesn't save the data. This will cause the form to be
1354         redrawn.
1355     """
1356
1357     """ don't validate anything other than slice """
1358     dont_validate_fields = ("startTime", "duration")
1359
1360     def full_clean(self):
1361         result = super(ReservationAddForm, self).full_clean()
1362
1363         for fieldname in self.dont_validate_fields:
1364             if fieldname in self._errors:
1365                 del self._errors[fieldname]
1366
1367         return result
1368
1369     """ don't save anything """
1370     def is_valid(self):
1371         return False
1372
1373 class ReservationAdmin(PlanetStackBaseAdmin):
1374     fieldList = ['backend_status_text', 'slice', 'startTime', 'duration']
1375     fieldsets = [('Reservation Details', {'fields': fieldList, 'classes': ['suit-tab suit-tab-general']})]
1376     readonly_fields = ('backend_status_text', )
1377     list_display = ('startTime', 'duration')
1378     form = ReservationAddForm
1379
1380     suit_form_tabs = (('general','Reservation Details'), ('reservedresources','Reserved Resources'))
1381
1382     inlines = [ReservedResourceInline]
1383     user_readonly_fields = fieldList
1384
1385     def add_view(self, request, form_url='', extra_context=None):
1386         timezone.activate(request.user.timezone)
1387         request._refresh = False
1388         request._slice = None
1389         if request.method == 'POST':
1390             # "refresh" will be set to "1" if the form was submitted due to
1391             # a change in the Slice dropdown.
1392             if request.POST.get("refresh","1") == "1":
1393                 request._refresh = True
1394                 request.POST["refresh"] = "0"
1395
1396             # Keep track of the slice that was selected, so the
1397             # reservedResource inline can filter items for the slice.
1398             request._slice = request.POST.get("slice",None)
1399             if (request._slice is not None):
1400                 request._slice = Slice.objects.get(id=request._slice)
1401
1402         result =  super(ReservationAdmin, self).add_view(request, form_url, extra_context)
1403         return result
1404
1405     def changelist_view(self, request, extra_context = None):
1406         timezone.activate(request.user.timezone)
1407         return super(ReservationAdmin, self).changelist_view(request, extra_context)
1408
1409     def get_form(self, request, obj=None, **kwargs):
1410         request._obj_ = obj
1411         if obj is not None:
1412             # For changes, set request._slice to the slice already set in the
1413             # object.
1414             request._slice = obj.slice
1415             self.form = ReservationChangeForm
1416         else:
1417             if getattr(request, "_refresh", False):
1418                 self.form = ReservationAddRefreshForm
1419             else:
1420                 self.form = ReservationAddForm
1421         return super(ReservationAdmin, self).get_form(request, obj, **kwargs)
1422
1423     def get_readonly_fields(self, request, obj=None):
1424         if (obj is not None):
1425             # Prevent slice from being changed after the reservation has been
1426             # created.
1427             return ['slice']
1428         else:
1429             return []
1430
1431     def queryset(self, request):
1432         return Reservation.select_by_user(request.user)
1433
1434 class NetworkParameterTypeAdmin(PlanetStackBaseAdmin):
1435     list_display = ("backend_status_icon", "name", )
1436     list_display_links = ('backend_status_icon', 'name', )
1437     user_readonly_fields = ['name']
1438     user_readonly_inlines = []
1439
1440 class RouterAdmin(PlanetStackBaseAdmin):
1441     list_display = ("backend_status_icon", "name", )
1442     list_display_links = ('backend_status_icon', 'name', )
1443     user_readonly_fields = ['name']
1444     user_readonly_inlines = []
1445
1446 class RouterInline(PlStackTabularInline):
1447     model = Router.networks.through
1448     extra = 0
1449     verbose_name_plural = "Routers"
1450     verbose_name = "Router"
1451     suit_classes = 'suit-tab suit-tab-routers'
1452
1453 class NetworkParameterInline(PlStackGenericTabularInline):
1454     model = NetworkParameter
1455     extra = 0
1456     verbose_name_plural = "Parameters"
1457     verbose_name = "Parameter"
1458     suit_classes = 'suit-tab suit-tab-netparams'
1459     fields = ['backend_status_icon', 'parameter', 'value']
1460     readonly_fields = ('backend_status_icon', )
1461
1462 class NetworkSliversInline(PlStackTabularInline):
1463     fields = ['backend_status_icon', 'network','sliver','ip']
1464     readonly_fields = ("backend_status_icon", "ip", )
1465     model = NetworkSliver
1466     selflink_fieldname = "sliver"
1467     extra = 0
1468     verbose_name_plural = "Slivers"
1469     verbose_name = "Sliver"
1470     suit_classes = 'suit-tab suit-tab-networkslivers'
1471
1472 class NetworkSlicesInline(PlStackTabularInline):
1473     model = NetworkSlice
1474     selflink_fieldname = "slice"
1475     extra = 0
1476     verbose_name_plural = "Slices"
1477     verbose_name = "Slice"
1478     suit_classes = 'suit-tab suit-tab-networkslices'
1479     fields = ['backend_status_icon', 'network','slice']
1480     readonly_fields = ('backend_status_icon', )
1481
1482 class ControllerNetworkInline(PlStackTabularInline):
1483     model = ControllerNetwork
1484     extra = 0
1485     verbose_name_plural = "Controller Networks"
1486     verbose_name = "Controller Network"
1487     suit_classes = 'suit-tab suit-tab-admin-only'
1488     fields = ['backend_status_icon', 'controller','net_id','subnet_id']
1489     readonly_fields = ('backend_status_icon', )
1490
1491 class NetworkForm(forms.ModelForm):
1492     class Meta:
1493         model = Network
1494         widgets = {
1495             'topologyParameters': UploadTextareaWidget,
1496             'controllerParameters': UploadTextareaWidget,
1497         }
1498
1499 class NetworkAdmin(PlanetStackBaseAdmin):
1500     list_display = ("backend_status_icon", "name", "subnet", "ports", "labels")
1501     list_display_links = ('backend_status_icon', 'name', )
1502     readonly_fields = ("subnet", )
1503
1504     inlines = [NetworkParameterInline, NetworkSliversInline, NetworkSlicesInline, RouterInline]
1505     admin_inlines = [ControllerNetworkInline]
1506
1507     form=NetworkForm
1508
1509     fieldsets = [
1510         (None, {'fields': ['backend_status_text', 'name','template','ports','labels','owner','guaranteed_bandwidth', 'permit_all_slices','permitted_slices','network_id','router_id','subnet_id','subnet'],
1511                 'classes':['suit-tab suit-tab-general']}),
1512         (None, {'fields': ['topology_parameters', 'controller_url', 'controller_parameters'],
1513                 'classes':['suit-tab suit-tab-sdn']}),
1514                 ]
1515
1516     readonly_fields = ('backend_status_text', )
1517     user_readonly_fields = ['name','template','ports','labels','owner','guaranteed_bandwidth', 'permit_all_slices','permitted_slices','network_id','router_id','subnet_id','subnet']
1518
1519     @property
1520     def suit_form_tabs(self):
1521         tabs=[('general','Network Details'),
1522             ('sdn', 'SDN Configuration'),
1523             ('netparams', 'Parameters'),
1524             ('networkslivers','Slivers'),
1525             ('networkslices','Slices'),
1526             ('routers','Routers'),
1527         ]
1528
1529         request=getattr(_thread_locals, "request", None)
1530         if request and request.user.is_admin:
1531             tabs.append( ('admin-only', 'Admin-Only') )
1532
1533         return tabs
1534
1535
1536 class NetworkTemplateAdmin(PlanetStackBaseAdmin):
1537     list_display = ("backend_status_icon", "name", "guaranteed_bandwidth", "visibility")
1538     list_display_links = ('backend_status_icon', 'name', )
1539     user_readonly_fields = ["name", "guaranteed_bandwidth", "visibility"]
1540     user_readonly_inlines = []
1541     fieldsets = [
1542         (None, {'fields': ['name', 'description', 'guaranteed_bandwidth', 'visibility', 'translation', 'shared_network_name', 'shared_network_id', 'topology_kind', 'controller_kind'],
1543                 'classes':['suit-tab suit-tab-general']}),]
1544     suit_form_tabs = (('general','Network Template Details'), )
1545
1546 class FlavorAdmin(PlanetStackBaseAdmin):
1547     list_display = ("backend_status_icon", "name", "flavor", "order", "default")
1548     list_display_links = ("backend_status_icon", "name")
1549     user_readonly_fields = ("name", "flavor")
1550     fields = ("name", "description", "flavor", "order", "default")
1551
1552 # register a signal that caches the user's credentials when they log in
1553 def cache_credentials(sender, user, request, **kwds):
1554     auth = {'username': request.POST['username'],
1555             'password': request.POST['password']}
1556     request.session['auth'] = auth
1557 user_logged_in.connect(cache_credentials)
1558
1559 def dollar_field(fieldName, short_description):
1560     def newFunc(self, obj):
1561         try:
1562             x= "$ %0.2f" % float(getattr(obj, fieldName, 0.0))
1563         except:
1564             x=getattr(obj, fieldName, 0.0)
1565         return x
1566     newFunc.short_description = short_description
1567     return newFunc
1568
1569 def right_dollar_field(fieldName, short_description):
1570     def newFunc(self, obj):
1571         try:
1572             #x= '<div align=right style="width:6em">$ %0.2f</div>' % float(getattr(obj, fieldName, 0.0))
1573             x= '<div align=right>$ %0.2f</div>' % float(getattr(obj, fieldName, 0.0))
1574         except:
1575             x=getattr(obj, fieldName, 0.0)
1576         return x
1577     newFunc.short_description = short_description
1578     newFunc.allow_tags = True
1579     return newFunc
1580
1581 class InvoiceChargeInline(PlStackTabularInline):
1582     model = Charge
1583     extra = 0
1584     verbose_name_plural = "Charges"
1585     verbose_name = "Charge"
1586     exclude = ['account']
1587     fields = ["date", "kind", "state", "object", "coreHours", "dollar_amount", "slice"]
1588     readonly_fields = ["date", "kind", "state", "object", "coreHours", "dollar_amount", "slice"]
1589     can_delete = False
1590     max_num = 0
1591
1592     dollar_amount = right_dollar_field("amount", "Amount")
1593
1594 class InvoiceAdmin(admin.ModelAdmin):
1595     list_display = ("date", "account")
1596
1597     inlines = [InvoiceChargeInline]
1598
1599     fields = ["date", "account", "dollar_amount"]
1600     readonly_fields = ["date", "account", "dollar_amount"]
1601
1602     dollar_amount = dollar_field("amount", "Amount")
1603
1604 class InvoiceInline(PlStackTabularInline):
1605     model = Invoice
1606     extra = 0
1607     verbose_name_plural = "Invoices"
1608     verbose_name = "Invoice"
1609     fields = ["date", "dollar_amount"]
1610     readonly_fields = ["date", "dollar_amount"]
1611     suit_classes = 'suit-tab suit-tab-accountinvoice'
1612     can_delete=False
1613     max_num=0
1614
1615     dollar_amount = right_dollar_field("amount", "Amount")
1616
1617 class PendingChargeInline(PlStackTabularInline):
1618     model = Charge
1619     extra = 0
1620     verbose_name_plural = "Charges"
1621     verbose_name = "Charge"
1622     exclude = ["invoice"]
1623     fields = ["date", "kind", "state", "object", "coreHours", "dollar_amount", "slice"]
1624     readonly_fields = ["date", "kind", "state", "object", "coreHours", "dollar_amount", "slice"]
1625     suit_classes = 'suit-tab suit-tab-accountpendingcharges'
1626     can_delete=False
1627     max_num=0
1628
1629     def queryset(self, request):
1630         qs = super(PendingChargeInline, self).queryset(request)
1631         qs = qs.filter(state="pending")
1632         return qs
1633
1634     dollar_amount = right_dollar_field("amount", "Amount")
1635
1636 class PaymentInline(PlStackTabularInline):
1637     model=Payment
1638     extra = 1
1639     verbose_name_plural = "Payments"
1640     verbose_name = "Payment"
1641     fields = ["date", "dollar_amount"]
1642     readonly_fields = ["date", "dollar_amount"]
1643     suit_classes = 'suit-tab suit-tab-accountpayments'
1644     can_delete=False
1645     max_num=0
1646
1647     dollar_amount = right_dollar_field("amount", "Amount")
1648
1649 class AccountAdmin(admin.ModelAdmin):
1650     list_display = ("site", "balance_due")
1651
1652     inlines = [InvoiceInline, PaymentInline, PendingChargeInline]
1653
1654     fieldsets = [
1655         (None, {'fields': ['site', 'dollar_balance_due', 'dollar_total_invoices', 'dollar_total_payments'],'classes':['suit-tab suit-tab-general']}),]
1656
1657     readonly_fields = ['site', 'dollar_balance_due', 'dollar_total_invoices', 'dollar_total_payments']
1658
1659     suit_form_tabs =(
1660         ('general','Account Details'),
1661         ('accountinvoice', 'Invoices'),
1662         ('accountpayments', 'Payments'),
1663         ('accountpendingcharges','Pending Charges'),
1664     )
1665
1666     dollar_balance_due = dollar_field("balance_due", "Balance Due")
1667     dollar_total_invoices = dollar_field("total_invoices", "Total Invoices")
1668     dollar_total_payments = dollar_field("total_payments", "Total Payments")
1669
1670 # Now register the new UserAdmin...
1671 admin.site.register(User, UserAdmin)
1672 # ... and, since we're not using Django's builtin permissions,
1673 # unregister the Group model from admin.
1674 #admin.site.unregister(Group)
1675
1676 #Do not show django evolution in the admin interface
1677 from django_evolution.models import Version, Evolution
1678 #admin.site.unregister(Version)
1679 #admin.site.unregister(Evolution)
1680
1681
1682 # When debugging it is often easier to see all the classes, but for regular use 
1683 # only the top-levels should be displayed
1684 showAll = False
1685
1686 admin.site.register(Deployment, DeploymentAdmin)
1687 admin.site.register(Controller, ControllerAdmin)
1688 admin.site.register(Site, SiteAdmin)
1689 admin.site.register(Slice, SliceAdmin)
1690 admin.site.register(Service, ServiceAdmin)
1691 admin.site.register(Reservation, ReservationAdmin)
1692 admin.site.register(Network, NetworkAdmin)
1693 admin.site.register(Router, RouterAdmin)
1694 admin.site.register(NetworkTemplate, NetworkTemplateAdmin)
1695 admin.site.register(Account, AccountAdmin)
1696 admin.site.register(Invoice, InvoiceAdmin)
1697
1698 if True:
1699     admin.site.register(NetworkParameterType, NetworkParameterTypeAdmin)
1700     admin.site.register(ServiceClass, ServiceClassAdmin)
1701     #admin.site.register(PlanetStack)
1702     admin.site.register(Tag, TagAdmin)
1703     admin.site.register(ControllerRole)
1704     admin.site.register(SiteRole)
1705     admin.site.register(SliceRole)
1706     admin.site.register(PlanetStackRole)
1707     admin.site.register(Node, NodeAdmin)
1708     #admin.site.register(SlicePrivilege, SlicePrivilegeAdmin)
1709     #admin.site.register(SitePrivilege, SitePrivilegeAdmin)
1710     admin.site.register(Sliver, SliverAdmin)
1711     admin.site.register(Image, ImageAdmin)
1712     admin.site.register(DashboardView, DashboardViewAdmin)
1713     admin.site.register(Flavor, FlavorAdmin)
1714