checking in missing site/slice privilege steps
[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 ControllerSiteInline(PlStackTabularInline):
408     model = ControllerSite
409     extra = 0
410     suit_classes = 'suit-tab suit-tab-admin-only'
411     fields = ['controller', 'site', 'tenant_id']
412
413
414 class SitePrivilegeInline(PlStackTabularInline):
415     model = SitePrivilege
416     extra = 0
417     suit_classes = 'suit-tab suit-tab-siteprivileges'
418     fields = ['backend_status_icon', 'user','site', 'role']
419     readonly_fields = ('backend_status_icon', )
420
421     def formfield_for_foreignkey(self, db_field, request, **kwargs):
422         if db_field.name == 'site':
423             kwargs['queryset'] = Site.select_by_user(request.user)
424
425         if db_field.name == 'user':
426             kwargs['queryset'] = User.select_by_user(request.user)
427         return super(SitePrivilegeInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
428
429     def queryset(self, request):
430         return SitePrivilege.select_by_user(request.user)
431
432 class SiteDeploymentInline(PlStackTabularInline):
433     model = SiteDeployment
434     extra = 0
435     suit_classes = 'suit-tab suit-tab-deployments'
436     fields = ['backend_status_icon', 'deployment','site', 'controller']
437     readonly_fields = ('backend_status_icon', )
438
439     def formfield_for_foreignkey(self, db_field, request, **kwargs):
440         if db_field.name == 'site':
441             kwargs['queryset'] = Site.select_by_user(request.user)
442
443         if db_field.name == 'deployment':
444             kwargs['queryset'] = Deployment.select_by_user(request.user)
445
446         if db_field.name == 'controller':
447             kwargs['queryset'] = Controller.select_by_user(request.user)
448
449         return super(SiteDeploymentInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
450
451     def queryset(self, request):
452         return SiteDeployment.select_by_user(request.user)
453
454
455 class SlicePrivilegeInline(PlStackTabularInline):
456     model = SlicePrivilege
457     suit_classes = 'suit-tab suit-tab-sliceprivileges'
458     extra = 0
459     fields = ('backend_status_icon', 'user', 'slice', 'role')
460     readonly_fields = ('backend_status_icon', )
461
462     def formfield_for_foreignkey(self, db_field, request, **kwargs):
463         if db_field.name == 'slice':
464            kwargs['queryset'] = Slice.select_by_user(request.user)
465         if db_field.name == 'user':
466            kwargs['queryset'] = User.select_by_user(request.user)
467
468         return super(SlicePrivilegeInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
469
470     def queryset(self, request):
471         return SlicePrivilege.select_by_user(request.user)
472
473 class SliceNetworkInline(PlStackTabularInline):
474     model = Network.slices.through
475     selflink_fieldname = "network"
476     extra = 0
477     verbose_name = "Network Connection"
478     verbose_name_plural = "Network Connections"
479     suit_classes = 'suit-tab suit-tab-slicenetworks'
480     fields = ['backend_status_icon', 'network']
481     readonly_fields = ('backend_status_icon', )
482
483 class ImageDeploymentsInline(PlStackTabularInline):
484     model = ImageDeployments
485     extra = 0
486     verbose_name = "Image Deployments"
487     verbose_name_plural = "Image Deployments"
488     suit_classes = 'suit-tab suit-tab-imagedeployments'
489     fields = ['backend_status_icon', 'image', 'deployment']
490     readonly_fields = ['backend_status_icon']
491
492 class ControllerImagesInline(PlStackTabularInline):
493     model = ControllerImages
494     extra = 0
495     verbose_name = "Controller Images"
496     verbose_name_plural = "Controller Images"
497     suit_classes = 'suit-tab suit-tab-admin-only'
498     fields = ['backend_status_icon', 'image', 'controller', 'glance_image_id']
499     readonly_fields = ['backend_status_icon', 'glance_image_id']
500
501 class SliceRoleAdmin(PlanetStackBaseAdmin):
502     model = SliceRole
503     pass
504
505 class SiteRoleAdmin(PlanetStackBaseAdmin):
506     model = SiteRole
507     pass
508
509 class DeploymentAdminForm(forms.ModelForm):
510     sites = forms.ModelMultipleChoiceField(
511         queryset=Site.objects.all(),
512         required=False,
513         help_text="Select which sites are allowed to host nodes in this deployment",
514         widget=FilteredSelectMultiple(
515             verbose_name=('Sites'), is_stacked=False
516         )
517     )
518     images = forms.ModelMultipleChoiceField(
519         queryset=Image.objects.all(),
520         required=False,
521         help_text="Select which images should be deployed on this deployment",
522         widget=FilteredSelectMultiple(
523             verbose_name=('Images'), is_stacked=False
524         )
525     )
526     flavors = forms.ModelMultipleChoiceField(
527         queryset=Flavor.objects.all(),
528         required=False,
529         help_text="Select which flavors should be usable on this deployment",
530         widget=FilteredSelectMultiple(
531             verbose_name=('Flavors'), is_stacked=False
532         )
533     )
534     class Meta:
535         model = Deployment
536         many_to_many = ["flavors",]
537
538     def __init__(self, *args, **kwargs):
539       request = kwargs.pop('request', None)
540       super(DeploymentAdminForm, self).__init__(*args, **kwargs)
541
542       self.fields['accessControl'].initial = "allow site " + request.user.site.name
543
544       if self.instance and self.instance.pk:
545         self.fields['sites'].initial = [x.site for x in self.instance.sitedeployments.all()]
546         self.fields['images'].initial = [x.image for x in self.instance.imagedeployments.all()]
547         self.fields['flavors'].initial = self.instance.flavors.all()
548
549     def manipulate_m2m_objs(self, this_obj, selected_objs, all_relations, relation_class, local_attrname, foreign_attrname):
550         """ helper function for handling m2m relations from the MultipleChoiceField
551
552             this_obj: the source object we want to link from
553
554             selected_objs: a list of destination objects we want to link to
555
556             all_relations: the full set of relations involving this_obj, including ones we don't want
557
558             relation_class: the class that implements the relation from source to dest
559
560             local_attrname: field name representing this_obj in relation_class
561
562             foreign_attrname: field name representing selected_objs in relation_class
563
564             This function will remove all newobjclass relations from this_obj
565             that are not contained in selected_objs, and add any relations that
566             are in selected_objs but don't exist in the data model yet.
567         """
568
569         existing_dest_objs = []
570         for relation in list(all_relations):
571             if getattr(relation, foreign_attrname) not in selected_objs:
572                 #print "deleting site", sdp.site
573                 relation.delete()
574             else:
575                 existing_dest_objs.append(getattr(relation, foreign_attrname))
576
577         for dest_obj in selected_objs:
578             if dest_obj not in existing_dest_objs:
579                 #print "adding site", site
580                 kwargs = {foreign_attrname: dest_obj, local_attrname: this_obj}
581                 relation = relation_class(**kwargs)
582                 relation.save()
583
584     def save(self, commit=True):
585       deployment = super(DeploymentAdminForm, self).save(commit=False)
586
587       if commit:
588         deployment.save()
589         # this has to be done after save() if/when a deployment is first created
590         deployment.flavors = self.cleaned_data['flavors']
591
592       if deployment.pk:
593         # save_m2m() doesn't seem to work with 'through' relations. So we
594         #    create/destroy the through models ourselves. There has to be
595         #    a better way...
596
597         self.manipulate_m2m_objs(deployment, self.cleaned_data['sites'], deployment.sitedeployments.all(), SiteDeployment, "deployment", "site")
598         self.manipulate_m2m_objs(deployment, self.cleaned_data['images'], deployment.imagedeployments.all(), ImageDeployments, "deployment", "image")
599         # manipulate_m2m_objs doesn't work for Flavor/Deployment relationship
600         # so well handle that manually here
601         for flavor in deployment.flavors.all():
602             if getattr(flavor, 'name') not in self.cleaned_data['flavors']:
603                 deployment.flavors.remove(flavor)
604         for flavor in self.cleaned_data['flavors']:
605             if flavor not in deployment.flavors.all():
606                 flavor.deployments.add(deployment)
607
608       self.save_m2m()
609
610       return deployment
611
612 class DeploymentAdminROForm(DeploymentAdminForm):
613     def save(self, commit=True):
614         raise PermissionDenied
615
616 class SiteAssocInline(PlStackTabularInline):
617     model = Site.deployments.through
618     extra = 0
619     suit_classes = 'suit-tab suit-tab-sites'
620
621 class DeploymentAdmin(PlanetStackBaseAdmin):
622     model = Deployment
623     fieldList = ['backend_status_text', 'name', 'sites', 'images', 'flavors', 'accessControl']
624     fieldsets = [(None, {'fields': fieldList, 'classes':['suit-tab suit-tab-sites']})]
625     # node no longer directly connected to deployment
626     #inlines = [DeploymentPrivilegeInline,NodeInline,TagInline,ImageDeploymentsInline]
627     inlines = [DeploymentPrivilegeInline,TagInline,ImageDeploymentsInline]
628     list_display = ['backend_status_icon', 'name']
629     list_display_links = ('backend_status_icon', 'name', )
630     readonly_fields = ('backend_status_text', )
631
632     user_readonly_fields = ['name']
633
634     # nodes no longer direclty connected to deployments
635     #suit_form_tabs =(('sites','Deployment Details'),('nodes','Nodes'),('deploymentprivileges','Privileges'),('tags','Tags'),('imagedeployments','Images'))
636     suit_form_tabs =(('sites','Deployment Details'),('deploymentprivileges','Privileges'),('tags','Tags'),('imagedeployments','Images'))
637
638     def get_form(self, request, obj=None, **kwargs):
639         if request.user.isReadOnlyUser():
640             kwargs["form"] = DeploymentAdminROForm
641         else:
642             kwargs["form"] = DeploymentAdminForm
643         adminForm = super(DeploymentAdmin,self).get_form(request, obj, **kwargs)
644
645         # from stackexchange: pass the request object into the form
646
647         class AdminFormMetaClass(adminForm):
648            def __new__(cls, *args, **kwargs):
649                kwargs['request'] = request
650                return adminForm(*args, **kwargs)
651
652         return AdminFormMetaClass
653
654 class ControllerAdminForm(forms.ModelForm):
655     sites = forms.ModelMultipleChoiceField(
656         queryset=Site.objects.all(),
657         required=False,
658         help_text="Select which sites are managed by this controller",
659         widget=FilteredSelectMultiple(
660             verbose_name=('Sites'), is_stacked=False
661         )
662     )
663
664     class Meta: 
665         model = Controller
666         
667     def __init__(self, *args, **kwargs):
668         request = kwargs.pop('request', None)
669         super(ControllerAdminForm, self).__init__(*args, **kwargs)  
670
671         if self.instance and self.instance.pk:
672             self.fields['sites'].initial = [x.site_deployment for x in self.instance.controllersite.all()]
673
674     def manipulate_m2m_objs(self, this_obj, selected_objs, all_relations, relation_class, local_attrname, foreign_attrname):
675         """ helper function for handling m2m relations from the MultipleChoiceField
676             this_obj: the source object we want to link from
677             selected_objs: a list of destination objects we want to link to
678             all_relations: the full set of relations involving this_obj, including ones we don't want
679             relation_class: the class that implements the relation from source to dest
680             local_attrname: field name representing this_obj in relation_class
681             foreign_attrname: field name representing selected_objs in relation_class
682             This function will remove all newobjclass relations from this_obj
683             that are not contained in selected_objs, and add any relations that
684             are in selected_objs but don't exist in the data model yet.
685         """
686         existing_dest_objs = []
687         for relation in list(all_relations):
688             if getattr(relation, foreign_attrname) not in selected_objs:
689                 #print "deleting site", sdp.site
690                 relation.delete()
691             else:
692                 existing_dest_objs.append(getattr(relation, foreign_attrname))
693
694         for dest_obj in selected_objs:
695             if dest_obj not in existing_dest_objs:
696                 #print "adding site", site
697                 kwargs = {foreign_attrname: dest_obj, local_attrname: this_obj}
698                 relation = relation_class(**kwargs)
699                 relation.save()
700
701     def save(self, commit=True):
702         controller = super(ControllerAdminForm, self).save(commit=False)
703         if commit:
704             controller.save()
705
706         if controller.pk:
707             # save_m2m() doesn't seem to work with 'through' relations. So we
708             #    create/destroy the through models ourselves. There has to be
709             #    a better way...
710             self.manipulate_m2m_objs(controller, self.cleaned_data['sites'], controller.controllersite.all(), ControllerSite, "controller", "site")
711             pass
712         
713         self.save_m2m()
714
715         return controller 
716       
717 class ControllerAdmin(PlanetStackBaseAdmin):
718     model = Controller 
719     fieldList = ['name', 'version', 'backend_type', 'auth_url', 'admin_user', 'admin_tenant','admin_password']
720     #fieldsets = [(None, {'fields': fieldList, 'classes':['suit-tab suit-tab-general']})]
721     inlines = [ControllerSiteInline] # ,ControllerImagesInline]
722     list_display = ['backend_status_icon', 'name', 'version', 'backend_type']
723     list_display_links = ('backend_status_icon', 'name', )
724     readonly_fields = ('backend_status_text',)
725
726     user_readonly_fields = []
727
728     def get_form(self, request, obj=None, **kwargs):
729         print self.fieldsets
730         if request.user.isReadOnlyUser():
731             kwargs["form"] = ControllerAdminROForm
732         else:
733             kwargs["form"] = ControllerAdminForm
734         adminForm = super(ControllerAdmin,self).get_form(request, obj, **kwargs)
735
736         # from stackexchange: pass the request object into the form
737
738         class AdminFormMetaClass(adminForm):
739            def __new__(cls, *args, **kwargs):
740                kwargs['request'] = request
741                return adminForm(*args, **kwargs)
742
743         return AdminFormMetaClass
744
745 class ServiceAttrAsTabInline(PlStackTabularInline):
746     model = ServiceAttribute
747     fields = ['name','value']
748     extra = 0
749     suit_classes = 'suit-tab suit-tab-serviceattrs'
750
751 class ServiceAdmin(PlanetStackBaseAdmin):
752     list_display = ("backend_status_icon","name","description","versionNumber","enabled","published")
753     list_display_links = ('backend_status_icon', 'name', )
754     fieldList = ["backend_status_text","name","description","versionNumber","enabled","published"]
755     fieldsets = [(None, {'fields': fieldList, 'classes':['suit-tab suit-tab-general']})]
756     inlines = [ServiceAttrAsTabInline,SliceInline]
757     readonly_fields = ('backend_status_text', )
758
759     user_readonly_fields = fieldList
760
761     suit_form_tabs =(('general', 'Service Details'),
762         ('slices','Slices'),
763         ('serviceattrs','Additional Attributes'),
764     )
765
766 class SiteAdmin(PlanetStackBaseAdmin):
767     fieldList = ['backend_status_text', 'name', 'site_url', 'enabled', 'is_public', 'login_base', 'accountLink','location']
768     fieldsets = [
769         (None, {'fields': fieldList, 'classes':['suit-tab suit-tab-general']}),
770         #('Deployment Networks', {'fields': ['deployments'], 'classes':['suit-tab suit-tab-deployments']}),
771     ]
772     suit_form_tabs =(('general', 'Site Details'),
773         ('users','Users'),
774         ('siteprivileges','Privileges'),
775         ('deployments','Deployments'),
776         ('slices','Slices'),
777         #('nodes','Nodes'),
778         ('tags','Tags'),
779     )
780     readonly_fields = ['backend_status_text', 'accountLink']
781
782     user_readonly_fields = ['name', 'deployments','site_url', 'enabled', 'is_public', 'login_base', 'accountLink']
783
784     list_display = ('backend_status_icon', 'name', 'login_base','site_url', 'enabled')
785     list_display_links = ('backend_status_icon', 'name', )
786     filter_horizontal = ('deployments',)
787     inlines = [SliceInline,UserInline,TagInline, SitePrivilegeInline, SiteDeploymentInline]
788     search_fields = ['name']
789
790     def queryset(self, request):
791         return Site.select_by_user(request.user)
792
793     def get_formsets(self, request, obj=None):
794         for inline in self.get_inline_instances(request, obj):
795             # hide MyInline in the add view
796             if obj is None:
797                 continue
798             if isinstance(inline, SliverInline):
799                 inline.model.caller = request.user
800             yield inline.get_formset(request, obj)
801
802     def accountLink(self, obj):
803         link_obj = obj.accounts.all()
804         if link_obj:
805             reverse_path = "admin:core_account_change"
806             url = reverse(reverse_path, args =(link_obj[0].id,))
807             return "<a href='%s'>%s</a>" % (url, "view billing details")
808         else:
809             return "no billing data for this site"
810     accountLink.allow_tags = True
811     accountLink.short_description = "Billing"
812
813     def save_model(self, request, obj, form, change):
814         # update openstack connection to use this site/tenant
815         obj.save_by_user(request.user) 
816
817     def delete_model(self, request, obj):
818         obj.delete_by_user(request.user)
819         
820
821 class SitePrivilegeAdmin(PlanetStackBaseAdmin):
822     fieldList = ['backend_status_text', 'user', 'site', 'role']
823     fieldsets = [
824         (None, {'fields': fieldList, 'classes':['collapse']})
825     ]
826     readonly_fields = ('backend_status_text', )
827     list_display = ('backend_status_icon', 'user', 'site', 'role')
828     list_display_links = list_display
829     user_readonly_fields = fieldList
830     user_readonly_inlines = []
831
832     def formfield_for_foreignkey(self, db_field, request, **kwargs):
833         if db_field.name == 'site':
834             if not request.user.is_admin:
835                 # only show sites where user is an admin or pi
836                 sites = set()
837                 for site_privilege in SitePrivilege.objects.filer(user=request.user):
838                     if site_privilege.role.role_type in ['admin', 'pi']:
839                         sites.add(site_privilege.site)
840                 kwargs['queryset'] = Site.objects.filter(site__in=list(sites))
841
842         if db_field.name == 'user':
843             if not request.user.is_admin:
844                 # only show users from sites where caller has admin or pi role
845                 roles = Role.objects.filter(role_type__in=['admin', 'pi'])
846                 site_privileges = SitePrivilege.objects.filter(user=request.user).filter(role__in=roles)
847                 sites = [site_privilege.site for site_privilege in site_privileges]
848                 site_privileges = SitePrivilege.objects.filter(site__in=sites)
849                 emails = [site_privilege.user.email for site_privilege in site_privileges]
850                 users = User.objects.filter(email__in=emails)
851                 kwargs['queryset'] = users
852
853         return super(SitePrivilegeAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
854
855     def queryset(self, request):
856         # admins can see all privileges. Users can only see privileges at sites
857         # where they have the admin role or pi role.
858         qs = super(SitePrivilegeAdmin, self).queryset(request)
859         #if not request.user.is_admin:
860         #    roles = Role.objects.filter(role_type__in=['admin', 'pi'])
861         #    site_privileges = SitePrivilege.objects.filter(user=request.user).filter(role__in=roles)
862         #    login_bases = [site_privilege.site.login_base for site_privilege in site_privileges]
863         #    sites = Site.objects.filter(login_base__in=login_bases)
864         #    qs = qs.filter(site__in=sites)
865         return qs
866
867 class SliceForm(forms.ModelForm):
868     class Meta:
869         model = Slice
870         widgets = {
871             'service': LinkedSelect
872         }
873
874     def clean(self):
875         cleaned_data = super(SliceForm, self).clean()
876         name = cleaned_data.get('name')
877         site = cleaned_data.get('site')
878         slice_id = self.instance.id
879         if not site and slice_id:
880             site = Slice.objects.get(id=slice_id).site
881         if (not isinstance(site,Site)):
882             # previous code indicates 'site' could be a site_id and not a site?
883             site = Slice.objects.get(id=site.id)
884         if not name.startswith(site.login_base):
885             raise forms.ValidationError('slice name must begin with %s' % site.login_base)
886         return cleaned_data
887
888 class ControllerSliceInline(PlStackTabularInline):
889     model = ControllerSlice
890     extra = 0
891     verbose_name = "Controller Slices"
892     verbose_name_plural = "Controller Slices"
893     suit_classes = 'suit-tab suit-tab-admin-only'
894     fields = ['backend_status_icon', 'controller', 'tenant_id']
895     readonly_fields = ('backend_status_icon', )
896
897 class SliceAdmin(PlanetStackBaseAdmin):
898     form = SliceForm
899     fieldList = ['backend_status_text', 'site', 'name', 'serviceClass', 'enabled','description', 'service', 'slice_url', 'max_slivers']
900     fieldsets = [('Slice Details', {'fields': fieldList, 'classes':['suit-tab suit-tab-general']}),]
901     readonly_fields = ('backend_status_text', )
902     list_display = ('backend_status_icon', 'name', 'site','serviceClass', 'slice_url', 'max_slivers')
903     list_display_links = ('backend_status_icon', 'name', )
904     inlines = [SlicePrivilegeInline,SliverInline, TagInline, ReservationInline,SliceNetworkInline]
905     admin_inlines = [ControllerSliceInline]
906
907     user_readonly_fields = fieldList
908
909     @property
910     def suit_form_tabs(self):
911         tabs =[('general', 'Slice Details'),
912           ('slicenetworks','Networks'),
913           ('sliceprivileges','Privileges'),
914           ('slivers','Slivers'),
915           ('tags','Tags'),
916           ('reservations','Reservations'),
917           ]
918
919         request=getattr(_thread_locals, "request", None)
920         if request and request.user.is_admin:
921             tabs.append( ('admin-only', 'Admin-Only') )
922
923         return tabs
924     
925     def add_view(self, request, form_url='', extra_context=None):
926         # revert to default read-only fields
927         self.readonly_fields = ('backend_status_text',)
928         return super(SliceAdmin, self).add_view(request, form_url, extra_context=extra_context)
929
930     def change_view(self, request, object_id, form_url='', extra_context=None):
931         # cannot change the site of an existing slice so make the site field read only
932         if object_id:
933             self.readonly_fields = ('backend_status_text','site')
934         return super(SliceAdmin, self).change_view(request, object_id, form_url)
935
936     def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
937         deployment_nodes = []
938         for node in Node.objects.all():
939             deployment_nodes.append( (node.site_deployment.id, node.id, node.name) )
940
941         deployment_flavors = []
942         for flavor in Flavor.objects.all():
943             for deployment in flavor.deployments.all():
944                 deployment_flavors.append( (deployment.id, flavor.id, flavor.name) )
945
946         deployment_images = []
947         for image in Image.objects.all():
948             for deployment_image in image.imagedeployments.all():
949                 deployment_images.append( (deployment_image.deployment.id, image.id, image.name) )
950
951         site_login_bases = []
952         for site in Site.objects.all():
953             site_login_bases.append((site.id, site.login_base))
954
955         context["deployment_nodes"] = deployment_nodes
956         context["deployment_flavors"] = deployment_flavors
957         context["deployment_images"] = deployment_images
958         context["site_login_bases"] = site_login_bases
959         return super(SliceAdmin, self).render_change_form(request, context, add, change, form_url, obj)
960
961     def formfield_for_foreignkey(self, db_field, request, **kwargs):
962         if db_field.name == 'site':
963             kwargs['queryset'] = Site.select_by_user(request.user)
964             kwargs['widget'] = forms.Select(attrs={'onChange': "update_slice_prefix(this, $($(this).closest('fieldset')[0]).find('.field-name input')[0].id)"})
965
966         return super(SliceAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
967
968     def queryset(self, request):
969         # admins can see all keys. Users can only see slices they belong to.
970         return Slice.select_by_user(request.user)
971
972     def get_formsets(self, request, obj=None):
973         for inline in self.get_inline_instances(request, obj):
974             # hide MyInline in the add view
975             if obj is None:
976                 continue
977             if isinstance(inline, SliverInline):
978                 inline.model.caller = request.user
979             yield inline.get_formset(request, obj)
980
981 class SlicePrivilegeAdmin(PlanetStackBaseAdmin):
982     fieldsets = [
983         (None, {'fields': ['backend_status_text', 'user', 'slice', 'role']})
984     ]
985     readonly_fields = ('backend_status_text', )
986     list_display = ('backend_status_icon', 'user', 'slice', 'role')
987     list_display_links = list_display
988
989     user_readonly_fields = ['user', 'slice', 'role']
990     user_readonly_inlines = []
991
992     def formfield_for_foreignkey(self, db_field, request, **kwargs):
993         if db_field.name == 'slice':
994             kwargs['queryset'] = Slice.select_by_user(request.user)
995         
996         if db_field.name == 'user':
997             kwargs['queryset'] = User.select_by_user(request.user)
998
999         return super(SlicePrivilegeAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
1000
1001     def queryset(self, request):
1002         # admins can see all memberships. Users can only see memberships of
1003         # slices where they have the admin role.
1004         return SlicePrivilege.select_by_user(request.user)
1005
1006     def save_model(self, request, obj, form, change):
1007         # update openstack connection to use this site/tenant
1008         auth = request.session.get('auth', {})
1009         auth['tenant'] = obj.slice.slicename
1010         obj.os_manager = OpenStackManager(auth=auth, caller=request.user)
1011         obj.save()
1012
1013     def delete_model(self, request, obj):
1014         # update openstack connection to use this site/tenant
1015         auth = request.session.get('auth', {})
1016         auth['tenant'] = obj.slice.slicename
1017         obj.os_manager = OpenStackManager(auth=auth, caller=request.user)
1018         obj.delete()
1019
1020
1021 class ImageAdmin(PlanetStackBaseAdmin):
1022
1023     fieldsets = [('Image Details',
1024                    {'fields': ['backend_status_text', 'name', 'disk_format', 'container_format'],
1025                     'classes': ['suit-tab suit-tab-general']})
1026                ]
1027     readonly_fields = ('backend_status_text', )
1028
1029     suit_form_tabs =(('general','Image Details'),('slivers','Slivers'),('imagedeployments','Deployments'), ('controllerimages', 'Controllers'))
1030
1031     inlines = [SliverInline, ControllerImagesInline]
1032
1033     user_readonly_fields = ['name', 'disk_format', 'container_format']
1034
1035     list_display = ['backend_status_icon', 'name']
1036     list_display_links = ('backend_status_icon', 'name', )
1037
1038 class NodeForm(forms.ModelForm):
1039     class Meta:
1040         widgets = {
1041             'site': LinkedSelect,
1042             'deployment': LinkedSelect
1043         }
1044
1045 class NodeAdmin(PlanetStackBaseAdmin):
1046     form = NodeForm
1047     list_display = ('backend_status_icon', 'name', 'site_deployment')
1048     list_display_links = ('backend_status_icon', 'name', )
1049     list_filter = ('site_deployment',)
1050
1051     inlines = [TagInline,SliverInline]
1052     fieldsets = [('Node Details', {'fields': ['backend_status_text', 'name','site_deployment'], 'classes':['suit-tab suit-tab-details']})]
1053     readonly_fields = ('backend_status_text', )
1054
1055     user_readonly_fields = ['name','site_deployment']
1056     user_readonly_inlines = [TagInline,SliverInline]
1057
1058     suit_form_tabs =(('details','Node Details'),('slivers','Slivers'),('tags','Tags'))
1059
1060
1061 class SliverForm(forms.ModelForm):
1062     class Meta:
1063         model = Sliver
1064         ip = forms.CharField(widget=PlainTextWidget)
1065         instance_name = forms.CharField(widget=PlainTextWidget)
1066         widgets = {
1067             'ip': PlainTextWidget(),
1068             'instance_name': PlainTextWidget(),
1069             'slice': LinkedSelect,
1070             'deployment': LinkedSelect,
1071             'node': LinkedSelect,
1072             'image': LinkedSelect
1073         }
1074
1075 class TagAdmin(PlanetStackBaseAdmin):
1076     list_display = ['backend_status_icon', 'service', 'name', 'value', 'content_type', 'content_object',]
1077     list_display_links = list_display
1078     user_readonly_fields = ['service', 'name', 'value', 'content_type', 'content_object',]
1079     user_readonly_inlines = []
1080
1081 class SliverAdmin(PlanetStackBaseAdmin):
1082     form = SliverForm
1083     fieldsets = [
1084         ('Sliver Details', {'fields': ['backend_status_text', 'slice', 'deployment', 'node', 'ip', 'instance_name', 'flavor', 'image', ], 'classes': ['suit-tab suit-tab-general'], })
1085     ]
1086     readonly_fields = ('backend_status_text', )
1087     list_display = ['backend_status_icon', 'ip', 'instance_name', 'slice', 'flavor', 'image', 'node', 'deployment']
1088     list_display_links = ('backend_status_icon', 'ip',)
1089
1090     suit_form_tabs =(('general', 'Sliver Details'),
1091         ('tags','Tags'),
1092     )
1093
1094     inlines = [TagInline]
1095
1096     user_readonly_fields = ['slice', 'deployment', 'node', 'ip', 'instance_name', 'flavor', 'image']
1097
1098     def formfield_for_foreignkey(self, db_field, request, **kwargs):
1099         if db_field.name == 'slice':
1100             kwargs['queryset'] = Slice.select_by_user(request.user)
1101
1102         return super(SliverAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
1103
1104     def queryset(self, request):
1105         # admins can see all slivers. Users can only see slivers of
1106         # the slices they belong to.
1107         return Sliver.select_by_user(request.user)
1108
1109
1110     def get_formsets(self, request, obj=None):
1111         # make some fields read only if we are updating an existing record
1112         if obj == None:
1113             #self.readonly_fields = ('ip', 'instance_name')
1114             self.readonly_fields = ('backend_status_text',)
1115         else:
1116             self.readonly_fields = ('backend_status_text',)
1117             #self.readonly_fields = ('ip', 'instance_name', 'slice', 'image', 'key')
1118
1119         for inline in self.get_inline_instances(request, obj):
1120             # hide MyInline in the add view
1121             if obj is None:
1122                 continue
1123             if isinstance(inline, SliverInline):
1124                 inline.model.caller = request.user
1125             yield inline.get_formset(request, obj)
1126
1127     #def save_model(self, request, obj, form, change):
1128     #    # update openstack connection to use this site/tenant
1129     #    auth = request.session.get('auth', {})
1130     #    auth['tenant'] = obj.slice.name
1131     #    obj.os_manager = OpenStackManager(auth=auth, caller=request.user)
1132     #    obj.creator = request.user
1133     #    obj.save()
1134
1135     #def delete_model(self, request, obj):
1136     #    # update openstack connection to use this site/tenant
1137     #    auth = request.session.get('auth', {})
1138     #    auth['tenant'] = obj.slice.name
1139     #    obj.os_manager = OpenStackManager(auth=auth, caller=request.user)
1140     #    obj.delete()
1141
1142 class UserCreationForm(forms.ModelForm):
1143     """A form for creating new users. Includes all the required
1144     fields, plus a repeated password."""
1145     password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
1146     password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput)
1147
1148     class Meta:
1149         model = User
1150         fields = ('email', 'firstname', 'lastname', 'phone', 'public_key')
1151
1152     def clean_password2(self):
1153         # Check that the two password entries match
1154         password1 = self.cleaned_data.get("password1")
1155         password2 = self.cleaned_data.get("password2")
1156         if password1 and password2 and password1 != password2:
1157             raise forms.ValidationError("Passwords don't match")
1158         return password2
1159
1160     def save(self, commit=True):
1161         # Save the provided password in hashed format
1162         user = super(UserCreationForm, self).save(commit=False)
1163         user.password = self.cleaned_data["password1"]
1164         #user.set_password(self.cleaned_data["password1"])
1165         if commit:
1166             user.save()
1167         return user
1168
1169
1170 class UserChangeForm(forms.ModelForm):
1171     """A form for updating users. Includes all the fields on
1172     the user, but replaces the password field with admin's
1173     password hash display field.
1174     """
1175     password = ReadOnlyPasswordHashField(label='Password',
1176                    help_text= '<a href=\"password/\">Change Password</a>.')
1177
1178     class Meta:
1179         model = User
1180         widgets = { 'public_key': UploadTextareaWidget, }
1181
1182     def clean_password(self):
1183         # Regardless of what the user provides, return the initial value.
1184         # This is done here, rather than on the field, because the
1185         # field does not have access to the initial value
1186         return self.initial["password"]
1187
1188 class UserDashboardViewInline(PlStackTabularInline):
1189     model = UserDashboardView
1190     extra = 0
1191     suit_classes = 'suit-tab suit-tab-dashboards'
1192     fields = ['user', 'dashboardView', 'order']
1193
1194 class UserAdmin(PermissionCheckingAdminMixin, UserAdmin):
1195     # Note: Make sure PermissionCheckingAdminMixin is listed before
1196     # admin.ModelAdmin in the class declaration.
1197
1198     class Meta:
1199         app_label = "core"
1200
1201     # The forms to add and change user instances
1202     form = UserChangeForm
1203     add_form = UserCreationForm
1204
1205     # The fields to be used in displaying the User model.
1206     # These override the definitions on the base UserAdmin
1207     # that reference specific fields on auth.User.
1208     list_display = ('email', 'firstname', 'lastname', 'site', 'last_login')
1209     list_filter = ('site',)
1210     inlines = [SlicePrivilegeInline,SitePrivilegeInline,UserDashboardViewInline]
1211
1212     fieldListLoginDetails = ['backend_status_text', 'email','site','password','is_active','is_readonly','is_admin','public_key']
1213     fieldListContactInfo = ['firstname','lastname','phone','timezone']
1214
1215     fieldsets = (
1216         ('Login Details', {'fields': ['backend_status_text', 'email', 'site','password', 'is_active', 'is_readonly', 'is_admin', 'public_key'], 'classes':['suit-tab suit-tab-general']}),
1217         ('Contact Information', {'fields': ('firstname','lastname','phone', 'timezone'), 'classes':['suit-tab suit-tab-contact']}),
1218         #('Dashboard Views', {'fields': ('dashboards',), 'classes':['suit-tab suit-tab-dashboards']}),
1219         #('Important dates', {'fields': ('last_login',)}),
1220     )
1221     add_fieldsets = (
1222         (None, {
1223             'classes': ('wide',),
1224             'fields': ('email', 'firstname', 'lastname', 'is_readonly', 'phone', 'public_key','password1', 'password2')},
1225         ),
1226     )
1227     readonly_fields = ('backend_status_text', )
1228     search_fields = ('email',)
1229     ordering = ('email',)
1230     filter_horizontal = ()
1231
1232     user_readonly_fields = fieldListLoginDetails + fieldListContactInfo
1233
1234     @property
1235     def suit_form_tabs(self):
1236         if getattr(_thread_locals, "obj", None) is None:
1237             return []
1238         else:
1239             return (('general','Login Details'),
1240                          ('contact','Contact Information'),
1241                          ('sliceprivileges','Slice Privileges'),
1242                          ('siteprivileges','Site Privileges'),
1243                          ('controllerprivileges','Controller Privileges'),
1244                          ('dashboards','Dashboard Views'))
1245
1246     def formfield_for_foreignkey(self, db_field, request, **kwargs):
1247         if db_field.name == 'site':
1248             kwargs['queryset'] = Site.select_by_user(request.user)
1249
1250         return super(UserAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
1251
1252     def queryset(self, request):
1253         return User.select_by_user(request.user)
1254
1255 class ControllerDashboardViewInline(PlStackTabularInline):
1256     model = ControllerDashboardView
1257     extra = 0
1258     fields = ["controller", "url"]
1259     suit_classes = 'suit-tab suit-tab-controllers'
1260
1261 class DashboardViewAdmin(PlanetStackBaseAdmin):
1262     fieldsets = [('Dashboard View Details',
1263                    {'fields': ['backend_status_text', 'name', 'url'],
1264                     'classes': ['suit-tab suit-tab-general']})
1265                ]
1266     readonly_fields = ('backend_status_text', )
1267     inlines = [ControllerDashboardViewInline]
1268
1269     suit_form_tabs =(('general','Dashboard View Details'),
1270                      ('controllers', 'Per-controller Dashboard Details'))
1271
1272 class ServiceResourceInline(PlStackTabularInline):
1273     model = ServiceResource
1274     extra = 0
1275
1276 class ServiceClassAdmin(PlanetStackBaseAdmin):
1277     list_display = ('backend_status_icon', 'name', 'commitment', 'membershipFee')
1278     list_display_links = ('backend_status_icon', 'name', )
1279     inlines = [ServiceResourceInline]
1280
1281     user_readonly_fields = ['name', 'commitment', 'membershipFee']
1282     user_readonly_inlines = []
1283
1284 class ReservedResourceInline(PlStackTabularInline):
1285     model = ReservedResource
1286     extra = 0
1287     suit_classes = 'suit-tab suit-tab-reservedresources'
1288
1289     def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
1290         field = super(ReservedResourceInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
1291
1292         if db_field.name == 'resource':
1293             # restrict resources to those that the slice's service class allows
1294             if request._slice is not None:
1295                 field.queryset = field.queryset.filter(serviceClass = request._slice.serviceClass, calendarReservable=True)
1296                 if len(field.queryset) > 0:
1297                     field.initial = field.queryset.all()[0]
1298             else:\r
1299                 field.queryset = field.queryset.none()\r
1300         elif db_field.name == 'sliver':\r
1301             # restrict slivers to those that belong to the slice\r
1302             if request._slice is not None:\r
1303                 field.queryset = field.queryset.filter(slice = request._slice)
1304             else:
1305                 field.queryset = field.queryset.none()\r
1306 \r
1307         return field
1308
1309     def queryset(self, request):
1310         return ReservedResource.select_by_user(request.user)
1311
1312 class ReservationChangeForm(forms.ModelForm):
1313     class Meta:
1314         model = Reservation
1315         widgets = {
1316             'slice' : LinkedSelect
1317         }
1318
1319 class ReservationAddForm(forms.ModelForm):
1320     slice = forms.ModelChoiceField(queryset=Slice.objects.all(), widget=forms.Select(attrs={"onChange":"document.getElementById('id_refresh').value=1; submit()"}))
1321     refresh = forms.CharField(widget=forms.HiddenInput())
1322
1323     class Media:
1324        css = {'all': ('planetstack.css',)}   # .field-refresh { display: none; }
1325
1326     def clean_slice(self):
1327         slice = self.cleaned_data.get("slice")
1328         x = ServiceResource.objects.filter(serviceClass = slice.serviceClass, calendarReservable=True)
1329         if len(x) == 0:
1330             raise forms.ValidationError("The slice you selected does not have a service class that allows reservations")
1331         return slice
1332
1333     class Meta:
1334         model = Reservation
1335         widgets = {
1336             'slice' : LinkedSelect
1337         }
1338
1339
1340 class ReservationAddRefreshForm(ReservationAddForm):
1341     """ This form is displayed when the Reservation Form receives an update
1342         from the Slice dropdown onChange handler. It doesn't validate the
1343         data and doesn't save the data. This will cause the form to be
1344         redrawn.
1345     """
1346
1347     """ don't validate anything other than slice """
1348     dont_validate_fields = ("startTime", "duration")
1349
1350     def full_clean(self):
1351         result = super(ReservationAddForm, self).full_clean()
1352
1353         for fieldname in self.dont_validate_fields:
1354             if fieldname in self._errors:
1355                 del self._errors[fieldname]
1356
1357         return result
1358
1359     """ don't save anything """
1360     def is_valid(self):
1361         return False
1362
1363 class ReservationAdmin(PlanetStackBaseAdmin):
1364     fieldList = ['backend_status_text', 'slice', 'startTime', 'duration']
1365     fieldsets = [('Reservation Details', {'fields': fieldList, 'classes': ['suit-tab suit-tab-general']})]
1366     readonly_fields = ('backend_status_text', )
1367     list_display = ('startTime', 'duration')
1368     form = ReservationAddForm
1369
1370     suit_form_tabs = (('general','Reservation Details'), ('reservedresources','Reserved Resources'))
1371
1372     inlines = [ReservedResourceInline]
1373     user_readonly_fields = fieldList
1374
1375     def add_view(self, request, form_url='', extra_context=None):
1376         timezone.activate(request.user.timezone)
1377         request._refresh = False
1378         request._slice = None
1379         if request.method == 'POST':
1380             # "refresh" will be set to "1" if the form was submitted due to
1381             # a change in the Slice dropdown.
1382             if request.POST.get("refresh","1") == "1":
1383                 request._refresh = True
1384                 request.POST["refresh"] = "0"
1385
1386             # Keep track of the slice that was selected, so the
1387             # reservedResource inline can filter items for the slice.
1388             request._slice = request.POST.get("slice",None)
1389             if (request._slice is not None):
1390                 request._slice = Slice.objects.get(id=request._slice)
1391
1392         result =  super(ReservationAdmin, self).add_view(request, form_url, extra_context)
1393         return result
1394
1395     def changelist_view(self, request, extra_context = None):
1396         timezone.activate(request.user.timezone)
1397         return super(ReservationAdmin, self).changelist_view(request, extra_context)
1398
1399     def get_form(self, request, obj=None, **kwargs):
1400         request._obj_ = obj
1401         if obj is not None:
1402             # For changes, set request._slice to the slice already set in the
1403             # object.
1404             request._slice = obj.slice
1405             self.form = ReservationChangeForm
1406         else:
1407             if getattr(request, "_refresh", False):
1408                 self.form = ReservationAddRefreshForm
1409             else:
1410                 self.form = ReservationAddForm
1411         return super(ReservationAdmin, self).get_form(request, obj, **kwargs)
1412
1413     def get_readonly_fields(self, request, obj=None):
1414         if (obj is not None):
1415             # Prevent slice from being changed after the reservation has been
1416             # created.
1417             return ['slice']
1418         else:
1419             return []
1420
1421     def queryset(self, request):
1422         return Reservation.select_by_user(request.user)
1423
1424 class NetworkParameterTypeAdmin(PlanetStackBaseAdmin):
1425     list_display = ("backend_status_icon", "name", )
1426     list_display_links = ('backend_status_icon', 'name', )
1427     user_readonly_fields = ['name']
1428     user_readonly_inlines = []
1429
1430 class RouterAdmin(PlanetStackBaseAdmin):
1431     list_display = ("backend_status_icon", "name", )
1432     list_display_links = ('backend_status_icon', 'name', )
1433     user_readonly_fields = ['name']
1434     user_readonly_inlines = []
1435
1436 class RouterInline(PlStackTabularInline):
1437     model = Router.networks.through
1438     extra = 0
1439     verbose_name_plural = "Routers"
1440     verbose_name = "Router"
1441     suit_classes = 'suit-tab suit-tab-routers'
1442
1443 class NetworkParameterInline(PlStackGenericTabularInline):
1444     model = NetworkParameter
1445     extra = 0
1446     verbose_name_plural = "Parameters"
1447     verbose_name = "Parameter"
1448     suit_classes = 'suit-tab suit-tab-netparams'
1449     fields = ['backend_status_icon', 'parameter', 'value']
1450     readonly_fields = ('backend_status_icon', )
1451
1452 class NetworkSliversInline(PlStackTabularInline):
1453     fields = ['backend_status_icon', 'network','sliver','ip']
1454     readonly_fields = ("backend_status_icon", "ip", )
1455     model = NetworkSliver
1456     selflink_fieldname = "sliver"
1457     extra = 0
1458     verbose_name_plural = "Slivers"
1459     verbose_name = "Sliver"
1460     suit_classes = 'suit-tab suit-tab-networkslivers'
1461
1462 class NetworkSlicesInline(PlStackTabularInline):
1463     model = NetworkSlice
1464     selflink_fieldname = "slice"
1465     extra = 0
1466     verbose_name_plural = "Slices"
1467     verbose_name = "Slice"
1468     suit_classes = 'suit-tab suit-tab-networkslices'
1469     fields = ['backend_status_icon', 'network','slice']
1470     readonly_fields = ('backend_status_icon', )
1471
1472 class ControllerNetworkInline(PlStackTabularInline):
1473     model = ControllerNetwork
1474     extra = 0
1475     verbose_name_plural = "Controller Networks"
1476     verbose_name = "Controller Network"
1477     suit_classes = 'suit-tab suit-tab-admin-only'
1478     fields = ['backend_status_icon', 'controller','net_id','subnet_id']
1479     readonly_fields = ('backend_status_icon', )
1480
1481 class NetworkForm(forms.ModelForm):
1482     class Meta:
1483         model = Network
1484         widgets = {
1485             'topologyParameters': UploadTextareaWidget,
1486             'controllerParameters': UploadTextareaWidget,
1487         }
1488
1489 class NetworkAdmin(PlanetStackBaseAdmin):
1490     list_display = ("backend_status_icon", "name", "subnet", "ports", "labels")
1491     list_display_links = ('backend_status_icon', 'name', )
1492     readonly_fields = ("subnet", )
1493
1494     inlines = [NetworkParameterInline, NetworkSliversInline, NetworkSlicesInline, RouterInline]
1495     admin_inlines = [ControllerNetworkInline]
1496
1497     form=NetworkForm
1498
1499     fieldsets = [
1500         (None, {'fields': ['backend_status_text', 'name','template','ports','labels','owner','guaranteed_bandwidth', 'permit_all_slices','permitted_slices','network_id','router_id','subnet_id','subnet'],
1501                 'classes':['suit-tab suit-tab-general']}),
1502         (None, {'fields': ['topology_parameters', 'controller_url', 'controller_parameters'],
1503                 'classes':['suit-tab suit-tab-sdn']}),
1504                 ]
1505
1506     readonly_fields = ('backend_status_text', )
1507     user_readonly_fields = ['name','template','ports','labels','owner','guaranteed_bandwidth', 'permit_all_slices','permitted_slices','network_id','router_id','subnet_id','subnet']
1508
1509     @property
1510     def suit_form_tabs(self):
1511         tabs=[('general','Network Details'),
1512             ('sdn', 'SDN Configuration'),
1513             ('netparams', 'Parameters'),
1514             ('networkslivers','Slivers'),
1515             ('networkslices','Slices'),
1516             ('routers','Routers'),
1517         ]
1518
1519         request=getattr(_thread_locals, "request", None)
1520         if request and request.user.is_admin:
1521             tabs.append( ('admin-only', 'Admin-Only') )
1522
1523         return tabs
1524
1525
1526 class NetworkTemplateAdmin(PlanetStackBaseAdmin):
1527     list_display = ("backend_status_icon", "name", "guaranteed_bandwidth", "visibility")
1528     list_display_links = ('backend_status_icon', 'name', )
1529     user_readonly_fields = ["name", "guaranteed_bandwidth", "visibility"]
1530     user_readonly_inlines = []
1531     fieldsets = [
1532         (None, {'fields': ['name', 'description', 'guaranteed_bandwidth', 'visibility', 'translation', 'shared_network_name', 'shared_network_id', 'topology_kind', 'controller_kind'],
1533                 'classes':['suit-tab suit-tab-general']}),]
1534     suit_form_tabs = (('general','Network Template Details'), )
1535
1536 class FlavorAdmin(PlanetStackBaseAdmin):
1537     list_display = ("backend_status_icon", "name", "flavor", "order", "default")
1538     list_display_links = ("backend_status_icon", "name")
1539     user_readonly_fields = ("name", "flavor")
1540     fields = ("name", "description", "flavor", "order", "default")
1541
1542 # register a signal that caches the user's credentials when they log in
1543 def cache_credentials(sender, user, request, **kwds):
1544     auth = {'username': request.POST['username'],
1545             'password': request.POST['password']}
1546     request.session['auth'] = auth
1547 user_logged_in.connect(cache_credentials)
1548
1549 def dollar_field(fieldName, short_description):
1550     def newFunc(self, obj):
1551         try:
1552             x= "$ %0.2f" % float(getattr(obj, fieldName, 0.0))
1553         except:
1554             x=getattr(obj, fieldName, 0.0)
1555         return x
1556     newFunc.short_description = short_description
1557     return newFunc
1558
1559 def right_dollar_field(fieldName, short_description):
1560     def newFunc(self, obj):
1561         try:
1562             #x= '<div align=right style="width:6em">$ %0.2f</div>' % float(getattr(obj, fieldName, 0.0))
1563             x= '<div align=right>$ %0.2f</div>' % float(getattr(obj, fieldName, 0.0))
1564         except:
1565             x=getattr(obj, fieldName, 0.0)
1566         return x
1567     newFunc.short_description = short_description
1568     newFunc.allow_tags = True
1569     return newFunc
1570
1571 class InvoiceChargeInline(PlStackTabularInline):
1572     model = Charge
1573     extra = 0
1574     verbose_name_plural = "Charges"
1575     verbose_name = "Charge"
1576     exclude = ['account']
1577     fields = ["date", "kind", "state", "object", "coreHours", "dollar_amount", "slice"]
1578     readonly_fields = ["date", "kind", "state", "object", "coreHours", "dollar_amount", "slice"]
1579     can_delete = False
1580     max_num = 0
1581
1582     dollar_amount = right_dollar_field("amount", "Amount")
1583
1584 class InvoiceAdmin(admin.ModelAdmin):
1585     list_display = ("date", "account")
1586
1587     inlines = [InvoiceChargeInline]
1588
1589     fields = ["date", "account", "dollar_amount"]
1590     readonly_fields = ["date", "account", "dollar_amount"]
1591
1592     dollar_amount = dollar_field("amount", "Amount")
1593
1594 class InvoiceInline(PlStackTabularInline):
1595     model = Invoice
1596     extra = 0
1597     verbose_name_plural = "Invoices"
1598     verbose_name = "Invoice"
1599     fields = ["date", "dollar_amount"]
1600     readonly_fields = ["date", "dollar_amount"]
1601     suit_classes = 'suit-tab suit-tab-accountinvoice'
1602     can_delete=False
1603     max_num=0
1604
1605     dollar_amount = right_dollar_field("amount", "Amount")
1606
1607 class PendingChargeInline(PlStackTabularInline):
1608     model = Charge
1609     extra = 0
1610     verbose_name_plural = "Charges"
1611     verbose_name = "Charge"
1612     exclude = ["invoice"]
1613     fields = ["date", "kind", "state", "object", "coreHours", "dollar_amount", "slice"]
1614     readonly_fields = ["date", "kind", "state", "object", "coreHours", "dollar_amount", "slice"]
1615     suit_classes = 'suit-tab suit-tab-accountpendingcharges'
1616     can_delete=False
1617     max_num=0
1618
1619     def queryset(self, request):
1620         qs = super(PendingChargeInline, self).queryset(request)
1621         qs = qs.filter(state="pending")
1622         return qs
1623
1624     dollar_amount = right_dollar_field("amount", "Amount")
1625
1626 class PaymentInline(PlStackTabularInline):
1627     model=Payment
1628     extra = 1
1629     verbose_name_plural = "Payments"
1630     verbose_name = "Payment"
1631     fields = ["date", "dollar_amount"]
1632     readonly_fields = ["date", "dollar_amount"]
1633     suit_classes = 'suit-tab suit-tab-accountpayments'
1634     can_delete=False
1635     max_num=0
1636
1637     dollar_amount = right_dollar_field("amount", "Amount")
1638
1639 class AccountAdmin(admin.ModelAdmin):
1640     list_display = ("site", "balance_due")
1641
1642     inlines = [InvoiceInline, PaymentInline, PendingChargeInline]
1643
1644     fieldsets = [
1645         (None, {'fields': ['site', 'dollar_balance_due', 'dollar_total_invoices', 'dollar_total_payments'],'classes':['suit-tab suit-tab-general']}),]
1646
1647     readonly_fields = ['site', 'dollar_balance_due', 'dollar_total_invoices', 'dollar_total_payments']
1648
1649     suit_form_tabs =(
1650         ('general','Account Details'),
1651         ('accountinvoice', 'Invoices'),
1652         ('accountpayments', 'Payments'),
1653         ('accountpendingcharges','Pending Charges'),
1654     )
1655
1656     dollar_balance_due = dollar_field("balance_due", "Balance Due")
1657     dollar_total_invoices = dollar_field("total_invoices", "Total Invoices")
1658     dollar_total_payments = dollar_field("total_payments", "Total Payments")
1659
1660 # Now register the new UserAdmin...
1661 admin.site.register(User, UserAdmin)
1662 # ... and, since we're not using Django's builtin permissions,
1663 # unregister the Group model from admin.
1664 #admin.site.unregister(Group)
1665
1666 #Do not show django evolution in the admin interface
1667 from django_evolution.models import Version, Evolution
1668 #admin.site.unregister(Version)
1669 #admin.site.unregister(Evolution)
1670
1671
1672 # When debugging it is often easier to see all the classes, but for regular use 
1673 # only the top-levels should be displayed
1674 showAll = False
1675
1676 admin.site.register(Deployment, DeploymentAdmin)
1677 admin.site.register(Controller, ControllerAdmin)
1678 admin.site.register(Site, SiteAdmin)
1679 admin.site.register(Slice, SliceAdmin)
1680 admin.site.register(Service, ServiceAdmin)
1681 admin.site.register(Reservation, ReservationAdmin)
1682 admin.site.register(Network, NetworkAdmin)
1683 admin.site.register(Router, RouterAdmin)
1684 admin.site.register(NetworkTemplate, NetworkTemplateAdmin)
1685 admin.site.register(Account, AccountAdmin)
1686 admin.site.register(Invoice, InvoiceAdmin)
1687
1688 if True:
1689     admin.site.register(NetworkParameterType, NetworkParameterTypeAdmin)
1690     admin.site.register(ServiceClass, ServiceClassAdmin)
1691     #admin.site.register(PlanetStack)
1692     admin.site.register(Tag, TagAdmin)
1693     admin.site.register(ControllerRole)
1694     admin.site.register(SiteRole)
1695     admin.site.register(SliceRole)
1696     admin.site.register(PlanetStackRole)
1697     admin.site.register(Node, NodeAdmin)
1698     #admin.site.register(SlicePrivilege, SlicePrivilegeAdmin)
1699     #admin.site.register(SitePrivilege, SitePrivilegeAdmin)
1700     admin.site.register(Sliver, SliverAdmin)
1701     admin.site.register(Image, ImageAdmin)
1702     admin.site.register(DashboardView, DashboardViewAdmin)
1703     admin.site.register(Flavor, FlavorAdmin)
1704