Skip to content

Commit

Permalink
admin dashboard (#48)
Browse files Browse the repository at this point in the history
Admin dashboard for easily viewing key information about your project

Currently set up to view all projects at once, because we don't normally have multiple projects running on a server when we do hub deployments. Shouldn't be too difficult to implement if necessary, though.

Fields and metrics displayed are very easily configurable - open to any suggestions
  • Loading branch information
jordanreedie authored and OrenLederman committed Oct 24, 2018
1 parent 97a5094 commit fc948c6
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 5 deletions.
11 changes: 11 additions & 0 deletions config/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
'rest_framework',
'rest_framework.authtoken',
'rest_framework_expiring_authtoken',
'controlcenter',
'import_export',
)
DJANGO_APPS = (
Expand All @@ -47,6 +48,10 @@
# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
INSTALLED_APPS = THIRD_PARTY_APPS + DJANGO_APPS + LOCAL_APPS

CONTROLCENTER_DASHBOARDS = (
('dash', 'openbadge.dashboard.BadgeDashboard'),
)

# MIDDLEWARE CONFIGURATION
# ------------------------------------------------------------------------------
MIDDLEWARE_CLASSES = (
Expand Down Expand Up @@ -191,3 +196,9 @@
DJANGO_SECURE_SSL_REDIRECT=False
# django-allauth
DJANGO_ACCOUNT_ALLOW_REGISTRATION=True

LOW_VOLTAGE=2.7
UNSYNC_CUTOFF_HOURS=24
NUM_UNSYNCS=2
LAST_SEEN_CUTOFF_SHORT_HOURS=2
LAST_SEEN_CUTOFF_LONG_HOURS=6
2 changes: 2 additions & 0 deletions config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
from django.conf import settings
from django.conf.urls.static import static
from settings import common as settings
from controlcenter.views import controlcenter

urlpatterns = [
url(r'^grappelli/', include('grappelli.urls')),
url(r'^admin/', include(admin.site.urls)),
url(r'^dashboard/', include(controlcenter.urls)),

url(r'', include('openbadge.urls',namespace='openbadge')),

Expand Down
4 changes: 2 additions & 2 deletions openbadge-server/openbadge/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class MemberInline(admin.TabularInline, GetLocalTimeMixin):
'email')

readonly_fields = ('key', 'id', 'observed_id', 'last_voltage', 'last_seen', 'last_audio','last_proximity',
'last_contacted','last_unsync')
'last_contacted','last_unsync', 'last_unsync_ts')

def last_seen(self, obj):
return self.get_local_time(obj.last_seen_ts)
Expand Down Expand Up @@ -156,7 +156,7 @@ def time_diff(x):
@register(Member)
class MemberAdmin(ImportExportModelAdmin, GetLocalTimeMixin):
readonly_fields = ('key', 'id', 'observed_id', 'last_seen', 'last_audio', 'last_proximity',
'last_contacted', 'last_unsync',)
'last_contacted', 'last_unsync', 'last_unsync_ts')
list_display = (
'key','project' , 'id', 'name', 'badge', 'observed_id', 'last_voltage',
'last_seen', 'last_seen_ts', 'last_audio', 'last_audio_ts', 'last_audio_ts_fract',
Expand Down
163 changes: 163 additions & 0 deletions openbadge-server/openbadge/dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
from controlcenter import Dashboard, widgets
from .models import Member, Unsync, Hub, Beacon
from django.conf import settings
from django.db.models import Count
from datetime import datetime
import time
import pytz
from pytz import timezone


def hours_to_secs(hrs):
return hrs * 60 * 60

def secs_to_hours(secs):
return (secs / 60) / 60

def secs_to_minutes(secs):
return round((secs / 60), 1)

def cutoff_to_ts(cutoff):
# cutoff is in hours
return time.time() - hours_to_secs(cutoff)

def timestamp_to_date(ts):
return (pytz.utc.localize(datetime.utcfromtimestamp(ts))
.astimezone(timezone(settings.TIME_ZONE))
.strftime('%Y-%m-%d %H:%M:%S %Z'))



class BaseItemList(widgets.ItemList):

def last_seen_date(self, obj):
if (obj.last_seen_ts is not None and obj.last_seen_ts != 0):
return timestamp_to_date(int(obj.last_seen_ts))
else:
return "Not yet seen"

last_seen_date.short_description = "Last Seen"

def last_unsync_date(self, obj):
if (obj.last_unsync_ts is not None and obj.last_unsync_ts != 0):
return timestamp_to_date(int(obj.last_unsync_ts))
else:
return "No Unsyncs Recorded"

last_unsync_date.short_description = "Last Unsync"

class LowVoltageMembers(BaseItemList):

model = Member
list_display = ('id', 'name', 'last_seen_date', 'last_voltage', 'last_unsync_date')
width = widgets.LARGE
sortable = True
limit_to = None
height = 400

def get_queryset(self):
return (self.model.objects
.filter(active=True)
.filter(last_voltage__lt=settings.LOW_VOLTAGE)
.order_by('last_voltage'))


class ManyResetMembers(BaseItemList):
model = Unsync
title = "MEMBERS WITH MULTIPLE RESETS WITHIN {} HOURS".format(settings.UNSYNC_CUTOFF_HOURS)
width = widgets.LARGE
sortable = True

# get all unsyncs since cutoff
def get_queryset(self):
return (self.model.objects
.filter(unsync_ts__gt=cutoff_to_ts(settings.UNSYNC_CUTOFF_HOURS))
.values('member__id', 'member__key', 'member__name', 'member__last_voltage')
.annotate(num_unsyncs=Count('member__id'))
.filter(num_unsyncs__gte=settings.NUM_UNSYNCS)
.order_by('-num_unsyncs'))

list_display = ('member__id', 'member__key', 'member__name', 'num_unsyncs', 'member__last_voltage')


class ThingNotSeen(BaseItemList):
limit_to = None
width = widgets.LARGE
sortable = True

def minutes_since_last_seen(self, obj):
if (obj.last_seen_ts and obj.last_seen_ts != 0):
return secs_to_minutes(time.time() - int(obj.last_seen_ts))
else:
return "Not yet seen"

def cutoff_long(self):
return cutoff_to_ts(settings.LAST_SEEN_CUTOFF_LONG_HOURS)

def cutoff_short(self):
return cutoff_to_ts(settings.LAST_SEEN_CUTOFF_SHORT_HOURS)

minutes_since_last_seen.short_description = "minutes since last seen"

class HubsNotSeen(ThingNotSeen):
model = Hub
title = "HUBS NOT SEEN IN {} HOURS".format(settings.LAST_SEEN_CUTOFF_SHORT_HOURS)
list_display = ('id', 'name', 'last_seen_date', 'minutes_since_last_seen')

def get_queryset(self):
return self.model.objects.filter(last_seen_ts__lt=self.cutoff_short())


class BeaconsNotSeen(ThingNotSeen):
model = Beacon
title = "BEACONS NOT SEEN IN {} HOURS".format(settings.LAST_SEEN_CUTOFF_SHORT_HOURS)
list_display = ('id', 'name', 'last_seen_date', 'last_voltage', 'minutes_since_last_seen')

def get_queryset(self):
return (self.model.objects
.filter(last_seen_ts__lt=self.cutoff_short())
.filter(active=True)
.order_by('last_seen_ts'))


class MembersNotSeenShort(ThingNotSeen):
model = Member
title = "MEMBERS NOT SEEN IN {} HOURS".format(settings.LAST_SEEN_CUTOFF_SHORT_HOURS)
list_display = ('id', 'name', 'last_seen_date', 'minutes_since_last_seen', 'last_voltage', 'last_unsync_date')

def get_queryset(self):
return (self.model.objects
.filter(last_seen_ts__lt=self.cutoff_short())
.filter(active=True)
.order_by('last_seen_ts'))


class MembersNotSeenLong(ThingNotSeen):
model = Member
title = "MEMBERS NOT SEEN IN {} HOURS".format(settings.LAST_SEEN_CUTOFF_LONG_HOURS)
list_display = ('id', 'name', 'last_seen_date', 'minutes_since_last_seen', 'last_voltage', 'last_unsync_date')

def get_queryset(self):
return (self.model.objects
.filter(last_seen_ts__lt=self.cutoff_long())
.filter(active=True)
.order_by('last_seen_ts'))


class MembersAll(ThingNotSeen):
model = Member
title = "ALL MEMBERS"
list_display = ('id', 'name', 'last_seen_date', 'minutes_since_last_seen', 'last_voltage', 'last_unsync_date')

def get_queryset(self):
return self.model.objects.filter(active=True)


class BadgeDashboard(Dashboard):
widgets = (
ManyResetMembers,
LowVoltageMembers,
(MembersNotSeenShort, MembersNotSeenLong, MembersAll),
BeaconsNotSeen,
HubsNotSeen
)
33 changes: 33 additions & 0 deletions openbadge-server/openbadge/migrations/0010_auto_20180816_2310.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations
import openbadge.models


class Migration(migrations.Migration):

dependencies = [
('openbadge', '0009_hub_all_ip_addresses'),
]

operations = [
migrations.CreateModel(
name='Unsync',
fields=[
('id', models.AutoField(serialize=False, primary_key=True)),
('key', models.CharField(db_index=True, unique=True, max_length=10, blank=True)),
('date_created', models.DateTimeField(auto_now_add=True)),
('date_updated', models.DateTimeField(auto_now=True)),
('unsync_ts', models.DecimalField(default=openbadge.models._now_as_epoch, max_digits=20, decimal_places=3)),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='unsync',
name='member',
field=models.ForeignKey(related_name='unsyncs', to='openbadge.Member'),
),
]
17 changes: 15 additions & 2 deletions openbadge-server/openbadge/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,12 +286,13 @@ class Member(BaseModelMinimal):
last_audio_ts_fract = models.DecimalField(max_digits=20, decimal_places=3, default=Decimal(0))
last_proximity_ts = models.DecimalField(max_digits=20, decimal_places=3, default=_now_as_epoch)
last_contacted_ts = models.DecimalField(max_digits=20, decimal_places=3, default=Decimal(0))
last_unsync_ts = models.DecimalField(max_digits=20, decimal_places=3, default=Decimal(0))
last_voltage = models.DecimalField(max_digits=5, decimal_places=3, default=Decimal(0))
last_seen_ts = models.DecimalField(max_digits=20, decimal_places=3, default=Decimal(0))
last_unsync_ts = models.DecimalField(max_digits=20, decimal_places=3, default=Decimal(0))

project = models.ForeignKey(Project, related_name="members")


def get_advertisement_project_id(self):
return self.project.advertisement_project_id

Expand All @@ -316,7 +317,7 @@ def datetime_to_epoch(cls, d):
"""
epoch_seconds = (d - datetime.datetime(1970, 1, 1)).total_seconds()
long_epoch_seconds = long(floor(epoch_seconds))
ts_fract = d.microsecond / 1000;
ts_fract = d.microsecond / 1000
return (long_epoch_seconds, ts_fract)

@classmethod
Expand Down Expand Up @@ -376,6 +377,18 @@ def __unicode__(self):
return unicode(self.name)


class Unsync(BaseModel):
"""
Represents a single time in which a badge became unsynced / restarted
Many-to-one relationship w/ a Member
"""


member = models.ForeignKey(Member, related_name='unsyncs')
unsync_ts = models.DecimalField(max_digits=20, decimal_places=3, default=_now_as_epoch, db_index=True)


class Meeting(BaseModel):
"""
Represents a Meeting, which belongs to a Project, and has a log_file and a last_update_time, among other standard
Expand Down
13 changes: 12 additions & 1 deletion openbadge-server/openbadge/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from rest_framework import serializers
import time

from .models import Member, Project, Hub, Beacon
from .models import Member, Project, Hub, Beacon, Unsync

import logging

Expand All @@ -19,6 +19,8 @@ class Meta:
read_only_fields = ('id','project','advertisement_project_id', 'key')

def update(self, instance, validated_data):
# instance is the current Member stored in the DB,
# validated_data is the incoming data

# if we have an older audio_ts, update it
if validated_data.get('last_audio_ts') > instance.last_audio_ts:
Expand All @@ -41,7 +43,16 @@ def update(self, instance, validated_data):
if validated_data.get('last_contacted_ts') > instance.last_contacted_ts:
instance.last_contacted_ts = validated_data.get('last_contacted_ts', instance.last_contacted_ts)

# need to record all unsyncs
# if this is not performant we can maintain both the last unsync
# in the member object as well as a separate table of unsync history
if validated_data.get('last_unsync_ts') > instance.last_unsync_ts:
# create an unsync obj to keep a history of all unsyncs
Unsync.objects.create(
member=instance,
unsync_ts=validated_data.get('last_unsync_ts', instance.last_unsync_ts))

# save the latest ts in the member obj
instance.last_unsync_ts = validated_data.get('last_unsync_ts', instance.last_unsync_ts)

instance.observed_id = validated_data.get('observed_id', instance.observed_id)
Expand Down
2 changes: 2 additions & 0 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ djangorestframework-expiring-authtoken==0.1.1
pytz==2015.7
python-dateutil==2.5.3
jsonfield==1.0.3
django-controlcenter===0.2.6


# Configuration
django-environ==0.4.1
Expand Down

0 comments on commit fc948c6

Please sign in to comment.