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