6 from django.views.generic import TemplateView, View
8 from pprint import pprint
10 from syndicate.models import *
11 from core.models import *
12 from hpc.models import ContentProvider
13 from operator import attrgetter
14 from django import template
15 from django.views.decorators.csrf import csrf_exempt
16 from django.http import HttpResponse, HttpResponseServerError
17 from django.core import urlresolvers
18 from django.contrib.gis.geoip import GeoIP
19 from ipware.ip import get_ip
20 from operator import itemgetter, attrgetter
24 BLESSED_SITES = ["Stanford", "Washington", "Princeton", "GeorgiaTech", "MaxPlanck"]
26 if os.path.exists("/home/smbaker/projects/vicci/cdn/bigquery"):
27 sys.path.append("/home/smbaker/projects/vicci/cdn/bigquery")
29 sys.path.append("/opt/planetstack/hpc_wizard")
31 from planetstack_analytics import DoPlanetStackAnalytics, PlanetStackAnalytics, RED_LOAD, BLUE_LOAD
33 class DashboardWelcomeView(TemplateView):
34 template_name = 'admin/dashboard/welcome.html'
36 def get(self, request, *args, **kwargs):
37 context = self.get_context_data(**kwargs)
38 context = getDashboardContext(request.user, context)
39 return self.render_to_response(context=context)
41 class DashboardDynamicView(TemplateView):
42 head_template = r"""{% extends "admin/dashboard/dashboard_base.html" %}
43 {% load admin_static %}
47 tail_template = r"{% endblock %}"
49 def get(self, request, name="root", *args, **kwargs):
50 context = self.get_context_data(**kwargs)
51 context = getDashboardContext(request.user, context)
54 return self.multiDashboardView(request, context)
56 return self.singleDashboardView(request, name, context)
58 def readDashboard(self, fn):
60 template= open("/opt/planetstack/templates/admin/dashboard/%s.html" % fn, "r").read()
62 template = '<div id="tabs-5"></div>' + template
65 return "failed to open %s" % fn
67 def multiDashboardView(self, request, context):
68 head_template = self.head_template
69 tail_template = self.tail_template
73 <ul id="suit_form_tabs" class="nav nav-tabs nav-tabs-suit" data-tab-prefix="suit-tab">
76 dashboards = request.user.get_dashboards()
78 # customize is a special dashboard they always get
79 customize = DashboardView.objects.filter(name="Customize")
81 dashboards.append(customize[0])
83 for i,view in enumerate(dashboards):
84 body = body + '<li><a href="#dashtab-%d">%s</a></li>\n' % (i, view.name)
86 body = body + "</ul>\n"
88 for i,view in enumerate(dashboards):
90 body = body + '<div id="dashtab-%d">\n' % i
91 if url.startswith("template:"):
93 body = body + self.readDashboard(fn)
94 body = body + '</div>\n'
98 t = template.Template(head_template + body + self.tail_template)
101 response_kwargs.setdefault('content_type', self.content_type)
102 return self.response_class(
\r
108 def singleDashboardView(self, request, name, context):
109 head_template = self.head_template
110 tail_template = self.tail_template
112 t = template.Template(head_template + self.readDashboard(fn) + self.tail_template)
115 response_kwargs.setdefault('content_type', self.content_type)
116 return self.response_class(
\r
122 def getDashboardContext(user, context={}, tableFormat = False):
125 userSliceData = getSliceInfo(user)
127 context['userSliceInfo'] = userSliceTableFormatter(userSliceData)
129 context['userSliceInfo'] = userSliceData
130 context['cdnData'] = getCDNOperatorData(wait=False)
131 context['cdnContentProviders'] = getCDNContentProviderData()
133 (dashboards, unusedDashboards)= getDashboards(user)
134 unusedDashboards=[x for x in unusedDashboards if x!="Customize"]
135 context['dashboards'] = dashboards
136 context['unusedDashboards'] = unusedDashboards
140 def getDashboards(user):
141 #dashboards = sorted(list(user.dashboardViews.all()), key=attrgetter('order'))
142 dashboards = user.get_dashboards()
144 dashboard_names = [d.name for d in dashboards]
146 unused_dashboard_names = []
147 for dashboardView in DashboardView.objects.all():
148 if not dashboardView.name in dashboard_names:
149 unused_dashboard_names.append(dashboardView.name)
151 return (dashboard_names, unused_dashboard_names)
153 class TenantCreateSlice(View):
154 def post(self, request, *args, **kwargs):
155 sliceName = request.POST.get("sliceName", "0")
156 serviceClass = request.POST.get("serviceClass", "0")
157 imageName = request.POST.get("imageName", "0")
158 actionToDo = request.POST.get("actionToDo", "0")
159 #network = request.POST.get("network","0")
160 mountDataSets = request.POST.get("mountDataSets","0")
161 if (actionToDo == "add"):
162 serviceClass = ServiceClass.objects.get(name=serviceClass)
163 site = request.user.site
164 image = Image.objects.get(name=imageName)
165 newSlice = Slice(name=sliceName,serviceClass=serviceClass,site=site,imagePreference=image,mountDataSets=mountDataSets)
167 return HttpResponse("Slice created")
169 class TenantUpdateSlice(View):
170 def post(self, request, *args, **kwargs):
\r
171 sliceName = request.POST.get("sliceName", "0")
\r
172 serviceClass = request.POST.get("serviceClass", "0")
\r
173 imageName = request.POST.get("imageName", "0")
\r
174 actionToDo = request.POST.get("actionToDo", "0")
\r
175 #network = request.POST.get("network","0")
\r
176 dataSet = request.POST.get("dataSet","0")
\r
177 slice = Slice.objects.all()
\r
178 for entry in slice:
\r
179 serviceClass = ServiceClass.objects.get(name=serviceClass)
\r
180 if(entry.name==sliceName):
\r
181 if (actionToDo == "update"):
\r
182 setattr(entry,'serviceClass',serviceClass)
\r
183 setattr(entry,'imagePreference',imageName)
\r
184 #setattr(entry,'network',network)
\r
185 setattr(entry,'mountDataSets',dataSet)
\r
188 return HttpResponse("Slice updated")
\r
190 def getTenantSliceInfo(user, tableFormat = False):
191 tenantSliceDetails = {}
192 tenantSliceData = getTenantInfo(user)
193 tenantServiceClassData = getServiceClassInfo(user)
195 tenantSliceDetails['userSliceInfo'] = userSliceTableFormatter(tenantSliceData)
196 tenantSliceDetails['sliceServiceClass']=userSliceTableFormatter(tenantServiceClassData)
198 tenantSliceDetails['userSliceInfo'] = tenantSliceData
199 tenantSliceDetails['sliceServiceClass']=userSliceTableFormatter(tenantServiceClassData)
200 tenantSliceDetails['image']=userSliceTableFormatter(getImageInfo(user))
201 #tenantSliceDetails['network']=userSliceTableFormatter(getNetworkInfo(user))
202 tenantSliceDetails['deploymentSites']=userSliceTableFormatter(getDeploymentSites())
203 tenantSliceDetails['sites'] = userSliceTableFormatter(getTenantSitesInfo())
204 tenantSliceDetails['mountDataSets'] = userSliceTableFormatter(getMountDataSets())
205 tenantSliceDetails['publicKey'] = getPublicKey(user)
206 return tenantSliceDetails
208 def getTenantInfo(user):
209 slices =Slice.objects.all()
212 sliceName = Slice.objects.get(id=entry.id).name
213 slice = Slice.objects.get(name=Slice.objects.get(id=entry.id).name)
214 sliceServiceClass = entry.serviceClass.name
215 preferredImage = entry.imagePreference
216 sliceDataSet = entry.mountDataSets
217 #sliceNetwork = entry.network
223 for sliver in slice.slivers.all():
224 if sliver.node.site.name in BLESSED_SITES:
225 sliceSite[sliver.node.site.name] = sliceSite.get(sliver.node.site.name,0) + 1
226 sliceImage = sliver.image.name
227 sliceNode[str(sliver)] = sliver.node.name
228 numSliver = sum(sliceSite.values())
229 numSites = len(sliceSite)
230 userSliceInfo.append({'sliceName': sliceName,'sliceServiceClass': sliceServiceClass,'preferredImage':preferredImage,'numOfSites':numSites, 'sliceSite':sliceSite,'sliceImage':sliceImage,'numOfSlivers':numSliver,'sliceDataSet':sliceDataSet,'instanceNodePair':sliceNode})
233 def getTenantSitesInfo():
235 for entry in Site.objects.all():
236 if entry.name in BLESSED_SITES:
237 tenantSiteInfo.append({'siteName':entry.name})
238 return tenantSiteInfo
240 def userSliceTableFormatter(data):
247 def getPublicKey(user):
248 users=User.objects.all()
\r
250 if (str(key.email)==str(user)):
\r
251 sshKey = key.public_key
\r
254 def getServiceClassInfo(user):
255 serviceClassList = ServiceClass.objects.all()
257 for entry in serviceClassList:
258 sliceInfo.append({'serviceClass':entry.name})
261 def getImageInfo(user):
262 imageList = Image.objects.all()
263 #imageList = ['Fedora 16 LXC rev 1.3','Hadoop','MPI']
265 for imageEntry in imageList:
266 imageInfo.append({'Image':imageEntry.name})
267 #imageInfo.append({'Image':imageEntry})
270 def createPrivateVolume(user, sliceName):
271 caps = Volume.CAP_READ_DATA | Volume.CAP_WRITE_DATA | Volume.CAP_HOST_DATA
272 getattr(Volume.default_gateway_caps,"read data") | \
273 getattr(Volume.default_gateway_caps,"write data") | \
274 getattr(Volume.default_gateway_caps,"host files")
275 v = Volume(name="private_" + sliceName, owner_id=user, description="private volume for %s" % sliceName, blocksize=61440, private=True, archive=False, default_gateway_caps = caps)
278 SYNDICATE_REPLICATE_PORTNUM = 1025
282 inuse[SYNDICATE_REPLICATE_PORTNUM] = True
283 for vs in VolumeSlice.objects.all():
284 inuse[vs.peer_portnum]=True
285 inuse[vs.replicate_portnum]=True
286 for network in Network.objects.all():
287 network_ports = [x.strip() for x in network.ports.split(",")]
288 for network_port in network_ports:
290 inuse[int(network_port)] = True
292 # in case someone has put a malformed port number in the list
294 for i in range(1025, 65535):
295 if not inuse.get(i,False):
299 def mountVolume(sliceName, volumeName, readWrite):
300 slice = Slice.objects.get(name=sliceName)
301 volume = Volume.objects.get(name=volumeName)
302 # choose some unused port numbers
303 flags = Volume.CAP_READ_DATA
305 flags = flags | Volume.CAP_WRITE_DATA
306 vs = VolumeSlice(volume_id = volume, slice_id = slice, gateway_caps=flags, peer_portnum = get_free_port(), replicate_portnum = SYNDICATE_REPLICATE_PORTNUM)
309 def hasPrivateVolume(sliceName):
310 slice = Slice.objects.get(name=sliceName)
311 for vs in VolumeSlice.objects.filter(slice_id=slice):
312 if vs.volume_id.private:
316 def getMountDataSets():
318 for volume in Volume.objects.all():
\r
319 if not volume.private:
\r
320 dataSetInfo.append({'DataSet': volume.name})
\r
324 def getNetworkInfo(user):
325 #networkList = Network.objects.all()
326 networkList = ['Private Only','Private and Publicly Routable']
328 for networkEntry in networkList:
329 #networkInfo.append({'Network':networkEntry.name})
330 networkInfo.append({'Network':networkEntry})
333 def getDeploymentSites():
334 deploymentList = Deployment.objects.all()
336 for entry in deploymentList:
337 deploymentInfo.append({'DeploymentSite':entry.name})
338 return deploymentInfo
340 def getSliceInfo(user):
341 sliceList = Slice.objects.all()
342 slicePrivs = SlicePrivilege.objects.filter(user=user)
344 for entry in slicePrivs:
346 slicename = Slice.objects.get(id=entry.slice.id).name
347 slice = Slice.objects.get(name=Slice.objects.get(id=entry.slice.id).name)
348 sliverList=Sliver.objects.all()
350 for sliver in slice.slivers.all():
351 #sites_used['deploymentSites'] = sliver.node.deployment.name
352 # sites_used[sliver.image.name] = sliver.image.name
353 sites_used[sliver.node.site.name] = sliver.numberCores
354 sliceid = Slice.objects.get(id=entry.slice.id).id
356 sliverList = Sliver.objects.filter(slice=entry.slice.id)
359 if x.node.site not in siteList:
360 siteList[x.node.site] = 1
361 slivercount = len(sliverList)
362 sitecount = len(siteList)
364 traceback.print_exc()
368 userSliceInfo.append({'slicename': slicename, 'sliceid':sliceid,
369 'sitesUsed':sites_used,
370 'role': SliceRole.objects.get(id=entry.role.id).role,
371 'slivercount': slivercount,
372 'sitecount':sitecount})
376 def getCDNContentProviderData():
378 for dm_cp in ContentProvider.objects.all():
379 cp = {"name": dm_cp.name,
380 "account": dm_cp.account}
385 def getCDNOperatorData(randomizeData = False, wait=True):
386 HPC_SLICE_NAME = "HyperCache"
388 bq = PlanetStackAnalytics()
390 rows = bq.get_cached_query_results(bq.compose_cached_query(), wait)
392 # wait=False on the first time the Dashboard is opened. This means we might
393 # not have any rows yet. The dashboard code polls every 30 seconds, so it
394 # will eventually pick them up.
397 rows = bq.postprocess_results(rows, filter={"event": "hpc_heartbeat"}, maxi=["cpu"], count=["hostname"], computed=["bytes_sent/elapsed"], groupBy=["Time","site"], maxDeltaTime=80)
399 # dictionaryize the statistics rows by site name
402 stats_rows[row["site"]] = row
406 slice = Slice.objects.filter(name=HPC_SLICE_NAME)
408 slice_slivers = list(slice[0].slivers.all())
413 for site in Site.objects.all():
414 # compute number of slivers allocated in the data model
415 allocated_slivers = 0
416 for sliver in slice_slivers:
417 if sliver.node.site == site:
418 allocated_slivers = allocated_slivers + 1
420 stats_row = stats_rows.get(site.name,{})
422 max_cpu = stats_row.get("max_avg_cpu", stats_row.get("max_cpu",0))
423 cpu=float(max_cpu)/100.0
424 hotness = max(0.0, ((cpu*RED_LOAD) - BLUE_LOAD)/(RED_LOAD-BLUE_LOAD))
426 # format it to what that CDN Operations View is expecting
427 new_row = {"lat": float(site.location.longitude),
428 "long": float(site.location.longitude),
429 "lat": float(site.location.latitude),
431 "numNodes": int(site.nodes.count()),
432 "activeHPCSlivers": int(stats_row.get("count_hostname", 0)), # measured number of slivers, from bigquery statistics
433 "numHPCSlivers": allocated_slivers, # allocated number of slivers, from data model
434 "siteUrl": str(site.site_url),
435 "bandwidth": stats_row.get("sum_computed_bytes_sent_div_elapsed",0),
437 "hot": float(hotness)}
438 new_rows[str(site.name)] = new_row
440 # get rid of sites with 0 slivers that overlap other sites with >0 slivers
441 for (k,v) in new_rows.items():
443 if v["numHPCSlivers"]==0:
444 for v2 in new_rows.values():
445 if (v!=v2) and (v2["numHPCSlivers"]>=0):
446 d = haversine(v["lat"],v["long"],v2["lat"],v2["long"])
454 def getPageSummary(request):
455 slice = request.GET.get('slice', None)
456 site = request.GET.get('site', None)
457 node = request.GET.get('node', None)
459 class SimulatorView(View):
460 def get(self, request, **kwargs):
461 sim = json.loads(file("/tmp/simulator.json","r").read())
462 text = "<html><head></head><body>"
463 text += "Iteration: %d<br>" % sim["iteration"]
464 text += "Elapsed since report %d<br><br>" % sim["elapsed_since_report"]
465 text += "<table border=1>"
466 text += "<tr><th>site</th><th>trend</th><th>weight</th><th>bytes_sent</th><th>hot</th></tr>"
467 for site in sim["site_load"].values():
469 text += "<td>%s</td><td>%0.2f</td><td>%0.2f</td><td>%d</td><td>%0.2f</td>" % \
470 (site["name"], site["trend"], site["weight"], site["bytes_sent"], site["load_frac"])
473 text += "</body></html>"
474 return HttpResponse(text)
476 class DashboardUserSiteView(View):
477 def get(self, request, **kwargs):
478 return HttpResponse(json.dumps(getDashboardContext(request.user, tableFormat=True)), mimetype='application/javascript')
480 class TenantViewData(View):
481 def get(self, request, **kwargs):
482 return HttpResponse(json.dumps(getTenantSliceInfo(request.user, True)), mimetype='application/javascript')
484 def haversine(site_lat, site_lon, lat, lon):
486 if lat and lon and site_lat and site_lon:
487 site_lat = float(site_lat)
488 site_lon = float(site_lon)
492 a = math.sin( math.radians((lat - site_lat)/2.0) )**2 + math.cos( math.radians(lat) )*math.cos( math.radians(site_lat) )*(math.sin( math.radians((lon - site_lon)/2.0 ) )**2)
493 c = 2 * math.atan2( math.sqrt(a), math.sqrt(1 - a) )
498 def siteSortKey(site, slice=None, count=None, lat=None, lon=None):
499 # try to pick a site we're already using
500 has_slivers_here=False
502 for sliver in slice.slivers.all():
503 if sliver.node.site.name == site.name:
504 has_slivers_here=True
507 d = haversine(site.location.latitude, site.location.longitude, lat, lon)
509 return (-has_slivers_here, d)
511 def tenant_pick_sites(user, user_ip=None, slice=None, count=None):
512 """ Returns list of sites, sorted from most favorable to least favorable """
516 client_geo = GeoIP().city(user_ip)
518 lat=float(client_geo["latitude"])
519 lon=float(client_geo["longitude"])
521 print "exception in geo code"
522 traceback.print_exc()
524 sites = Site.objects.all()
525 sites = [x for x in sites if x.name in BLESSED_SITES]
526 sites = sorted(sites, key=functools.partial(siteSortKey, slice=slice, count=count, lat=lat, lon=lon))
530 def slice_increase_slivers(user, user_ip, siteList, slice, count, noAct=False):
533 # let's compute how many slivers are in use in each node of each site
534 for site in siteList:
535 site.nodeList = list(site.nodes.all())
536 for node in site.nodeList:
538 for sliver in node.slivers.all():
539 if sliver.slice.id == slice.id:
540 node.sliverCount = node.sliverCount + 1
542 # Allocate slivers to nodes
543 # for now, assume we want to allocate all slivers from the same site
544 nodes = siteList[0].nodeList
546 # Sort the node list by number of slivers per node, then pick the
547 # node with the least number of slivers.
548 nodes = sorted(nodes, key=attrgetter("sliverCount"))
551 print "adding sliver at node", node.name, "of site", node.site.name
554 sliver = Sliver(name=node.name,
557 image = Image.objects.all()[0],
558 creator = User.objects.get(email=user),
559 deploymentNetwork=node.deployment,
563 node.sliverCount = node.sliverCount + 1
567 sitesChanged[node.site.name] = sitesChanged.get(node.site.name,0) + 1
571 def slice_decrease_slivers(user, siteList, slice, count, noAct=False):
575 siteNames = [site.name for site in siteList]
579 for sliver in slice.slivers.all():
580 if(not siteNames) or (sliver.node.site.name in siteNames):
\r
582 sliverList[sliver.name]=node.name
584 for key in sliverList:
586 sliver = Sliver.objects.filter(name=key)[0]
\r
588 print "deleting sliver",sliverList[key],"at node",sliver.node.name
\r
590 sitesChanged[sliver.node.site.name] = sitesChanged.get(sliver.node.site.name,0) - 1
\r
594 class TenantDeleteSliceView(View):
595 def post(self,request):
\r
596 sliceName = request.POST.get("sliceName",None)
\r
597 slice = Slice.objects.get(name=sliceName)
\r
598 #print slice, slice.id
\r
599 sliceToDel=Slice(name=sliceName, id=slice.id)
\r
601 return HttpResponse("Slice deleted")
603 class TenantAddOrRemoveSliverView(View):
604 """ Add or remove slivers from a Slice
607 siteName - name of site. If not specified, PlanetStack will pick the
609 actionToDo - [add | rem]
610 count - number of slivers to add or remove
611 sliceName - name of slice
612 noAct - if set, no changes will be made to db, but result will still
613 show which sites would have been modified.
616 Dictionary of sites that were modified, and the count of nodes
617 that were added or removed at each site.
619 def post(self, request, *args, **kwargs):
620 siteName = request.POST.get("siteName", None)
621 actionToDo = request.POST.get("actionToDo", None)
622 count = int(request.POST.get("count","0"))
623 sliceName = request.POST.get("slice", None)
624 noAct = request.POST.get("noAct", False)
627 return HttpResponseServerError("No slice name given")
629 slice = Slice.objects.get(name=sliceName)
632 siteList = [Site.objects.get(name=siteName)]
636 if (actionToDo == "add"):
637 user_ip = request.GET.get("ip", get_ip(request))
638 if (siteList is None):
639 siteList = tenant_pick_sites(user, user_ip, slice, count)
641 sitesChanged = slice_increase_slivers(request.user, user_ip, siteList, slice, count, noAct)
642 elif (actionToDo == "rem"):
643 sitesChanged = slice_decrease_slivers(request.user, siteList, slice, count, noAct)
645 return HttpResponseServerError("Unknown actionToDo %s" % actionToDo)
647 return HttpResponse(json.dumps(sitesChanged), mimetype='application/javascript')
649 def get(self, request, *args, **kwargs):
650 request.POST = request.GET
651 return self.post(request, *args, **kwargs) # for testing REST in browser
652 #return HttpResponseServerError("GET is not supported")
654 class TenantPickSitesView(View):
655 """ primarily just for testing purposes """
656 def get(self, request, *args, **kwargs):
657 count = request.GET.get("count","0")
658 slice = request.GET.get("slice",None)
660 slice = Slice.objects.get(name=slice)
661 ip = request.GET.get("ip", get_ip(request))
662 sites = tenant_pick_sites(request.user, user_ip=ip, count=0, slice=slice)
663 sites = [x.name for x in sites]
664 return HttpResponse(json.dumps(sites), mimetype='application/javascript')
666 class DashboardSummaryAjaxView(View):
667 def get(self, request, **kwargs):
671 return float(sum(x))/len(x)
673 sites = getCDNOperatorData().values()
675 sites = [site for site in sites if site["numHPCSlivers"]>0]
677 total_slivers = sum( [site["numHPCSlivers"] for site in sites] )
678 total_bandwidth = sum( [site["bandwidth"] for site in sites] )
679 average_cpu = int(avg( [site["load"] for site in sites] ))
681 result= {"total_slivers": total_slivers,
682 "total_bandwidth": total_bandwidth,
683 "average_cpu": average_cpu}
685 return HttpResponse(json.dumps(result), mimetype='application/javascript')
687 class DashboardAddOrRemoveSliverView(View):
688 # TODO: deprecate this view in favor of using TenantAddOrRemoveSliverView
690 def post(self, request, *args, **kwargs):
691 siteName = request.POST.get("site", None)
692 actionToDo = request.POST.get("actionToDo", "0")
694 siteList = [Site.objects.get(name=siteName)]
695 slice = Slice.objects.get(name="HyperCache")
697 if (actionToDo == "add"):
698 user_ip = request.GET.get("ip", get_ip(request))
699 slice_increase_slivers(request.user, user_ip, siteList, slice, 1)
700 elif (actionToDo == "rem"):
701 slice_decrease_slivers(request.user, siteList, slice, 1)
704 print 'Ask for site: ' + siteName + ' to ' + actionToDo + ' another HPC Sliver'
705 return HttpResponse('This is POST request ')
707 class DashboardAjaxView(View):
708 def get(self, request, **kwargs):
709 return HttpResponse(json.dumps(getCDNOperatorData(True)), mimetype='application/javascript')
711 class DashboardAnalyticsAjaxView(View):
712 def get(self, request, name="hello_world", **kwargs):
713 if (name == "hpcSummary"):
714 return HttpResponse(json.dumps(hpc_wizard.get_hpc_wizard().get_summary_for_view()), mimetype='application/javascript')
715 elif (name == "hpcUserSite"):
716 return HttpResponse(json.dumps(getDashboardContext(request.user, tableFormat=True)), mimetype='application/javascript')
717 elif (name == "hpcMap"):
718 return HttpResponse(json.dumps(getCDNOperatorData(True)), mimetype='application/javascript')
719 elif (name == "bigquery"):
720 (mimetype, data) = DoPlanetStackAnalytics(request)
721 return HttpResponse(data, mimetype=mimetype)
723 return HttpResponse(json.dumps("Unknown"), mimetype='application/javascript')
725 class DashboardCustomize(View):
726 def post(self, request, *args, **kwargs):
\r
727 dashboards = request.POST.get("dashboards", None)
\r
729 return HttpResponse("no data")
\r
731 dashboards = [x.strip() for x in dashboards.split(",")]
\r
733 dashboards = [DashboardView.objects.get(name=x) for x in dashboards]
\r
735 request.user.dashboardViews.all().delete()
\r
737 for i,dashboard in enumerate(dashboards):
\r
738 udbv = UserDashboardView(user=request.user, dashboardView=dashboard, order=i)
\r
741 return HttpResponse("updated")
\r