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