Skip to content

Commit

Permalink
feat: Variable JSON device readings (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
Aiky30 authored Jan 24, 2024
1 parent 56d5a75 commit f323079
Show file tree
Hide file tree
Showing 12 changed files with 269 additions and 20 deletions.
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ classifiers = [
"Homepage" = "https://github.com/Aiky30/shed-pi"
"Bug Tracker" = "https://github.com/Aiky30/shed-pi/issues"

[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "shedpi_hub_example_project.settings"

[tool.black]
exclude = '''
/(
Expand Down
11 changes: 11 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
asgiref==3.7.2
attrs==23.2.0
build==0.10.0
cfgv==3.4.0
distlib==0.3.8
Django==5.0.1
exceptiongroup==1.2.0
factory-boy==3.3.0
Faker==22.5.0
filelock==3.13.1
identify==2.5.33
iniconfig==2.0.0
jsonschema==4.21.1
jsonschema-specifications==2023.12.1
nodeenv==1.8.0
packaging==23.1
platformdirs==4.1.0
pluggy==1.4.0
pre-commit==3.6.0
pyproject_hooks==1.0.0
pytest==7.4.4
pytest-django==4.7.0
python-dateutil==2.8.2
PyYAML==6.0.1
referencing==0.32.1
rpds-py==0.17.1
setuptools-scm==7.1.0
six==1.16.0
sqlparse==0.4.4
tomli==2.0.1
typing_extensions==4.7.1
Expand Down
11 changes: 8 additions & 3 deletions shedpi_hub_dashboard/admin.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
from django.contrib import admin

from .models import Device, DeviceReading
from .models import Device, DeviceModule, DeviceModuleReading


@admin.register(Device)
class DeviceAdmin(admin.ModelAdmin):
list_display = ("id", "name")


@admin.register(DeviceReading)
class DeviceReadingAdmin(admin.ModelAdmin):
@admin.register(DeviceModule)
class DeviceModuleAdmin(admin.ModelAdmin):
pass


@admin.register(DeviceModuleReading)
class DeviceModuleReadingAdmin(admin.ModelAdmin):
pass
Empty file.
25 changes: 25 additions & 0 deletions shedpi_hub_dashboard/forms/fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import json

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)

# Calculate the size of the contents
row_lengths = [len(r) for r in value.split("\n")]
content_width = max(len(row_lengths) + 2, 20)
content_height = max(max(row_lengths) + 2, 45)

# Adjust the size of TextArea to fit to content
self.attrs["rows"] = min(content_width, 60)
self.attrs["cols"] = min(content_height, 155)

return value


class PrettyJsonFormField(JSONFormField):
widget = PrettyJSONWidget
46 changes: 38 additions & 8 deletions shedpi_hub_dashboard/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Generated by Django 5.0.1 on 2024-01-17 22:11
# Generated by Django 5.0.1 on 2024-01-23 19:02

import django.db.models.deletion
import shedpi_hub_dashboard.models
import uuid
from django.db import migrations, models

Expand Down Expand Up @@ -28,7 +29,35 @@ class Migration(migrations.Migration):
],
),
migrations.CreateModel(
name="DeviceReading",
name="DeviceModule",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.CharField(max_length=20)),
("location", models.CharField(max_length=50)),
(
"schema",
shedpi_hub_dashboard.models.PrettySONField(blank=True, null=True),
),
(
"device",
models.ForeignKey(
help_text="A device which manages the module.",
on_delete=django.db.models.deletion.CASCADE,
to="shedpi_hub_dashboard.device",
),
),
],
),
migrations.CreateModel(
name="DeviceModuleReading",
fields=[
(
"id",
Expand All @@ -39,16 +68,17 @@ class Migration(migrations.Migration):
verbose_name="ID",
),
),
("device_temp", models.CharField(max_length=8)),
("probe_temp", models.CharField(max_length=8)),
("measurement_type", models.CharField(max_length=10)),
("datetime", models.DateTimeField()),
(
"device",
"data",
shedpi_hub_dashboard.models.PrettySONField(blank=True, null=True),
),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"device_module",
models.ForeignKey(
help_text="A device whose readings were collected.",
on_delete=django.db.models.deletion.CASCADE,
to="shedpi_hub_dashboard.device",
to="shedpi_hub_dashboard.devicemodule",
),
),
],
Expand Down
57 changes: 51 additions & 6 deletions shedpi_hub_dashboard/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
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)


class Device(models.Model):
Expand All @@ -12,14 +23,48 @@ def __str__(self):
return self.name


class DeviceReading(models.Model):
class DeviceModule(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
device = models.ForeignKey(
Device,
on_delete=models.CASCADE,
help_text="A device which manages the module.",
)
name = models.CharField(max_length=20)
location = models.CharField(max_length=50)
schema = PrettySONField(null=True, blank=True)

def __str__(self):
return self.name


class DeviceModuleReading(models.Model):
device_module = models.ForeignKey(
DeviceModule,
on_delete=models.CASCADE,
help_text="A device whose readings were collected.",
)
# TODO: Create a Json configurable engine for storage and retrieval fields¬
device_temp = models.CharField(max_length=8)
probe_temp = models.CharField(max_length=8)
measurement_type = models.CharField(max_length=10)
datetime = models.DateTimeField()
data = PrettySONField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)

def validate_data(self) -> None:
"""
Validates the data against the schema defined in the DeviceModule,
the purpose is to ensure that the data stored matches the data structure expected
"""
schema = self.device_module.schema

# Only validate if a schema exists
if schema:
validate(
instance=self.data,
schema=schema,
)

def save(self, *args, **kwargs) -> None:
"""
On save validates
"""
self.validate_data()

super().save(*args, **kwargs)
1 change: 0 additions & 1 deletion shedpi_hub_dashboard/tests.py

This file was deleted.

98 changes: 98 additions & 0 deletions shedpi_hub_dashboard/tests/test_json_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import pytest
from jsonschema.exceptions import SchemaError, ValidationError

from shedpi_hub_dashboard.models import DeviceModuleReading
from shedpi_hub_dashboard.tests.utils.factories import DeviceModuleFactory


@pytest.mark.django_db
def test_schema_validation_happy_path():
"""
The data validation should succeed when the schema is valid
"""
schema = {
"$id": "https://example.com/person.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Person",
"type": "object",
"properties": {
"firstName": {"type": "string", "description": "The person's first name."},
"lastName": {"type": "string", "description": "The person's last name."},
"age": {
"description": "Age in years which must be equal to or greater than zero.",
"type": "integer",
"minimum": 0,
},
},
}
data = {"firstName": "John", "lastName": "Doe", "age": 21}
device_module = DeviceModuleFactory(schema=schema)

reading = DeviceModuleReading(device_module=device_module, data=data)

# By default no validation error is raised
reading.save()


@pytest.mark.django_db
def test_json_schema_invalid_data():
schema = {
"$id": "https://example.com/person.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Person",
"type": "object",
"properties": {
"age": {
"description": "Age in years which must be equal to or greater than zero.",
"type": "integer",
"minimum": 0,
}
},
}
data = {"age": 21}
updated_data = {"age": "Some text"}
device_module = DeviceModuleFactory(schema=schema)
reading = DeviceModuleReading(device_module=device_module, data=data)

# No error occurs with valid data
reading.save()

# With invalid data it now throws a validation error
with pytest.raises(ValidationError):
reading.data = updated_data
reading.save()


@pytest.mark.django_db
def test_json_schema_update_with_invalid_data():
schema = {
"$id": "https://example.com/person.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Person",
"type": "object",
"properties": {
"age": {
"description": "Age in years which must be equal to or greater than zero.",
"type": "integer",
"minimum": 0,
}
},
}
data = {"age": "Some text"}
device_module = DeviceModuleFactory(schema=schema)
reading = DeviceModuleReading(device_module=device_module, data=data)

with pytest.raises(ValidationError):
reading.save()


@pytest.mark.django_db
def test_json_schema_invalid_schema():
schema = {"type": 1234}
data = {"firstName": "John", "lastName": "Doe", "age": 21}

device_module = DeviceModuleFactory(schema=schema)
reading = DeviceModuleReading(device_module=device_module, data=data)

with pytest.raises(SchemaError):
reading.save()
2 changes: 0 additions & 2 deletions shedpi_hub_dashboard/tests/test_runs.py

This file was deleted.

Empty file.
35 changes: 35 additions & 0 deletions shedpi_hub_dashboard/tests/utils/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from typing import ClassVar
from uuid import uuid4

import factory
from factory.django import DjangoModelFactory

from shedpi_hub_dashboard.models import Device, DeviceModule, DeviceModuleReading


class DeviceFactory(DjangoModelFactory):
class Meta:
model = Device

id = factory.LazyFunction(lambda: str(uuid4()))
name = factory.Faker("word")
location = factory.Faker("word")


class DeviceModuleFactory(DjangoModelFactory):
class Meta:
model = DeviceModule

id = factory.LazyFunction(lambda: str(uuid4()))
device = factory.SubFactory(DeviceFactory)
name = factory.Faker("word")
location = factory.Faker("word")
schema: ClassVar[dict] = {}


class DeviceModuleReadingFactory(DjangoModelFactory):
class Meta:
model = DeviceModuleReading

device_module = factory.SubFactory(DeviceModuleFactory)
data: ClassVar[dict] = {}

0 comments on commit f323079

Please sign in to comment.