refactor and 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', 'controllerNetwork', '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 == 'controllerNetwork':
353            kwargs['queryset'] = Deployment.select_by_acl(request.user)
354            kwargs['widget'] = forms.Select(attrs={'onChange': "sliver_deployment_changed(this);"})
355         elif 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 ControllerSiteDeploymentsInline(PlStackTabularInline):
418     model = ControllerSiteDeployments
419     extra = 0
420     suit_classes = 'suit-tab suit-tab-admin-only'
421     fields = ['controller', 'site_deployment', 'tenant_id']
422
423 class SitePrivilegeInline(PlStackTabularInline):
424     model = SitePrivilege
425     extra = 0
426     suit_classes = 'suit-tab suit-tab-siteprivileges'
427     fields = ['backend_status_icon', 'user','site', 'role']
428     readonly_fields = ('backend_status_icon', )
429
430     def formfield_for_foreignkey(self, db_field, request, **kwargs):
431         if db_field.name == 'site':
432             kwargs['queryset'] = Site.select_by_user(request.user)
433
434         if db_field.name == 'user':
435             kwargs['queryset'] = User.select_by_user(request.user)
436         return super(SitePrivilegeInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
437
438     def queryset(self, request):
439         return SitePrivilege.select_by_user(request.user)
440
441 class SiteDeploymentsInline(PlStackTabularInline):
442     model = SiteDeployments
443     extra = 0
444     suit_classes = 'suit-tab suit-tab-deployments'
445     fields = ['backend_status_icon', 'deployment','site', 'controller']
446     readonly_fields = ('backend_status_icon', )
447
448     def formfield_for_foreignkey(self, db_field, request, **kwargs):
449         if db_field.name == 'site':
450             kwargs['queryset'] = Site.select_by_user(request.user)
451
452         if db_field.name == 'deployment':
453             kwargs['queryset'] = Deployment.select_by_user(request.user)
454
455         if db_field.name == 'controller':
456             kwargs['queryset'] = Controller.select_by_user(request.user)
457
458         return super(SiteDeploymentsInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
459
460     def queryset(self, request):
461         return SiteDeployments.select_by_user(request.user)
462
463
464 class SlicePrivilegeInline(PlStackTabularInline):
465     model = SlicePrivilege
466     suit_classes = 'suit-tab suit-tab-sliceprivileges'
467     extra = 0
468     fields = ('backend_status_icon', 'user', 'slice', 'role')
469     readonly_fields = ('backend_status_icon', )
470
471     def formfield_for_foreignkey(self, db_field, request, **kwargs):
472         if db_field.name == 'slice':
473            kwargs['queryset'] = Slice.select_by_user(request.user)
474         if db_field.name == 'user':
475            kwargs['queryset'] = User.select_by_user(request.user)
476
477         return super(SlicePrivilegeInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
478
479     def queryset(self, request):
480         return SlicePrivilege.select_by_user(request.user)
481
482 class SliceNetworkInline(PlStackTabularInline):
483     model = Network.slices.through
484     selflink_fieldname = "network"
485     extra = 0
486     verbose_name = "Network Connection"
487     verbose_name_plural = "Network Connections"
488     suit_classes = 'suit-tab suit-tab-slicenetworks'
489     fields = ['backend_status_icon', 'network']
490     readonly_fields = ('backend_status_icon', )
491
492 class ImageDeploymentsInline(PlStackTabularInline):
493     model = ImageDeployments
494     extra = 0
495     verbose_name = "Image Deployments"
496     verbose_name_plural = "Image Deployments"
497     suit_classes = 'suit-tab suit-tab-imagedeployments'
498     fields = ['backend_status_icon', 'image', 'deployment']
499     readonly_fields = ['backend_status_icon']
500
501 class ControllerImagesInline(PlStackTabularInline):
502     model = ControllerImages
503     extra = 0
504     verbose_name = "Controller Images"
505     verbose_name_plural = "Controller Images"
506     suit_classes = 'suit-tab suit-tab-admin-only'
507     fields = ['backend_status_icon', 'image', 'controller', 'glance_image_id']
508     readonly_fields = ['backend_status_icon', 'glance_image_id']
509
510 class SliceRoleAdmin(PlanetStackBaseAdmin):
511     model = SliceRole
512     pass
513
514 class SiteRoleAdmin(PlanetStackBaseAdmin):
515     model = SiteRole
516     pass
517
518 class DeploymentAdminForm(forms.ModelForm):
519     sites = forms.ModelMultipleChoiceField(
520         queryset=Site.objects.all(),
521         required=False,
522         help_text="Select which sites are allowed to host nodes in this deployment",
523         widget=FilteredSelectMultiple(
524             verbose_name=('Sites'), is_stacked=False
525         )
526     )
527     images = forms.ModelMultipleChoiceField(
528         queryset=Image.objects.all(),
529         required=False,
530         help_text="Select which images should be deployed on this deployment",
531         widget=FilteredSelectMultiple(
532             verbose_name=('Images'), is_stacked=False
533         )
534     )
535     flavors = forms.ModelMultipleChoiceField(
536         queryset=Flavor.objects.all(),
537         required=False,
538         help_text="Select which flavors should be usable on this deployment",
539         widget=FilteredSelectMultiple(
540             verbose_name=('Flavors'), is_stacked=False
541         )
542     )
543     class Meta:
544         model = Deployment
545         many_to_many = ["flavors",]
546
547     def __init__(self, *args, **kwargs):
548       request = kwargs.pop('request', None)
549       super(DeploymentAdminForm, self).__init__(*args, **kwargs)
550
551       self.fields['accessControl'].initial = "allow site " + request.user.site.name
552
553       if self.instance and self.instance.pk:
554         self.fields['sites'].initial = [x.site for x in self.instance.sitedeployments.all()]
555         self.fields['images'].initial = [x.image for x in self.instance.imagedeployments.all()]
556         self.fields['flavors'].initial = self.instance.flavors.all()
557
558     def manipulate_m2m_objs(self, this_obj, selected_objs, all_relations, relation_class, local_attrname, foreign_attrname):
559         """ helper function for handling m2m relations from the MultipleChoiceField
560
561             this_obj: the source object we want to link from
562
563             selected_objs: a list of destination objects we want to link to
564
565             all_relations: the full set of relations involving this_obj, including ones we don't want
566
567             relation_class: the class that implements the relation from source to dest
568
569             local_attrname: field name representing this_obj in relation_class
570
571             foreign_attrname: field name representing selected_objs in relation_class
572
573             This function will remove all newobjclass relations from this_obj
574             that are not contained in selected_objs, and add any relations that
575             are in selected_objs but don't exist in the data model yet.
576         """
577
578         existing_dest_objs = []
579         for relation in list(all_relations):
580             if getattr(relation, foreign_attrname) not in selected_objs:
581                 #print "deleting site", sdp.site
582                 relation.delete()
583             else:
584                 existing_dest_objs.append(getattr(relation, foreign_attrname))
585
586         for dest_obj in selected_objs:
587             if dest_obj not in existing_dest_objs:
588                 #print "adding site", site
589                 kwargs = {foreign_attrname: dest_obj, local_attrname: this_obj}
590                 relation = relation_class(**kwargs)
591                 relation.save()
592
593     def save(self, commit=True):
594       deployment = super(DeploymentAdminForm, self).save(commit=False)
595
596       if commit:
597         deployment.save()
598         # this has to be done after save() if/when a deployment is first created
599         deployment.flavors = self.cleaned_data['flavors']
600
601       if deployment.pk:
602         # save_m2m() doesn't seem to work with 'through' relations. So we
603         #    create/destroy the through models ourselves. There has to be
604         #    a better way...
605
606         self.manipulate_m2m_objs(deployment, self.cleaned_data['sites'], deployment.sitedeployments.all(), SiteDeployments, "deployment", "site")
607         self.manipulate_m2m_objs(deployment, self.cleaned_data['images'], deployment.imagedeployments.all(), DeploymentImages, "deployment", "image")
608
609       self.save_m2m()
610
611       return deployment
612
613 class DeploymentAdminROForm(DeploymentAdminForm):
614     def save(self, commit=True):
615         raise PermissionDenied
616
617 class SiteAssocInline(PlStackTabularInline):
618     model = Site.deployments.through
619     extra = 0
620     suit_classes = 'suit-tab suit-tab-sites'
621
622 class DeploymentAdmin(PlanetStackBaseAdmin):
623     model = Deployment
624     fieldList = ['backend_status_text', 'name', 'sites', 'images', 'flavors', 'accessControl']
625     fieldsets = [(None, {'fields': fieldList, 'classes':['suit-tab suit-tab-sites']})]
626     # node no longer directly connected to deployment
627     #inlines = [DeploymentPrivilegeInline,NodeInline,TagInline,ImageDeploymentsInline]
628     inlines = [DeploymentPrivilegeInline,TagInline,ImageDeploymentsInline]
629     list_display = ['backend_status_icon', 'name']
630     list_display_links = ('backend_status_icon', 'name', )
631     readonly_fields = ('backend_status_text', )
632
633     user_readonly_fields = ['name']
634
635     # nodes no longer direclty connected to deployments
636     #suit_form_tabs =(('sites','Deployment Details'),('nodes','Nodes'),('deploymentprivileges','Privileges'),('tags','Tags'),('imagedeployments','Images'))
637     suit_form_tabs =(('sites','Deployment Details'),('deploymentprivileges','Privileges'),('tags','Tags'),('imagedeployments','Images'))
638
639     def get_form(self, request, obj=None, **kwargs):
640         if request.user.isReadOnlyUser():
641             kwargs["form"] = DeploymentAdminROForm
642         else:
643             kwargs["form"] = DeploymentAdminForm
644         adminForm = super(DeploymentAdmin,self).get_form(request, obj, **kwargs)
645
646         # from stackexchange: pass the request object into the form
647
648         class AdminFormMetaClass(adminForm):
649            def __new__(cls, *args, **kwargs):
650                kwargs['request'] = request
651                return adminForm(*args, **kwargs)
652
653         return AdminFormMetaClass
654
655 class ControllerAdminForm(forms.ModelForm):
656     site_deployments = forms.ModelMultipleChoiceField(
657         queryset=SiteDeployments.objects.all(),
658         required=False,
659         help_text="Select which sites deployments are managed by this controller",
660         widget=FilteredSelectMultiple(
661             verbose_name=('Site Deployments'), is_stacked=False
662         )
663     )
664
665     class Meta: 
666         model = Controller
667         
668     def __init__(self, *args, **kwargs):
669         request = kwargs.pop('request', None)
670         super(ControllerAdminForm, self).__init__(*args, **kwargs)  
671
672         if self.instance and self.instance.pk:
673             self.fields['site_deployments'].initial = [x.site_deployment for x in self.instance.controllersitedeployments.all()]
674
675     def manipulate_m2m_objs(self, this_obj, selected_objs, all_relations, relation_class, local_attrname, foreign_attrname):
676         """ helper function for handling m2m relations from the MultipleChoiceField
677             this_obj: the source object we want to link from
678             selected_objs: a list of destination objects we want to link to
679             all_relations: the full set of relations involving this_obj, including ones we don't want
680             relation_class: the class that implements the relation from source to dest
681             local_attrname: field name representing this_obj in relation_class
682             foreign_attrname: field name representing selected_objs in relation_class
683             This function will remove all newobjclass relations from this_obj
684             that are not contained in selected_objs, and add any relations that
685             are in selected_objs but don't exist in the data model yet.
686         """
687         existing_dest_objs = []
688         for relation in list(all_relations):
689             if getattr(relation, foreign_attrname) not in selected_objs:
690                 #print "deleting site", sdp.site
691                 relation.delete()
692             else:
693                 existing_dest_objs.append(getattr(relation, foreign_attrname))
694
695         for dest_obj in selected_objs:
696             if dest_obj not in existing_dest_objs:
697                 #print "adding site", site
698                 kwargs = {foreign_attrname: dest_obj, local_attrname: this_obj}
699                 relation = relation_class(**kwargs)
700                 relation.save()
701
702     def save(self, commit=True):
703         controller = super(ControllerAdminForm, self).save(commit=False)
704         if commit:
705             controller.save()
706
707         if controller.pk:
708             # save_m2m() doesn't seem to work with 'through' relations. So we
709             #    create/destroy the through models ourselves. There has to be
710             #    a better way...
711             self.manipulate_m2m_objs(controller, self.cleaned_data['site_deployments'], controller.controllersitedeployments.all(), ControllerSiteDeployments, "controller", "site_deployment")
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 = [ControllerSiteDeploymentsInline] # ,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, SiteDeploymentsInline]
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 ControllerSlicesInline(PlStackTabularInline):
889     model = ControllerSlices
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 = [ControllerSlicesInline]
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.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.controller.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             'controllerNetwork': 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', 'controllerNetwork', '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', 'controllerNetwork']
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', 'controllerNetwork', '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,ControllerPrivilegeInline,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 DashboardViewAdmin(PlanetStackBaseAdmin):
1256     fieldsets = [('Dashboard View Details',
1257                    {'fields': ['backend_status_text', 'name', 'url'],
1258                     'classes': ['suit-tab suit-tab-general']})
1259                ]
1260     readonly_fields = ('backend_status_text', )
1261
1262     suit_form_tabs =(('general','Dashboard View Details'),)
1263
1264 class ServiceResourceInline(PlStackTabularInline):
1265     model = ServiceResource
1266     extra = 0
1267
1268 class ServiceClassAdmin(PlanetStackBaseAdmin):
1269     list_display = ('backend_status_icon', 'name', 'commitment', 'membershipFee')
1270     list_display_links = ('backend_status_icon', 'name', )
1271     inlines = [ServiceResourceInline]
1272
1273     user_readonly_fields = ['name', 'commitment', 'membershipFee']
1274     user_readonly_inlines = []
1275
1276 class ReservedResourceInline(PlStackTabularInline):
1277     model = ReservedResource
1278     extra = 0
1279     suit_classes = 'suit-tab suit-tab-reservedresources'
1280
1281     def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
1282         field = super(ReservedResourceInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
1283
1284         if db_field.name == 'resource':
1285             # restrict resources to those that the slice's service class allows
1286             if request._slice is not None:
1287                 field.queryset = field.queryset.filter(serviceClass = request._slice.serviceClass, calendarReservable=True)
1288                 if len(field.queryset) > 0:
1289                     field.initial = field.queryset.all()[0]
1290             else:\r
1291                 field.queryset = field.queryset.none()\r
1292         elif db_field.name == 'sliver':\r
1293             # restrict slivers to those that belong to the slice\r
1294             if request._slice is not None:\r
1295                 field.queryset = field.queryset.filter(slice = request._slice)
1296             else:
1297                 field.queryset = field.queryset.none()\r
1298 \r
1299         return field
1300
1301     def queryset(self, request):
1302         return ReservedResource.select_by_user(request.user)
1303
1304 class ReservationChangeForm(forms.ModelForm):
1305     class Meta:
1306         model = Reservation
1307         widgets = {
1308             'slice' : LinkedSelect
1309         }
1310
1311 class ReservationAddForm(forms.ModelForm):
1312     slice = forms.ModelChoiceField(queryset=Slice.objects.all(), widget=forms.Select(attrs={"onChange":"document.getElementById('id_refresh').value=1; submit()"}))
1313     refresh = forms.CharField(widget=forms.HiddenInput())
1314
1315     class Media:
1316        css = {'all': ('planetstack.css',)}   # .field-refresh { display: none; }
1317
1318     def clean_slice(self):
1319         slice = self.cleaned_data.get("slice")
1320         x = ServiceResource.objects.filter(serviceClass = slice.serviceClass, calendarReservable=True)
1321         if len(x) == 0:
1322             raise forms.ValidationError("The slice you selected does not have a service class that allows reservations")
1323         return slice
1324
1325     class Meta:
1326         model = Reservation
1327         widgets = {
1328             'slice' : LinkedSelect
1329         }
1330
1331
1332 class ReservationAddRefreshForm(ReservationAddForm):
1333     """ This form is displayed when the Reservation Form receives an update
1334         from the Slice dropdown onChange handler. It doesn't validate the
1335         data and doesn't save the data. This will cause the form to be
1336         redrawn.
1337     """
1338
1339     """ don't validate anything other than slice """
1340     dont_validate_fields = ("startTime", "duration")
1341
1342     def full_clean(self):
1343         result = super(ReservationAddForm, self).full_clean()
1344
1345         for fieldname in self.dont_validate_fields:
1346             if fieldname in self._errors:
1347                 del self._errors[fieldname]
1348
1349         return result
1350
1351     """ don't save anything """
1352     def is_valid(self):
1353         return False
1354
1355 class ReservationAdmin(PlanetStackBaseAdmin):
1356     fieldList = ['backend_status_text', 'slice', 'startTime', 'duration']
1357     fieldsets = [('Reservation Details', {'fields': fieldList, 'classes': ['suit-tab suit-tab-general']})]
1358     readonly_fields = ('backend_status_text', )
1359     list_display = ('startTime', 'duration')
1360     form = ReservationAddForm
1361
1362     suit_form_tabs = (('general','Reservation Details'), ('reservedresources','Reserved Resources'))
1363
1364     inlines = [ReservedResourceInline]
1365     user_readonly_fields = fieldList
1366
1367     def add_view(self, request, form_url='', extra_context=None):
1368         timezone.activate(request.user.timezone)
1369         request._refresh = False
1370         request._slice = None
1371         if request.method == 'POST':
1372             # "refresh" will be set to "1" if the form was submitted due to
1373             # a change in the Slice dropdown.
1374             if request.POST.get("refresh","1") == "1":
1375                 request._refresh = True
1376                 request.POST["refresh"] = "0"
1377
1378             # Keep track of the slice that was selected, so the
1379             # reservedResource inline can filter items for the slice.
1380             request._slice = request.POST.get("slice",None)
1381             if (request._slice is not None):
1382                 request._slice = Slice.objects.get(id=request._slice)
1383
1384         result =  super(ReservationAdmin, self).add_view(request, form_url, extra_context)
1385         return result
1386
1387     def changelist_view(self, request, extra_context = None):
1388         timezone.activate(request.user.timezone)
1389         return super(ReservationAdmin, self).changelist_view(request, extra_context)
1390
1391     def get_form(self, request, obj=None, **kwargs):
1392         request._obj_ = obj
1393         if obj is not None:
1394             # For changes, set request._slice to the slice already set in the
1395             # object.
1396             request._slice = obj.slice
1397             self.form = ReservationChangeForm
1398         else:
1399             if getattr(request, "_refresh", False):
1400                 self.form = ReservationAddRefreshForm
1401             else:
1402                 self.form = ReservationAddForm
1403         return super(ReservationAdmin, self).get_form(request, obj, **kwargs)
1404
1405     def get_readonly_fields(self, request, obj=None):
1406         if (obj is not None):
1407             # Prevent slice from being changed after the reservation has been
1408             # created.
1409             return ['slice']
1410         else:
1411             return []
1412
1413     def queryset(self, request):
1414         return Reservation.select_by_user(request.user)
1415
1416 class NetworkParameterTypeAdmin(PlanetStackBaseAdmin):
1417     list_display = ("backend_status_icon", "name", )
1418     list_display_links = ('backend_status_icon', 'name', )
1419     user_readonly_fields = ['name']
1420     user_readonly_inlines = []
1421
1422 class RouterAdmin(PlanetStackBaseAdmin):
1423     list_display = ("backend_status_icon", "name", )
1424     list_display_links = ('backend_status_icon', 'name', )
1425     user_readonly_fields = ['name']
1426     user_readonly_inlines = []
1427
1428 class RouterInline(PlStackTabularInline):
1429     model = Router.networks.through
1430     extra = 0
1431     verbose_name_plural = "Routers"
1432     verbose_name = "Router"
1433     suit_classes = 'suit-tab suit-tab-routers'
1434
1435 class NetworkParameterInline(PlStackGenericTabularInline):
1436     model = NetworkParameter
1437     extra = 0
1438     verbose_name_plural = "Parameters"
1439     verbose_name = "Parameter"
1440     suit_classes = 'suit-tab suit-tab-netparams'
1441     fields = ['backend_status_icon', 'parameter', 'value']
1442     readonly_fields = ('backend_status_icon', )
1443
1444 class NetworkSliversInline(PlStackTabularInline):
1445     fields = ['backend_status_icon', 'network','sliver','ip']
1446     readonly_fields = ("backend_status_icon", "ip", )
1447     model = NetworkSliver
1448     selflink_fieldname = "sliver"
1449     extra = 0
1450     verbose_name_plural = "Slivers"
1451     verbose_name = "Sliver"
1452     suit_classes = 'suit-tab suit-tab-networkslivers'
1453
1454 class NetworkSlicesInline(PlStackTabularInline):
1455     model = NetworkSlice
1456     selflink_fieldname = "slice"
1457     extra = 0
1458     verbose_name_plural = "Slices"
1459     verbose_name = "Slice"
1460     suit_classes = 'suit-tab suit-tab-networkslices'
1461     fields = ['backend_status_icon', 'network','slice']
1462     readonly_fields = ('backend_status_icon', )
1463
1464 class ControllerNetworksInline(PlStackTabularInline):
1465     model = ControllerNetworks
1466     extra = 0
1467     verbose_name_plural = "Controller Networks"
1468     verbose_name = "Controller Network"
1469     suit_classes = 'suit-tab suit-tab-admin-only'
1470     fields = ['backend_status_icon', 'controller','net_id','subnet_id']
1471     readonly_fields = ('backend_status_icon', )
1472
1473 class NetworkForm(forms.ModelForm):
1474     class Meta:
1475         model = Network
1476         widgets = {
1477             'topologyParameters': UploadTextareaWidget,
1478             'controllerParameters': UploadTextareaWidget,
1479         }
1480
1481 class NetworkAdmin(PlanetStackBaseAdmin):
1482     list_display = ("backend_status_icon", "name", "subnet", "ports", "labels")
1483     list_display_links = ('backend_status_icon', 'name', )
1484     readonly_fields = ("subnet", )
1485
1486     inlines = [NetworkParameterInline, NetworkSliversInline, NetworkSlicesInline, RouterInline]
1487     admin_inlines = [ControllerNetworksInline]
1488
1489     form=NetworkForm
1490
1491     fieldsets = [
1492         (None, {'fields': ['backend_status_text', 'name','template','ports','labels','owner','guaranteedBandwidth', 'permitAllSlices','permittedSlices','network_id','router_id','subnet_id','subnet'],
1493                 'classes':['suit-tab suit-tab-general']}),
1494         (None, {'fields': ['topologyParameters', 'controllerUrl', 'controllerParameters'],
1495                 'classes':['suit-tab suit-tab-sdn']}),
1496                 ]
1497
1498     readonly_fields = ('backend_status_text', )
1499     user_readonly_fields = ['name','template','ports','labels','owner','guaranteedBandwidth', 'permitAllSlices','permittedSlices','network_id','router_id','subnet_id','subnet']
1500
1501     @property
1502     def suit_form_tabs(self):
1503         tabs=[('general','Network Details'),
1504             ('sdn', 'SDN Configuration'),
1505             ('netparams', 'Parameters'),
1506             ('networkslivers','Slivers'),
1507             ('networkslices','Slices'),
1508             ('routers','Routers'),
1509         ]
1510
1511         request=getattr(_thread_locals, "request", None)
1512         if request and request.user.is_admin:
1513             tabs.append( ('admin-only', 'Admin-Only') )
1514
1515         return tabs
1516
1517
1518 class NetworkTemplateAdmin(PlanetStackBaseAdmin):
1519     list_display = ("backend_status_icon", "name", "guaranteedBandwidth", "visibility")
1520     list_display_links = ('backend_status_icon', 'name', )
1521     user_readonly_fields = ["name", "guaranteedBandwidth", "visibility"]
1522     user_readonly_inlines = []
1523     fieldsets = [
1524         (None, {'fields': ['name', 'description', 'guaranteedBandwidth', 'visibility', 'translation', 'sharedNetworkName', 'sharedNetworkId', 'topologyKind', 'controllerKind'],
1525                 'classes':['suit-tab suit-tab-general']}),]
1526     suit_form_tabs = (('general','Network Template Details'), )
1527
1528 class FlavorAdmin(PlanetStackBaseAdmin):
1529     list_display = ("backend_status_icon", "name", "flavor", "order", "default")
1530     list_display_links = ("backend_status_icon", "name")
1531     user_readonly_fields = ("name", "flavor")
1532     fields = ("name", "description", "flavor", "order", "default")
1533
1534 # register a signal that caches the user's credentials when they log in
1535 def cache_credentials(sender, user, request, **kwds):
1536     auth = {'username': request.POST['username'],
1537             'password': request.POST['password']}
1538     request.session['auth'] = auth
1539 user_logged_in.connect(cache_credentials)
1540
1541 def dollar_field(fieldName, short_description):
1542     def newFunc(self, obj):
1543         try:
1544             x= "$ %0.2f" % float(getattr(obj, fieldName, 0.0))
1545         except:
1546             x=getattr(obj, fieldName, 0.0)
1547         return x
1548     newFunc.short_description = short_description
1549     return newFunc
1550
1551 def right_dollar_field(fieldName, short_description):
1552     def newFunc(self, obj):
1553         try:
1554             #x= '<div align=right style="width:6em">$ %0.2f</div>' % float(getattr(obj, fieldName, 0.0))
1555             x= '<div align=right>$ %0.2f</div>' % float(getattr(obj, fieldName, 0.0))
1556         except:
1557             x=getattr(obj, fieldName, 0.0)
1558         return x
1559     newFunc.short_description = short_description
1560     newFunc.allow_tags = True
1561     return newFunc
1562
1563 class InvoiceChargeInline(PlStackTabularInline):
1564     model = Charge
1565     extra = 0
1566     verbose_name_plural = "Charges"
1567     verbose_name = "Charge"
1568     exclude = ['account']
1569     fields = ["date", "kind", "state", "object", "coreHours", "dollar_amount", "slice"]
1570     readonly_fields = ["date", "kind", "state", "object", "coreHours", "dollar_amount", "slice"]
1571     can_delete = False
1572     max_num = 0
1573
1574     dollar_amount = right_dollar_field("amount", "Amount")
1575
1576 class InvoiceAdmin(admin.ModelAdmin):
1577     list_display = ("date", "account")
1578
1579     inlines = [InvoiceChargeInline]
1580
1581     fields = ["date", "account", "dollar_amount"]
1582     readonly_fields = ["date", "account", "dollar_amount"]
1583
1584     dollar_amount = dollar_field("amount", "Amount")
1585
1586 class InvoiceInline(PlStackTabularInline):
1587     model = Invoice
1588     extra = 0
1589     verbose_name_plural = "Invoices"
1590     verbose_name = "Invoice"
1591     fields = ["date", "dollar_amount"]
1592     readonly_fields = ["date", "dollar_amount"]
1593     suit_classes = 'suit-tab suit-tab-accountinvoice'
1594     can_delete=False
1595     max_num=0
1596
1597     dollar_amount = right_dollar_field("amount", "Amount")
1598
1599 class PendingChargeInline(PlStackTabularInline):
1600     model = Charge
1601     extra = 0
1602     verbose_name_plural = "Charges"
1603     verbose_name = "Charge"
1604     exclude = ["invoice"]
1605     fields = ["date", "kind", "state", "object", "coreHours", "dollar_amount", "slice"]
1606     readonly_fields = ["date", "kind", "state", "object", "coreHours", "dollar_amount", "slice"]
1607     suit_classes = 'suit-tab suit-tab-accountpendingcharges'
1608     can_delete=False
1609     max_num=0
1610
1611     def queryset(self, request):
1612         qs = super(PendingChargeInline, self).queryset(request)
1613         qs = qs.filter(state="pending")
1614         return qs
1615
1616     dollar_amount = right_dollar_field("amount", "Amount")
1617
1618 class PaymentInline(PlStackTabularInline):
1619     model=Payment
1620     extra = 1
1621     verbose_name_plural = "Payments"
1622     verbose_name = "Payment"
1623     fields = ["date", "dollar_amount"]
1624     readonly_fields = ["date", "dollar_amount"]
1625     suit_classes = 'suit-tab suit-tab-accountpayments'
1626     can_delete=False
1627     max_num=0
1628
1629     dollar_amount = right_dollar_field("amount", "Amount")
1630
1631 class AccountAdmin(admin.ModelAdmin):
1632     list_display = ("site", "balance_due")
1633
1634     inlines = [InvoiceInline, PaymentInline, PendingChargeInline]
1635
1636     fieldsets = [
1637         (None, {'fields': ['site', 'dollar_balance_due', 'dollar_total_invoices', 'dollar_total_payments'],'classes':['suit-tab suit-tab-general']}),]
1638
1639     readonly_fields = ['site', 'dollar_balance_due', 'dollar_total_invoices', 'dollar_total_payments']
1640
1641     suit_form_tabs =(
1642         ('general','Account Details'),
1643         ('accountinvoice', 'Invoices'),
1644         ('accountpayments', 'Payments'),
1645         ('accountpendingcharges','Pending Charges'),
1646     )
1647
1648     dollar_balance_due = dollar_field("balance_due", "Balance Due")
1649     dollar_total_invoices = dollar_field("total_invoices", "Total Invoices")
1650     dollar_total_payments = dollar_field("total_payments", "Total Payments")
1651
1652 # Now register the new UserAdmin...
1653 admin.site.register(User, UserAdmin)
1654 # ... and, since we're not using Django's builtin permissions,
1655 # unregister the Group model from admin.
1656 #admin.site.unregister(Group)
1657
1658 #Do not show django evolution in the admin interface
1659 from django_evolution.models import Version, Evolution
1660 #admin.site.unregister(Version)
1661 #admin.site.unregister(Evolution)
1662
1663
1664 # When debugging it is often easier to see all the classes, but for regular use 
1665 # only the top-levels should be displayed
1666 showAll = False
1667
1668 admin.site.register(Deployment, DeploymentAdmin)
1669 admin.site.register(Controller, ControllerAdmin)
1670 admin.site.register(Site, SiteAdmin)
1671 admin.site.register(Slice, SliceAdmin)
1672 admin.site.register(Service, ServiceAdmin)
1673 admin.site.register(Reservation, ReservationAdmin)
1674 admin.site.register(Network, NetworkAdmin)
1675 admin.site.register(Router, RouterAdmin)
1676 admin.site.register(NetworkTemplate, NetworkTemplateAdmin)
1677 admin.site.register(Account, AccountAdmin)
1678 admin.site.register(Invoice, InvoiceAdmin)
1679
1680 if True:
1681     admin.site.register(NetworkParameterType, NetworkParameterTypeAdmin)
1682     admin.site.register(ServiceClass, ServiceClassAdmin)
1683     #admin.site.register(PlanetStack)
1684     admin.site.register(Tag, TagAdmin)
1685     admin.site.register(ControllerRole)
1686     admin.site.register(SiteRole)
1687     admin.site.register(SliceRole)
1688     admin.site.register(PlanetStackRole)
1689     admin.site.register(Node, NodeAdmin)
1690     #admin.site.register(SlicePrivilege, SlicePrivilegeAdmin)
1691     #admin.site.register(SitePrivilege, SitePrivilegeAdmin)
1692     admin.site.register(Sliver, SliverAdmin)
1693     admin.site.register(Image, ImageAdmin)
1694     admin.site.register(DashboardView, DashboardViewAdmin)
1695     admin.site.register(Flavor, FlavorAdmin)
1696