1 from core.models import Site
2 from core.models import *
3 from openstack.manager import OpenStackManager
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
11 from django.contrib.auth.forms import ReadOnlyPasswordHashField
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
19 import django_evolution
21 class ReadOnlyAwareAdmin(admin.ModelAdmin):
23 def has_add_permission(self, request, obj=None):
24 return (not self.__user_is_readonly(request))
26 def has_delete_permission(self, request, obj=None):
27 return (not self.__user_is_readonly(request))
29 def save_model(self, request, obj, form, change):
30 if self.__user_is_readonly(request):
31 raise PermissionDenied
34 return super(ReadOnlyAwareAdmin, self).save_model(request, obj, form, change)
36 def get_actions(self,request):
37 actions = super(ReadOnlyAwareAdmin,self).get_actions(request)
39 if self.__user_is_readonly(request):
40 if 'delete_selected' in actions:
41 del actions['delete_selected']
45 def change_view(self,request,object_id, extra_context=None):
46 if self.__user_is_readonly(request):
47 if not hasattr(self, "readonly_save"):
\r
48 # save the original readonly fields
\r
49 self.readonly_save = self.readonly_fields
\r
50 self.inlines_save = self.inlines
\r
51 if hasattr(self, "user_readonly_fields"):
\r
52 self.readonly_fields=self.user_readonly_fields
\r
53 if hasattr(self, "user_readonly_inlines"):
\r
54 self.inlines = self.user_readonly_inlines
\r
56 if hasattr(self, "readonly_save"):
\r
57 # restore the original readonly fields
\r
58 self.readonly_fields = self.readonly_save
\r
59 if hasattr(self, "inlines_save"):
\r
60 self.inlines = self.inlines_save
63 return super(ReadOnlyAwareAdmin, self).change_view(request, object_id, extra_context=extra_context)
64 except PermissionDenied:
66 if request.method == 'POST':
67 raise PermissionDenied
68 request.readonly = True
69 return super(ReadOnlyAwareAdmin, self).change_view(request, object_id, extra_context=extra_context)
71 def __user_is_readonly(self, request):
72 return request.user.isReadOnlyUser()
74 class SingletonAdmin (ReadOnlyAwareAdmin):
75 def has_add_permission(self, request):
76 if not super(SingletonAdmin, self).has_add_permission(request):
79 num_objects = self.model.objects.count()
86 class PlStackTabularInline(admin.TabularInline):
87 def __init__(self, *args, **kwargs):
88 super(PlStackTabularInline, self).__init__(*args, **kwargs)
90 # InlineModelAdmin as no get_fields() method, so in order to add
91 # the selflink field, we override __init__ to modify self.fields and
92 # self.readonly_fields.
96 def get_change_url(self, model, id):
97 """ Get the URL to a change form in the admin for this model """
98 reverse_path = "admin:%s_change" % (model._meta.db_table)
100 url = reverse(reverse_path, args=(id,))
101 except NoReverseMatch:
106 def setup_selflink(self):
107 if hasattr(self, "selflink_fieldname"):
108 """ self.selflink_model can be defined to punch through a relation
109 to its target object. For example, in SliceNetworkInline, set
110 selflink_model = "network", and the URL will lead to the Network
111 object instead of trying to bring up a change view of the
114 self.selflink_model = getattr(self.model,self.selflink_fieldname).field.rel.to
116 self.selflink_model = self.model
118 url = self.get_change_url(self.selflink_model, 0)
120 # We don't have an admin for this object, so don't create the
125 # Since we need to add "selflink" to the field list, we need to create
126 # self.fields if it is None.
127 if (self.fields is None):
129 for f in self.model._meta.fields:
130 if f.editable and f.name != "id":
131 self.fields.append(f.name)
133 self.fields = tuple(self.fields) + ("selflink", )
135 if self.readonly_fields is None:
136 self.readonly_fields = ()
138 self.readonly_fields = tuple(self.readonly_fields) + ("selflink", )
140 def selflink(self, obj):
141 if hasattr(self, "selflink_fieldname"):
142 obj = getattr(obj, self.selflink_fieldname)
145 url = self.get_change_url(self.selflink_model, obj.id)
146 return "<a href='%s'>Details</a>" % str(url)
148 return "Not present"
\r
150 selflink.allow_tags = True
151 selflink.short_description = "Details"
153 class ReadOnlyTabularInline(PlStackTabularInline):
156 def get_readonly_fields(self, request, obj=None):
159 def has_add_permission(self, request):
162 class ReservationROInline(ReadOnlyTabularInline):
165 suit_classes = 'suit-tab suit-tab-reservations'
166 fields = ['startTime','slice','duration']
168 class ReservationInline(PlStackTabularInline):
171 suit_classes = 'suit-tab suit-tab-reservations'
173 def queryset(self, request):
174 return Reservation.select_by_user(request.user)
176 class TagROInline(generic.GenericTabularInline):
179 suit_classes = 'suit-tab suit-tab-tags'
181 fields = ['service', 'name', 'value']
183 def get_readonly_fields(self, request, obj=None):
186 def has_add_permission(self, request):
190 class TagInline(generic.GenericTabularInline):
193 suit_classes = 'suit-tab suit-tab-tags'
194 fields = ['service', 'name', 'value']
196 def queryset(self, request):
197 return Tag.select_by_user(request.user)
199 class NetworkLookerUpper:
200 """ This is a callable that looks up a network name in a sliver and returns
201 the ip address for that network.
204 def __init__(self, name):
205 self.short_description = name
207 self.network_name = name
209 def __call__(self, obj):
211 for nbs in obj.networksliver_set.all():
212 if (nbs.network.name == self.network_name):
217 return self.network_name
219 class SliverROInline(ReadOnlyTabularInline):
221 fields = ['ip', 'instance_name', 'slice', 'numberCores', 'image', 'node', 'deploymentNetwork']
222 suit_classes = 'suit-tab suit-tab-slivers'
224 class SliverInline(PlStackTabularInline):
226 fields = ['ip', 'instance_name', 'slice', 'numberCores', 'image', 'node', 'deploymentNetwork']
228 readonly_fields = ['ip', 'instance_name']
229 suit_classes = 'suit-tab suit-tab-slivers'
231 def queryset(self, request):
232 return Sliver.select_by_user(request.user)
234 # Note this is breaking in the admin.py when trying to use an inline to add a node/image
235 # def _declared_fieldsets(self):
236 # # Return None so django will call get_fieldsets and we can insert our
240 # def get_readonly_fields(self, request, obj=None):
241 # readonly_fields = super(SliverInline, self).get_readonly_fields(request, obj)
243 # # Lookup the networks that are bound to the slivers, and add those
244 # # network names to the list of readonly fields.
246 # for sliver in obj.slivers.all():
247 # for nbs in sliver.networksliver_set.all():
249 # network_name = nbs.network.name
250 # if network_name not in [str(x) for x in readonly_fields]:
251 # readonly_fields.append(NetworkLookerUpper(network_name))
253 # return readonly_fields
255 # def get_fieldsets(self, request, obj=None):
256 # form = self.get_formset(request, obj).form
257 # # fields = the read/write files + the read-only fields
258 # fields = self.fields
259 # for fieldName in self.get_readonly_fields(request,obj):
260 # if not fieldName in fields:
261 # fields.append(fieldName)
263 # return [(None, {'fields': fields})]
267 class SiteROInline(ReadOnlyTabularInline):
270 fields = ['name', 'login_base', 'site_url', 'enabled']
271 suit_classes = 'suit-tab suit-tab-sites'
273 class SiteInline(PlStackTabularInline):
276 suit_classes = 'suit-tab suit-tab-sites'
278 def queryset(self, request):
279 return Site.select_by_user(request.user)
281 class UserROInline(ReadOnlyTabularInline):
283 fields = ['email', 'firstname', 'lastname']
285 suit_classes = 'suit-tab suit-tab-users'
287 class UserInline(PlStackTabularInline):
289 fields = ['email', 'firstname', 'lastname']
291 suit_classes = 'suit-tab suit-tab-users'
293 def queryset(self, request):
294 return User.select_by_user(request.user)
296 class SliceROInline(ReadOnlyTabularInline):
298 suit_classes = 'suit-tab suit-tab-slices'
299 fields = ['name','site', 'serviceClass', 'service']
301 class SliceInline(PlStackTabularInline):
303 fields = ['name','site', 'serviceClass', 'service']
305 suit_classes = 'suit-tab suit-tab-slices'
307 def queryset(self, request):
308 return Slice.select_by_user(request.user)
310 class NodeROInline(ReadOnlyTabularInline):
313 suit_classes = 'suit-tab suit-tab-nodes'
314 fields = ['name','deployment']
316 class NodeInline(PlStackTabularInline):
319 suit_classes = 'suit-tab suit-tab-nodes'
320 fields = ['name','deployment']
322 class DeploymentPrivilegeROInline(ReadOnlyTabularInline):
323 model = DeploymentPrivilege
325 suit_classes = 'suit-tab suit-tab-deploymentprivileges'
326 fields = ['user','role','deployment']
328 class DeploymentPrivilegeInline(PlStackTabularInline):
329 model = DeploymentPrivilege
331 suit_classes = 'suit-tab suit-tab-deploymentprivileges'
332 fields = ['user','role','deployment']
334 def queryset(self, request):
335 return DeploymentPrivilege.select_by_user(request.user)
337 #CLEANUP DOUBLE SitePrivilegeInline
338 class SitePrivilegeROInline(ReadOnlyTabularInline):
339 model = SitePrivilege
341 suit_classes = 'suit-tab suit-tab-siteprivileges'
342 fields = ['user','site', 'role']
344 class SitePrivilegeInline(PlStackTabularInline):
345 model = SitePrivilege
347 suit_classes = 'suit-tab suit-tab-siteprivileges'
348 fields = ['user','site', 'role']
350 def formfield_for_foreignkey(self, db_field, request, **kwargs):
351 if db_field.name == 'site':
352 kwargs['queryset'] = Site.select_by_user(request.user)
354 if db_field.name == 'user':
355 kwargs['queryset'] = User.select_by_user(request.user)
356 return super(SitePrivilegeInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
358 def queryset(self, request):
359 return SitePrivilege.select_by_user(request.user)
361 class SiteDeploymentROInline(ReadOnlyTabularInline):
362 model = SiteDeployments
363 #model = Site.deployments.through
365 suit_classes = 'suit-tab suit-tab-deployments'
366 fields = ['deployment','site']
368 class SiteDeploymentInline(PlStackTabularInline):
369 model = SiteDeployments
370 #model = Site.deployments.through
372 suit_classes = 'suit-tab suit-tab-deployments'
373 fields = ['deployment','site']
375 def formfield_for_foreignkey(self, db_field, request, **kwargs):
376 if db_field.name == 'site':
377 kwargs['queryset'] = Site.select_by_user(request.user)
379 if db_field.name == 'deployment':
380 kwargs['queryset'] = Deployment.select_by_user(request.user)
381 return super(SiteDeploymentInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
383 def queryset(self, request):
384 return SiteDeployments.select_by_user(request.user)
387 class SlicePrivilegeROInline(ReadOnlyTabularInline):
388 model = SlicePrivilege
390 suit_classes = 'suit-tab suit-tab-sliceprivileges'
391 fields = ['user', 'slice', 'role']
393 class SlicePrivilegeInline(PlStackTabularInline):
394 model = SlicePrivilege
395 suit_classes = 'suit-tab suit-tab-sliceprivileges'
397 fields = ('user', 'slice','role')
399 def formfield_for_foreignkey(self, db_field, request, **kwargs):
400 if db_field.name == 'slice':
401 kwargs['queryset'] = Slice.select_by_user(request.user)
402 if db_field.name == 'user':
403 kwargs['queryset'] = User.select_by_user(request.user)
405 return super(SlicePrivilegeInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
407 def queryset(self, request):
408 return SlicePrivilege.select_by_user(request.user)
410 class SliceNetworkROInline(ReadOnlyTabularInline):
411 model = Network.slices.through
413 verbose_name = "Network Connection"
414 verbose_name_plural = "Network Connections"
415 suit_classes = 'suit-tab suit-tab-slicenetworks'
418 class SliceNetworkInline(PlStackTabularInline):
419 model = Network.slices.through
420 selflink_fieldname = "network"
422 verbose_name = "Network Connection"
423 verbose_name_plural = "Network Connections"
424 suit_classes = 'suit-tab suit-tab-slicenetworks'
426 class PlainTextWidget(forms.HiddenInput):
427 input_type = 'hidden'
429 def render(self, name, value, attrs=None):
432 return mark_safe(str(value) + super(PlainTextWidget, self).render(name, value, attrs))
434 class PlanetStackBaseAdmin(ReadOnlyAwareAdmin):
437 def save_model(self, request, obj, form, change):
438 obj.caller = request.user
439 # update openstack connection to use this site/tenant
440 obj.save_by_user(request.user)
442 def delete_model(self, request, obj):
443 obj.delete_by_user(request.user)
445 def save_formset(self, request, form, formset, change):
446 instances = formset.save(commit=False)
447 for instance in instances:
448 instance.save_by_user(request.user)
451 class SliceRoleAdmin(PlanetStackBaseAdmin):
455 class SiteRoleAdmin(PlanetStackBaseAdmin):
459 class DeploymentAdminForm(forms.ModelForm):
460 sites = forms.ModelMultipleChoiceField(
461 queryset=Site.objects.all(),
463 widget=FilteredSelectMultiple(
464 verbose_name=('Sites'), is_stacked=False
470 def __init__(self, *args, **kwargs):
471 super(DeploymentAdminForm, self).__init__(*args, **kwargs)
473 if self.instance and self.instance.pk:
474 self.fields['sites'].initial = [x.site for x in self.instance.sitedeployments_set.all()]
476 def save(self, commit=True):
477 deployment = super(DeploymentAdminForm, self).save(commit=False)
483 # save_m2m() doesn't seem to work with 'through' relations. So we
484 # create/destroy the through models ourselves. There has to be
487 sites = self.cleaned_data['sites']
490 for sdp in list(deployment.sitedeployments_set.all()):
491 if sdp.site not in sites:
492 #print "deleting site", sdp.site
495 existing_sites.append(sdp.site)
498 if site not in existing_sites:
499 #print "adding site", site
500 sdp = SiteDeployments(site=site, deployment=deployment)
507 class DeploymentAdminROForm(DeploymentAdminForm):
508 def save(self, commit=True):
509 raise PermissionDenied
511 class SiteAssocInline(PlStackTabularInline):
512 model = Site.deployments.through
514 suit_classes = 'suit-tab suit-tab-sites'
516 class DeploymentAdmin(PlanetStackBaseAdmin):
517 #form = DeploymentAdminForm
519 fieldList = ['name','sites']
520 fieldsets = [(None, {'fields': fieldList, 'classes':['suit-tab suit-tab-sites']})]
521 inlines = [DeploymentPrivilegeInline,NodeInline,TagInline]
523 user_readonly_inlines = [DeploymentPrivilegeROInline,NodeROInline,TagROInline]
524 user_readonly_fields = ['name']
526 suit_form_tabs =(('sites','Deployment Details'),('nodes','Nodes'),('deploymentprivileges','Privileges'),('tags','Tags'))
528 def get_form(self, request, obj=None, **kwargs):
529 if request.user.isReadOnlyUser():
530 kwargs["form"] = DeploymentAdminROForm
532 kwargs["form"] = DeploymentAdminForm
533 return super(DeploymentAdmin,self).get_form(request, obj, **kwargs)
535 class ServiceAttrAsTabROInline(ReadOnlyTabularInline):
536 model = ServiceAttribute
537 fields = ['name','value']
539 suit_classes = 'suit-tab suit-tab-serviceattrs'
541 class ServiceAttrAsTabInline(PlStackTabularInline):
542 model = ServiceAttribute
543 fields = ['name','value']
545 suit_classes = 'suit-tab suit-tab-serviceattrs'
547 class ServiceAdmin(PlanetStackBaseAdmin):
548 list_display = ("name","description","versionNumber","enabled","published")
549 fieldList = ["name","description","versionNumber","enabled","published"]
550 fieldsets = [(None, {'fields': fieldList, 'classes':['suit-tab suit-tab-general']})]
551 inlines = [ServiceAttrAsTabInline,SliceInline]
553 user_readonly_fields = fieldList
554 user_readonly_inlines = [ServiceAttrAsTabROInline,SliceROInline]
556 suit_form_tabs =(('general', 'Service Details'),
558 ('serviceattrs','Additional Attributes'),
561 class SiteAdmin(PlanetStackBaseAdmin):
562 fieldList = ['name', 'site_url', 'enabled', 'is_public', 'login_base', 'accountLink','location']
564 (None, {'fields': fieldList, 'classes':['suit-tab suit-tab-general']}),
565 #('Deployment Networks', {'fields': ['deployments'], 'classes':['suit-tab suit-tab-deployments']}),
567 suit_form_tabs =(('general', 'Site Details'),
569 ('siteprivileges','Privileges'),
570 ('deployments','Deployments'),
575 readonly_fields = ['accountLink']
577 user_readonly_fields = ['name', 'deployments','site_url', 'enabled', 'is_public', 'login_base', 'accountLink']
578 user_readonly_inlines = [SliceROInline,UserROInline,TagROInline, NodeROInline, SitePrivilegeROInline,SiteDeploymentROInline]
580 list_display = ('name', 'login_base','site_url', 'enabled')
581 filter_horizontal = ('deployments',)
582 inlines = [SliceInline,UserInline,TagInline, NodeInline, SitePrivilegeInline, SiteDeploymentInline]
583 search_fields = ['name']
585 def queryset(self, request):
586 return Site.select_by_user(request.user)
588 def get_formsets(self, request, obj=None):
589 for inline in self.get_inline_instances(request, obj):
590 # hide MyInline in the add view
593 if isinstance(inline, SliceInline):
594 inline.model.caller = request.user
595 yield inline.get_formset(request, obj)
597 def get_formsets(self, request, obj=None):
598 for inline in self.get_inline_instances(request, obj):
599 # hide MyInline in the add view
602 if isinstance(inline, SliverInline):
603 inline.model.caller = request.user
604 yield inline.get_formset(request, obj)
606 def accountLink(self, obj):
607 link_obj = obj.accounts.all()
609 reverse_path = "admin:core_account_change"
610 url = reverse(reverse_path, args =(link_obj[0].id,))
611 return "<a href='%s'>%s</a>" % (url, "view billing details")
613 return "no billing data for this site"
614 accountLink.allow_tags = True
615 accountLink.short_description = "Billing"
617 def save_model(self, request, obj, form, change):
618 # update openstack connection to use this site/tenant
619 obj.save_by_user(request.user)
621 def delete_model(self, request, obj):
622 obj.delete_by_user(request.user)
625 class SitePrivilegeAdmin(PlanetStackBaseAdmin):
626 fieldList = ['user', 'site', 'role']
628 (None, {'fields': fieldList, 'classes':['collapse']})
630 list_display = ('user', 'site', 'role')
631 user_readonly_fields = fieldList
632 user_readonly_inlines = []
634 def formfield_for_foreignkey(self, db_field, request, **kwargs):
635 if db_field.name == 'site':
636 if not request.user.is_admin:
637 # only show sites where user is an admin or pi
639 for site_privilege in SitePrivilege.objects.filer(user=request.user):
640 if site_privilege.role.role_type in ['admin', 'pi']:
641 sites.add(site_privilege.site)
642 kwargs['queryset'] = Site.objects.filter(site__in=list(sites))
644 if db_field.name == 'user':
645 if not request.user.is_admin:
646 # only show users from sites where caller has admin or pi role
647 roles = Role.objects.filter(role_type__in=['admin', 'pi'])
648 site_privileges = SitePrivilege.objects.filter(user=request.user).filter(role__in=roles)
649 sites = [site_privilege.site for site_privilege in site_privileges]
650 site_privileges = SitePrivilege.objects.filter(site__in=sites)
651 emails = [site_privilege.user.email for site_privilege in site_privileges]
652 users = User.objects.filter(email__in=emails)
653 kwargs['queryset'] = users
655 return super(SitePrivilegeAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
657 def queryset(self, request):
658 # admins can see all privileges. Users can only see privileges at sites
659 # where they have the admin role or pi role.
660 qs = super(SitePrivilegeAdmin, self).queryset(request)
661 #if not request.user.is_admin:
662 # roles = Role.objects.filter(role_type__in=['admin', 'pi'])
663 # site_privileges = SitePrivilege.objects.filter(user=request.user).filter(role__in=roles)
664 # login_bases = [site_privilege.site.login_base for site_privilege in site_privileges]
665 # sites = Site.objects.filter(login_base__in=login_bases)
666 # qs = qs.filter(site__in=sites)
669 class SliceForm(forms.ModelForm):
673 'service': LinkedSelect
676 class SliceAdmin(PlanetStackBaseAdmin):
678 fieldList = ['name', 'site', 'serviceClass', 'enabled','description', 'service', 'slice_url', 'max_slivers']
679 fieldsets = [('Slice Details', {'fields': fieldList, 'classes':['suit-tab suit-tab-general']}),]
680 list_display = ('name', 'site','serviceClass', 'slice_url', 'max_slivers')
681 inlines = [SlicePrivilegeInline,SliverInline, TagInline, ReservationInline,SliceNetworkInline]
683 user_readonly_fields = fieldList
684 user_readonly_inlines = [SlicePrivilegeROInline,SliverROInline,TagROInline, ReservationROInline, SliceNetworkROInline]
686 suit_form_tabs =(('general', 'Slice Details'),
687 ('slicenetworks','Networks'),
688 ('sliceprivileges','Privileges'),
689 ('slivers','Slivers'),
691 ('reservations','Reservations'),
694 def formfield_for_foreignkey(self, db_field, request, **kwargs):
695 if db_field.name == 'site':
696 kwargs['queryset'] = Site.select_by_user(request.user)
698 return super(SliceAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
700 def queryset(self, request):
701 # admins can see all keys. Users can only see slices they belong to.
702 return Slice.select_by_user(request.user)
704 def get_formsets(self, request, obj=None):
705 for inline in self.get_inline_instances(request, obj):
706 # hide MyInline in the add view
709 if isinstance(inline, SliverInline):
710 inline.model.caller = request.user
711 yield inline.get_formset(request, obj)
714 class SlicePrivilegeAdmin(PlanetStackBaseAdmin):
716 (None, {'fields': ['user', 'slice', 'role']})
718 list_display = ('user', 'slice', 'role')
720 user_readonly_fields = ['user', 'slice', 'role']
721 user_readonly_inlines = []
723 def formfield_for_foreignkey(self, db_field, request, **kwargs):
724 if db_field.name == 'slice':
725 kwargs['queryset'] = Slice.select_by_user(request.user)
727 if db_field.name == 'user':
728 kwargs['queryset'] = User.select_by_user(request.user)
730 return super(SlicePrivilegeAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
732 def queryset(self, request):
733 # admins can see all memberships. Users can only see memberships of
734 # slices where they have the admin role.
735 return SlicePrivilege.select_by_user(request.user)
737 def save_model(self, request, obj, form, change):
738 # update openstack connection to use this site/tenant
739 auth = request.session.get('auth', {})
740 auth['tenant'] = obj.slice.name
741 obj.os_manager = OpenStackManager(auth=auth, caller=request.user)
744 def delete_model(self, request, obj):
745 # update openstack connection to use this site/tenant
746 auth = request.session.get('auth', {})
747 auth['tenant'] = obj.slice.name
748 obj.os_manager = OpenStackManager(auth=auth, caller=request.user)
752 class ImageAdmin(PlanetStackBaseAdmin):
754 fieldsets = [('Image Details',
755 {'fields': ['name', 'disk_format', 'container_format'],
756 'classes': ['suit-tab suit-tab-general']})
759 suit_form_tabs =(('general','Image Details'),('slivers','Slivers'))
761 inlines = [SliverInline]
763 user_readonly_fields = ['name', 'disk_format', 'container_format']
764 user_readonly_inlines = [SliverROInline]
766 class NodeForm(forms.ModelForm):
769 'site': LinkedSelect,
770 'deployment': LinkedSelect
773 class NodeAdmin(PlanetStackBaseAdmin):
775 list_display = ('name', 'site', 'deployment')
776 list_filter = ('deployment',)
778 inlines = [TagInline,SliverInline]
779 fieldsets = [('Node Details', {'fields': ['name','site','deployment'], 'classes':['suit-tab suit-tab-details']})]
781 user_readonly_fields = ['name','site','deployment']
782 user_readonly_inlines = [TagInline,SliverInline]
784 suit_form_tabs =(('details','Node Details'),('slivers','Slivers'),('tags','Tags'))
787 class SliverForm(forms.ModelForm):
790 ip = forms.CharField(widget=PlainTextWidget)
791 instance_name = forms.CharField(widget=PlainTextWidget)
793 'ip': PlainTextWidget(),
794 'instance_name': PlainTextWidget(),
795 'slice': LinkedSelect,
796 'deploymentNetwork': LinkedSelect,
797 'node': LinkedSelect,
798 'image': LinkedSelect
801 class TagAdmin(PlanetStackBaseAdmin):
802 list_display = ['service', 'name', 'value', 'content_type', 'content_object',]
803 user_readonly_fields = ['service', 'name', 'value', 'content_type', 'content_object',]
804 user_readonly_inlines = []
806 class SliverAdmin(PlanetStackBaseAdmin):
809 ('Sliver Details', {'fields': ['slice', 'deploymentNetwork', 'node', 'ip', 'instance_name', 'numberCores', 'image', ], 'classes': ['suit-tab suit-tab-general'], })
811 list_display = ['ip', 'instance_name', 'slice', 'numberCores', 'image', 'node', 'deploymentNetwork']
813 suit_form_tabs =(('general', 'Sliver Details'),
817 inlines = [TagInline]
819 user_readonly_fields = ['slice', 'deploymentNetwork', 'node', 'ip', 'instance_name', 'numberCores', 'image']
820 user_readonly_inlines = [TagROInline]
822 def formfield_for_foreignkey(self, db_field, request, **kwargs):
823 if db_field.name == 'slice':
824 kwargs['queryset'] = Slice.select_by_user(request.user)
826 return super(SliverAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
828 def queryset(self, request):
829 # admins can see all slivers. Users can only see slivers of
830 # the slices they belong to.
831 return Sliver.select_by_user(request.user)
834 def get_formsets(self, request, obj=None):
835 # make some fields read only if we are updating an existing record
837 #self.readonly_fields = ('ip', 'instance_name')
838 self.readonly_fields = ()
840 self.readonly_fields = ()
841 #self.readonly_fields = ('ip', 'instance_name', 'slice', 'image', 'key')
843 for inline in self.get_inline_instances(request, obj):
844 # hide MyInline in the add view
847 if isinstance(inline, SliverInline):
848 inline.model.caller = request.user
849 yield inline.get_formset(request, obj)
851 #def save_model(self, request, obj, form, change):
852 # # update openstack connection to use this site/tenant
853 # auth = request.session.get('auth', {})
854 # auth['tenant'] = obj.slice.name
855 # obj.os_manager = OpenStackManager(auth=auth, caller=request.user)
856 # obj.creator = request.user
859 #def delete_model(self, request, obj):
860 # # update openstack connection to use this site/tenant
861 # auth = request.session.get('auth', {})
862 # auth['tenant'] = obj.slice.name
863 # obj.os_manager = OpenStackManager(auth=auth, caller=request.user)
866 class UserCreationForm(forms.ModelForm):
867 """A form for creating new users. Includes all the required
868 fields, plus a repeated password."""
869 password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
870 password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput)
874 fields = ('email', 'firstname', 'lastname', 'phone', 'public_key')
876 def clean_password2(self):
877 # Check that the two password entries match
878 password1 = self.cleaned_data.get("password1")
879 password2 = self.cleaned_data.get("password2")
880 if password1 and password2 and password1 != password2:
881 raise forms.ValidationError("Passwords don't match")
884 def save(self, commit=True):
885 # Save the provided password in hashed format
886 user = super(UserCreationForm, self).save(commit=False)
887 user.password = self.cleaned_data["password1"]
888 #user.set_password(self.cleaned_data["password1"])
894 class UserChangeForm(forms.ModelForm):
895 """A form for updating users. Includes all the fields on
896 the user, but replaces the password field with admin's
897 password hash display field.
899 password = ReadOnlyPasswordHashField(label='Password',
900 help_text= '<a href=\"password/\">Change Password</a>.')
905 def clean_password(self):
906 # Regardless of what the user provides, return the initial value.
907 # This is done here, rather than on the field, because the
908 # field does not have access to the initial value
909 return self.initial["password"]
911 class UserDashboardViewInline(PlStackTabularInline):
912 model = UserDashboardView
914 suit_classes = 'suit-tab suit-tab-dashboards'
915 fields = ['user', 'dashboardView', 'order']
917 class UserDashboardViewROInline(ReadOnlyTabularInline):
918 model = UserDashboardView
920 suit_classes = 'suit-tab suit-tab-dashboards'
921 fields = ['user', 'dashboardView', 'order']
923 class UserAdmin(UserAdmin):
927 # The forms to add and change user instances
928 form = UserChangeForm
929 add_form = UserCreationForm
931 # The fields to be used in displaying the User model.
932 # These override the definitions on the base UserAdmin
933 # that reference specific fields on auth.User.
934 list_display = ('email', 'firstname', 'lastname', 'site', 'last_login')
935 #list_display = ('email', 'username','firstname', 'lastname', 'is_admin', 'last_login')
936 list_filter = ('site',)
937 inlines = [SlicePrivilegeInline,SitePrivilegeInline,DeploymentPrivilegeInline,UserDashboardViewInline]
939 fieldListLoginDetails = ['email','site','password','is_readonly','is_amin','public_key']
940 fieldListContactInfo = ['firstname','lastname','phone','timezone']
943 ('Login Details', {'fields': ['email', 'site','password', 'is_readonly', 'is_admin', 'public_key'], 'classes':['suit-tab suit-tab-general']}),
944 ('Contact Information', {'fields': ('firstname','lastname','phone', 'timezone'), 'classes':['suit-tab suit-tab-contact']}),
945 #('Dashboard Views', {'fields': ('dashboards',), 'classes':['suit-tab suit-tab-dashboards']}),
946 #('Important dates', {'fields': ('last_login',)}),
950 'classes': ('wide',),
951 'fields': ('email', 'firstname', 'lastname', 'is_readonly', 'phone', 'public_key','password1', 'password2')}
954 search_fields = ('email',)
955 ordering = ('email',)
956 filter_horizontal = ()
958 user_readonly_fields = fieldListLoginDetails + fieldListContactInfo
959 user_readonly_inlines = [SlicePrivilegeROInline,SitePrivilegeROInline,DeploymentPrivilegeROInline,UserDashboardViewROInline]
961 suit_form_tabs =(('general','Login Details'),
962 ('contact','Contact Information'),
963 ('sliceprivileges','Slice Privileges'),
964 ('siteprivileges','Site Privileges'),
965 ('deploymentprivileges','Deployment Privileges'),
966 ('dashboards','Dashboard Views'))
968 def formfield_for_foreignkey(self, db_field, request, **kwargs):
969 if db_field.name == 'site':
970 kwargs['queryset'] = Site.select_by_user(request.user)
972 return super(UserAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
974 def has_add_permission(self, request, obj=None):
975 return (not self.__user_is_readonly(request))
977 def has_delete_permission(self, request, obj=None):
978 return (not self.__user_is_readonly(request))
980 def get_actions(self,request):
981 actions = super(UserAdmin,self).get_actions(request)
983 if self.__user_is_readonly(request):
984 if 'delete_selected' in actions:
985 del actions['delete_selected']
989 def change_view(self,request,object_id, extra_context=None):
991 if self.__user_is_readonly(request):
992 if not hasattr(self, "readonly_save"):
993 # save the original readonly fields
\r
994 self.readonly_save = self.readonly_fields
\r
995 self.inlines_save = self.inlines
996 self.readonly_fields=self.user_readonly_fields
997 self.inlines = self.user_readonly_inlines
999 if hasattr(self, "readonly_save"):
\r
1000 # restore the original readonly fields
\r
1001 self.readonly_fields = self.readonly_save
\r
1002 self.inlines = self.inlines_save
1005 return super(UserAdmin, self).change_view(request, object_id, extra_context=extra_context)
1006 except PermissionDenied:
1008 if request.method == 'POST':
1009 raise PermissionDenied
1010 request.readonly = True
1011 return super(UserAdmin, self).change_view(request, object_id, extra_context=extra_context)
1013 def __user_is_readonly(self, request):
1014 #groups = [x.name for x in request.user.groups.all() ]
1015 #return "readonly" in groups
1016 return request.user.isReadOnlyUser()
1018 def queryset(self, request):
1019 return User.select_by_user(request.user)
1021 class DashboardViewAdmin(PlanetStackBaseAdmin):
1022 fieldsets = [('Dashboard View Details',
1023 {'fields': ['name', 'url'],
1024 'classes': ['suit-tab suit-tab-general']})
1027 suit_form_tabs =(('general','Dashboard View Details'),)
1029 class ServiceResourceROInline(ReadOnlyTabularInline):
1030 model = ServiceResource
1032 fields = ['serviceClass', 'name', 'maxUnitsDeployment', 'maxUnitsNode', 'maxDuration', 'bucketInRate', 'bucketMaxSize', 'cost', 'calendarReservable']
1034 class ServiceResourceInline(PlStackTabularInline):
1035 model = ServiceResource
1038 class ServiceClassAdmin(PlanetStackBaseAdmin):
1039 list_display = ('name', 'commitment', 'membershipFee')
1040 inlines = [ServiceResourceInline]
1042 user_readonly_fields = ['name', 'commitment', 'membershipFee']
1043 user_readonly_inlines = []
1045 class ReservedResourceROInline(ReadOnlyTabularInline):
1046 model = ReservedResource
1048 fields = ['sliver', 'resource','quantity','reservationSet']
1049 suit_classes = 'suit-tab suit-tab-reservedresources'
1051 class ReservedResourceInline(PlStackTabularInline):
1052 model = ReservedResource
1054 suit_classes = 'suit-tab suit-tab-reservedresources'
1056 def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
1057 field = super(ReservedResourceInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
1059 if db_field.name == 'resource':
1060 # restrict resources to those that the slice's service class allows
1061 if request._slice is not None:
1062 field.queryset = field.queryset.filter(serviceClass = request._slice.serviceClass, calendarReservable=True)
1063 if len(field.queryset) > 0:
1064 field.initial = field.queryset.all()[0]
1066 field.queryset = field.queryset.none()
\r
1067 elif db_field.name == 'sliver':
\r
1068 # restrict slivers to those that belong to the slice
\r
1069 if request._slice is not None:
\r
1070 field.queryset = field.queryset.filter(slice = request._slice)
1072 field.queryset = field.queryset.none()
\r
1076 def queryset(self, request):
1077 return ReservedResource.select_by_user(request.user)
1079 class ReservationChangeForm(forms.ModelForm):
1083 'slice' : LinkedSelect
1086 class ReservationAddForm(forms.ModelForm):
1087 slice = forms.ModelChoiceField(queryset=Slice.objects.all(), widget=forms.Select(attrs={"onChange":"document.getElementById('id_refresh').value=1; submit()"}))
1088 refresh = forms.CharField(widget=forms.HiddenInput())
1091 css = {'all': ('planetstack.css',)} # .field-refresh { display: none; }
1093 def clean_slice(self):
1094 slice = self.cleaned_data.get("slice")
1095 x = ServiceResource.objects.filter(serviceClass = slice.serviceClass, calendarReservable=True)
1097 raise forms.ValidationError("The slice you selected does not have a service class that allows reservations")
1103 'slice' : LinkedSelect
1107 class ReservationAddRefreshForm(ReservationAddForm):
1108 """ This form is displayed when the Reservation Form receives an update
1109 from the Slice dropdown onChange handler. It doesn't validate the
1110 data and doesn't save the data. This will cause the form to be
1114 """ don't validate anything other than slice """
1115 dont_validate_fields = ("startTime", "duration")
1117 def full_clean(self):
1118 result = super(ReservationAddForm, self).full_clean()
1120 for fieldname in self.dont_validate_fields:
1121 if fieldname in self._errors:
1122 del self._errors[fieldname]
1126 """ don't save anything """
1130 class ReservationAdmin(PlanetStackBaseAdmin):
1131 fieldList = ['slice', 'startTime', 'duration']
1132 fieldsets = [('Reservation Details', {'fields': fieldList, 'classes': ['suit-tab suit-tab-general']})]
1133 list_display = ('startTime', 'duration')
1134 form = ReservationAddForm
1136 suit_form_tabs = (('general','Reservation Details'), ('reservedresources','Reserved Resources'))
1138 inlines = [ReservedResourceInline]
1139 user_readonly_inlines = [ReservedResourceROInline]
1140 user_readonly_fields = fieldList
1142 def add_view(self, request, form_url='', extra_context=None):
1143 timezone.activate(request.user.timezone)
1144 request._refresh = False
1145 request._slice = None
1146 if request.method == 'POST':
1147 # "refresh" will be set to "1" if the form was submitted due to
1148 # a change in the Slice dropdown.
1149 if request.POST.get("refresh","1") == "1":
1150 request._refresh = True
1151 request.POST["refresh"] = "0"
1153 # Keep track of the slice that was selected, so the
1154 # reservedResource inline can filter items for the slice.
1155 request._slice = request.POST.get("slice",None)
1156 if (request._slice is not None):
1157 request._slice = Slice.objects.get(id=request._slice)
1159 result = super(ReservationAdmin, self).add_view(request, form_url, extra_context)
1162 def changelist_view(self, request, extra_context = None):
1163 timezone.activate(request.user.timezone)
1164 return super(ReservationAdmin, self).changelist_view(request, extra_context)
1166 def get_form(self, request, obj=None, **kwargs):
1169 # For changes, set request._slice to the slice already set in the
1171 request._slice = obj.slice
1172 self.form = ReservationChangeForm
1174 if getattr(request, "_refresh", False):
1175 self.form = ReservationAddRefreshForm
1177 self.form = ReservationAddForm
1178 return super(ReservationAdmin, self).get_form(request, obj, **kwargs)
1180 def get_readonly_fields(self, request, obj=None):
1181 if (obj is not None):
1182 # Prevent slice from being changed after the reservation has been
1188 def queryset(self, request):
1189 return Reservation.select_by_user(request.user)
1191 class NetworkParameterTypeAdmin(PlanetStackBaseAdmin):
1192 list_display = ("name", )
1193 user_readonly_fields = ['name']
1194 user_readonly_inlines = []
1196 class RouterAdmin(PlanetStackBaseAdmin):
1197 list_display = ("name", )
1198 user_readonly_fields = ['name']
1199 user_readonly_inlines = []
1201 class RouterROInline(ReadOnlyTabularInline):
1202 model = Router.networks.through
1204 verbose_name_plural = "Routers"
1205 verbose_name = "Router"
1206 suit_classes = 'suit-tab suit-tab-routers'
1208 fields = ['name', 'owner', 'permittedNetworks', 'networks']
1210 class RouterInline(PlStackTabularInline):
1211 model = Router.networks.through
1213 verbose_name_plural = "Routers"
1214 verbose_name = "Router"
1215 suit_classes = 'suit-tab suit-tab-routers'
1217 class NetworkParameterROInline(ReadOnlyTabularInline):
1218 model = NetworkParameter
1220 verbose_name_plural = "Parameters"
1221 verbose_name = "Parameter"
1222 suit_classes = 'suit-tab suit-tab-netparams'
1223 fields = ['parameter', 'value', 'content_type', 'object_id', 'content_object']
1225 class NetworkParameterInline(generic.GenericTabularInline):
1226 model = NetworkParameter
1228 verbose_name_plural = "Parameters"
1229 verbose_name = "Parameter"
1230 suit_classes = 'suit-tab suit-tab-netparams'
1232 class NetworkSliversROInline(ReadOnlyTabularInline):
1233 fields = ['network', 'sliver', 'ip', 'port_id']
1234 model = NetworkSliver
1236 verbose_name_plural = "Slivers"
1237 verbose_name = "Sliver"
1238 suit_classes = 'suit-tab suit-tab-networkslivers'
1240 class NetworkSliversInline(PlStackTabularInline):
1241 readonly_fields = ("ip", )
1242 model = NetworkSliver
1243 selflink_fieldname = "sliver"
1245 verbose_name_plural = "Slivers"
1246 verbose_name = "Sliver"
1247 suit_classes = 'suit-tab suit-tab-networkslivers'
1249 class NetworkSlicesROInline(ReadOnlyTabularInline):
1250 model = NetworkSlice
1252 verbose_name_plural = "Slices"
1253 verbose_name = "Slice"
1254 suit_classes = 'suit-tab suit-tab-networkslices'
1255 fields = ['network','slice']
1257 class NetworkSlicesInline(PlStackTabularInline):
1258 model = NetworkSlice
1259 selflink_fieldname = "slice"
1261 verbose_name_plural = "Slices"
1262 verbose_name = "Slice"
1263 suit_classes = 'suit-tab suit-tab-networkslices'
1265 class NetworkAdmin(PlanetStackBaseAdmin):
1266 list_display = ("name", "subnet", "ports", "labels")
1267 readonly_fields = ("subnet", )
1269 inlines = [NetworkParameterInline, NetworkSliversInline, NetworkSlicesInline, RouterInline]
1272 (None, {'fields': ['name','template','ports','labels','owner','guaranteedBandwidth', 'permitAllSlices','permittedSlices','network_id','router_id','subnet_id','subnet'], 'classes':['suit-tab suit-tab-general']}),]
1274 user_readonly_fields = ['name','template','ports','labels','owner','guaranteedBandwidth', 'permitAllSlices','permittedSlices','network_id','router_id','subnet_id','subnet']
1275 user_readonly_inlines = [NetworkParameterROInline, NetworkSliversROInline, NetworkSlicesROInline, RouterROInline]
1278 ('general','Network Details'),
1279 ('netparams', 'Parameters'),
1280 ('networkslivers','Slivers'),
1281 ('networkslices','Slices'),
1282 ('routers','Routers'),
1284 class NetworkTemplateAdmin(PlanetStackBaseAdmin):
1285 list_display = ("name", "guaranteedBandwidth", "visibility")
1286 user_readonly_fields = ["name", "guaranteedBandwidth", "visibility"]
1287 user_readonly_inlines = []
1289 # register a signal that caches the user's credentials when they log in
1290 def cache_credentials(sender, user, request, **kwds):
1291 auth = {'username': request.POST['username'],
1292 'password': request.POST['password']}
1293 request.session['auth'] = auth
1294 user_logged_in.connect(cache_credentials)
1296 def dollar_field(fieldName, short_description):
1297 def newFunc(self, obj):
1299 x= "$ %0.2f" % float(getattr(obj, fieldName, 0.0))
1301 x=getattr(obj, fieldName, 0.0)
1303 newFunc.short_description = short_description
1306 def right_dollar_field(fieldName, short_description):
1307 def newFunc(self, obj):
1309 #x= '<div align=right style="width:6em">$ %0.2f</div>' % float(getattr(obj, fieldName, 0.0))
1310 x= '<div align=right>$ %0.2f</div>' % float(getattr(obj, fieldName, 0.0))
1312 x=getattr(obj, fieldName, 0.0)
1314 newFunc.short_description = short_description
1315 newFunc.allow_tags = True
1318 class InvoiceChargeInline(PlStackTabularInline):
1321 verbose_name_plural = "Charges"
1322 verbose_name = "Charge"
1323 exclude = ['account']
1324 fields = ["date", "kind", "state", "object", "coreHours", "dollar_amount", "slice"]
1325 readonly_fields = ["date", "kind", "state", "object", "coreHours", "dollar_amount", "slice"]
1329 dollar_amount = right_dollar_field("amount", "Amount")
1331 class InvoiceAdmin(admin.ModelAdmin):
1332 list_display = ("date", "account")
1334 inlines = [InvoiceChargeInline]
1336 fields = ["date", "account", "dollar_amount"]
1337 readonly_fields = ["date", "account", "dollar_amount"]
1339 dollar_amount = dollar_field("amount", "Amount")
1341 class InvoiceInline(PlStackTabularInline):
1344 verbose_name_plural = "Invoices"
1345 verbose_name = "Invoice"
1346 fields = ["date", "dollar_amount"]
1347 readonly_fields = ["date", "dollar_amount"]
1348 suit_classes = 'suit-tab suit-tab-accountinvoice'
1352 dollar_amount = right_dollar_field("amount", "Amount")
1354 class PendingChargeInline(PlStackTabularInline):
1357 verbose_name_plural = "Charges"
1358 verbose_name = "Charge"
1359 exclude = ["invoice"]
1360 fields = ["date", "kind", "state", "object", "coreHours", "dollar_amount", "slice"]
1361 readonly_fields = ["date", "kind", "state", "object", "coreHours", "dollar_amount", "slice"]
1362 suit_classes = 'suit-tab suit-tab-accountpendingcharges'
1366 def queryset(self, request):
1367 qs = super(PendingChargeInline, self).queryset(request)
1368 qs = qs.filter(state="pending")
1371 dollar_amount = right_dollar_field("amount", "Amount")
1373 class PaymentInline(PlStackTabularInline):
1376 verbose_name_plural = "Payments"
1377 verbose_name = "Payment"
1378 fields = ["date", "dollar_amount"]
1379 readonly_fields = ["date", "dollar_amount"]
1380 suit_classes = 'suit-tab suit-tab-accountpayments'
1384 dollar_amount = right_dollar_field("amount", "Amount")
1386 class AccountAdmin(admin.ModelAdmin):
1387 list_display = ("site", "balance_due")
1389 inlines = [InvoiceInline, PaymentInline, PendingChargeInline]
1392 (None, {'fields': ['site', 'dollar_balance_due', 'dollar_total_invoices', 'dollar_total_payments'],'classes':['suit-tab suit-tab-general']}),]
1394 readonly_fields = ['site', 'dollar_balance_due', 'dollar_total_invoices', 'dollar_total_payments']
1397 ('general','Account Details'),
1398 ('accountinvoice', 'Invoices'),
1399 ('accountpayments', 'Payments'),
1400 ('accountpendingcharges','Pending Charges'),
1403 dollar_balance_due = dollar_field("balance_due", "Balance Due")
1404 dollar_total_invoices = dollar_field("total_invoices", "Total Invoices")
1405 dollar_total_payments = dollar_field("total_payments", "Total Payments")
1408 # Now register the new UserAdmin...
1409 admin.site.register(User, UserAdmin)
1410 # ... and, since we're not using Django's builtin permissions,
1411 # unregister the Group model from admin.
1412 #admin.site.unregister(Group)
1414 #Do not show django evolution in the admin interface
1415 from django_evolution.models import Version, Evolution
1416 #admin.site.unregister(Version)
1417 #admin.site.unregister(Evolution)
1420 # When debugging it is often easier to see all the classes, but for regular use
1421 # only the top-levels should be displayed
1424 admin.site.register(Deployment, DeploymentAdmin)
1425 admin.site.register(Site, SiteAdmin)
1426 admin.site.register(Slice, SliceAdmin)
1427 admin.site.register(Service, ServiceAdmin)
1428 admin.site.register(Reservation, ReservationAdmin)
1429 admin.site.register(Network, NetworkAdmin)
1430 admin.site.register(Router, RouterAdmin)
1431 admin.site.register(NetworkTemplate, NetworkTemplateAdmin)
1432 admin.site.register(Account, AccountAdmin)
1433 admin.site.register(Invoice, InvoiceAdmin)
1436 admin.site.register(NetworkParameterType, NetworkParameterTypeAdmin)
1437 admin.site.register(ServiceClass, ServiceClassAdmin)
1438 #admin.site.register(PlanetStack)
1439 admin.site.register(Tag, TagAdmin)
1440 admin.site.register(DeploymentRole)
1441 admin.site.register(SiteRole)
1442 admin.site.register(SliceRole)
1443 admin.site.register(PlanetStackRole)
1444 admin.site.register(Node, NodeAdmin)
1445 #admin.site.register(SlicePrivilege, SlicePrivilegeAdmin)
1446 #admin.site.register(SitePrivilege, SitePrivilegeAdmin)
1447 admin.site.register(Sliver, SliverAdmin)
1448 admin.site.register(Image, ImageAdmin)
1449 admin.site.register(DashboardView, DashboardViewAdmin)