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