Skip to content

Commit

Permalink
feat: RESTful endpoint for device module readings (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
Aiky30 authored Feb 16, 2024
1 parent f323079 commit 9974f5e
Show file tree
Hide file tree
Showing 15 changed files with 401 additions and 72 deletions.
7 changes: 7 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import pytest
from rest_framework.test import APIClient


@pytest.fixture
def client():
return APIClient()
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ build==0.10.0
cfgv==3.4.0
distlib==0.3.8
Django==5.0.1
djangorestframework==3.14.0
exceptiongroup==1.2.0
factory-boy==3.3.0
Faker==22.5.0
Expand All @@ -21,6 +22,7 @@ pyproject_hooks==1.0.0
pytest==7.4.4
pytest-django==4.7.0
python-dateutil==2.8.2
pytz==2023.3.post1
PyYAML==6.0.1
referencing==0.32.1
rpds-py==0.17.1
Expand Down
2 changes: 1 addition & 1 deletion shedpi_hub_dashboard/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ class DeviceModuleAdmin(admin.ModelAdmin):

@admin.register(DeviceModuleReading)
class DeviceModuleReadingAdmin(admin.ModelAdmin):
pass
list_display = ("id", "device_module_id", "created_at")
15 changes: 13 additions & 2 deletions shedpi_hub_dashboard/forms/fields.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import json

from django.db.models import JSONField
from django.forms import JSONField as JSONFormField
from django.forms import widgets


class PrettyJSONWidget(widgets.Textarea):
def format_value(self, value):
# Prettify the json
value = json.dumps(json.loads(value), indent=2, sort_keys=True)
try:
# Prettify the json
value = json.dumps(json.loads(value), indent=2, sort_keys=True)
except json.JSONDecodeError:
return super(PrettyJSONWidget, self).format_value(value)

# Calculate the size of the contents
row_lengths = [len(r) for r in value.split("\n")]
Expand All @@ -23,3 +27,10 @@ def format_value(self, value):

class PrettyJsonFormField(JSONFormField):
widget = PrettyJSONWidget


class PrettyJsonField(JSONField):
def formfield(self, **kwargs):
defaults = {"form_class": PrettyJsonFormField}
defaults.update(kwargs)
return super().formfield(**defaults)
4 changes: 2 additions & 2 deletions shedpi_hub_dashboard/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class Migration(migrations.Migration):
("location", models.CharField(max_length=50)),
(
"schema",
shedpi_hub_dashboard.models.PrettySONField(blank=True, null=True),
shedpi_hub_dashboard.models.PrettyJsonField(blank=True, null=True),
),
(
"device",
Expand All @@ -70,7 +70,7 @@ class Migration(migrations.Migration):
),
(
"data",
shedpi_hub_dashboard.models.PrettySONField(blank=True, null=True),
shedpi_hub_dashboard.models.PrettyJsonField(blank=True, null=True),
),
("created_at", models.DateTimeField(auto_now_add=True)),
(
Expand Down
14 changes: 3 additions & 11 deletions shedpi_hub_dashboard/models.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
import uuid

from django.db import models
from django.db.models import JSONField
from jsonschema import validate

from shedpi_hub_dashboard.forms.fields import PrettyJsonFormField


class PrettySONField(JSONField):
def formfield(self, **kwargs):
defaults = {"form_class": PrettyJsonFormField}
defaults.update(kwargs)
return super().formfield(**defaults)
from shedpi_hub_dashboard.forms.fields import PrettyJsonField


class Device(models.Model):
Expand All @@ -32,7 +24,7 @@ class DeviceModule(models.Model):
)
name = models.CharField(max_length=20)
location = models.CharField(max_length=50)
schema = PrettySONField(null=True, blank=True)
schema = PrettyJsonField(null=True, blank=True)

def __str__(self):
return self.name
Expand All @@ -44,7 +36,7 @@ class DeviceModuleReading(models.Model):
on_delete=models.CASCADE,
help_text="A device whose readings were collected.",
)
data = PrettySONField(null=True, blank=True)
data = PrettyJsonField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)

def validate_data(self) -> None:
Expand Down
16 changes: 16 additions & 0 deletions shedpi_hub_dashboard/serlializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from rest_framework import serializers

from .models import DeviceModule, DeviceModuleReading


class DeviceModuleSerializer(serializers.ModelSerializer):
class Meta:
model = DeviceModule
fields = "__all__"


class DeviceModuleReadingSerializer(serializers.ModelSerializer):
class Meta:
model = DeviceModuleReading
fields = "__all__"
# extra_kwargs = {"device_module": {"required": True}}
10 changes: 0 additions & 10 deletions shedpi_hub_dashboard/static/shedpi_hub_dashboard/dummy_data.json

This file was deleted.

185 changes: 157 additions & 28 deletions shedpi_hub_dashboard/static/shedpi_hub_dashboard/js/index.js
Original file line number Diff line number Diff line change
@@ -1,49 +1,178 @@
const contents = document.getElementsByClassName("contents")[0];
let section = contents

const contents = document.getElementsByClassName("contents");
let section = contents[0]
let deviceModuleEndpoint = ""

const url = section.getAttribute("data-json-feed")
const myRequest = new Request(url);
// Global store for the device modules, with schema
let storeDeviceModules = []
let deviceModuleSchemaMap = {}

/* Drop down selection */
// Create dropdown container
const deviceModuleSelectorContainer = document.createElement("div");
section.append(deviceModuleSelectorContainer);

const urlDeviceModule = "/api/v1/device-module/"
let endpointDeviceModule = new Request(urlDeviceModule);
response = fetch(endpointDeviceModule)
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}

return response.json();
})
.then((response) => {
storeDeviceModules = response
drawDropdown()

// Build schema map

});

let drawDropdown = function () {
let data = storeDeviceModules
let dropdown = document.createElement("select");

// Table Header
let emptySelector = document.createElement("option");
emptySelector.textContent = "Please Select"
dropdown.append(emptySelector)

dropdown.addEventListener('change', function (e) {
optionId = this.selectedOptions[0].id

if (optionId) {
loadTableData(optionId)
}
});

for (let deviceModuleIndex in data) {
const deviceModule = data[deviceModuleIndex]

let optionElement = document.createElement("option");
optionElement.textContent = deviceModule.device + " - " + deviceModule.name
optionElement.id = deviceModule.id

// Build schema map
deviceModuleSchemaMap[deviceModule.id] = deviceModule.schema

dropdown.append(optionElement);
}

// Add the drpdown to the page
deviceModuleSelectorContainer.append(dropdown);
};

/* Table visual */

// Create table container
const tableContainer = document.createElement("div");
section.append(tableContainer);

let loadTableData = function (deviceModuleId) {

// const url = section.getAttribute("data-json-feed")
const url = "http://localhost:8000//api/v1/device-module-readings/"
const endpoint = new URL(url);
endpoint.searchParams.append("device_module", deviceModuleId);

// FIXME: Need data output and need headings from Schema

// const urlDeviceModuleReading =
let endpointDeviceModuleReading = new Request(endpoint);

response = fetch(endpointDeviceModuleReading)
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}

return response.json();
})
.then((response) => {
drawTable(response, deviceModuleId)
});
}


let drawTable = function (dataset, deviceModuleId) {
// First empty the table container
tableContainer.textContent = ""

let drawTable = function (data) {
let table = document.createElement("table");

// Table Header
let headerRow = document.createElement("tr");

for(let heading in data.headings) {
// TODO: Build the header rows from the schema, or build a full list in th
// Could use the schema, what about historic data that may violate it,
// Built as a ist because the pagination would hammer the device modiule
const headingFields = dataset[0];

// TODO: Build the header rows from the schema, or build a full list in the backend and supply in the response
// Could use the schema, what about historic data that may violate it,
// Built as a ist because the pagination would hammer the device modiule

schema = deviceModuleSchemaMap[deviceModuleId]

let dataFields = []
if (schema) {
extra_fields = Object.keys(schema.properties)
dataFields = [...dataFields, ...extra_fields];
dataFields = [...new Set(dataFields)]
}

// FIXME: Need human readable headings, probably needs to come from the BE to be
for (let heading in headingFields) {

if (heading == "data") {

let headerItem = document.createElement("th");
headerItem.textContent = data.headings[heading]
headerRow.append(headerItem);
for (let headingIndex in dataFields) {
const heading = dataFields[headingIndex]
let headerItem = document.createElement("th");
headerItem.textContent = heading
headerRow.append(headerItem);
}
} else {
let headerItem = document.createElement("th");
headerItem.textContent = heading
headerRow.append(headerItem);
}
}

table.append(headerRow);

// Table Contents
for(let row in data.readings) {
for (let rowIndex in dataset) {
const row = dataset[rowIndex]
let contentRow = document.createElement("tr");
for(let reading in data.readings[row]) {
let contentItem = document.createElement("td");
contentItem.textContent = data.readings[row][reading]
contentRow.append(contentItem);
for (let reading in row) {
const fieldValue = row[reading]
if (typeof fieldValue == "object") {
for (let dataFieldIndex in dataFields) {
let contentItem = document.createElement("td");
const dataField = dataFields[dataFieldIndex]

// FIXME: Need to change the null value in the project to be an empty object
let mydict = {}
if (fieldValue != null) {
if (fieldValue.hasOwnProperty(dataField)) {
contentItem.textContent = fieldValue[dataField]
}
}

contentRow.append(contentItem);
}
} else {
let contentItem = document.createElement("td");
contentItem.textContent = row[reading]
contentRow.append(contentItem);
}
}
table.append(contentRow);
}

// Add the table to the page
section.append(table);
tableContainer.append(table);
}


response = fetch(myRequest)
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}

return response.json();
})
.then((response) => {
drawTable(response)
});
36 changes: 18 additions & 18 deletions shedpi_hub_dashboard/templates/shedpi_hub_dashboard/index.html
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@

{% load static %}

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>ShedPi</title>
<link rel="stylesheet" href="{% static 'shedpi_hub_dashboard/css/landing.css' %}">
</head>
<body>
<header>
<h1>Shed Pi data</h1>
</header>
<div class="contents" data-json-feed="{% static 'shedpi_hub_dashboard/dummy_data.json' %}">
</div>
<footer>
</footer>
<script src="{% static 'shedpi_hub_dashboard/js/index.js' %}"></script>
</body>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>ShedPi</title>
<link rel="stylesheet" href="{% static 'shedpi_hub_dashboard/css/landing.css' %}">
</head>
<body>
<header>
<h1>Shed Pi data</h1>
</header>
<div class="contents" data-endpoint-url="">

</div>
<footer>
</footer>
<script src="{% static 'shedpi_hub_dashboard/js/index.js' %}"></script>
</body>
</html>
Loading

0 comments on commit 9974f5e

Please sign in to comment.