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