From 0386286858fc30f6163ad7300c34f964cf11827d Mon Sep 17 00:00:00 2001 From: Matthew Macdonald-Wallace Date: Thu, 12 Oct 2023 11:23:28 +0100 Subject: [PATCH 1/9] Add models and valid SpaceAPI schema output for sensors --- memberportal/api_spacedirectory/admin.py | 17 +++ .../migrations/0001_initial.py | 99 +++++++++++++++++ .../migrations/0002_auto_20231012_1914.py | 25 +++++ .../api_spacedirectory/migrations/__init__.py | 0 memberportal/api_spacedirectory/models.py | 57 ++++++++++ memberportal/api_spacedirectory/views.py | 103 ++++++++++++------ 6 files changed, 267 insertions(+), 34 deletions(-) create mode 100644 memberportal/api_spacedirectory/admin.py create mode 100644 memberportal/api_spacedirectory/migrations/0001_initial.py create mode 100644 memberportal/api_spacedirectory/migrations/0002_auto_20231012_1914.py create mode 100644 memberportal/api_spacedirectory/migrations/__init__.py create mode 100644 memberportal/api_spacedirectory/models.py diff --git a/memberportal/api_spacedirectory/admin.py b/memberportal/api_spacedirectory/admin.py new file mode 100644 index 00000000..c6745475 --- /dev/null +++ b/memberportal/api_spacedirectory/admin.py @@ -0,0 +1,17 @@ +from django.contrib import admin +from .models import SpaceAPI, SpaceAPISensor, SpaceAPISensorProperties + + +@admin.register(SpaceAPI) +class SpaceAPIAdmin(admin.ModelAdmin): + pass + + +@admin.register(SpaceAPISensor) +class SpaceAPISensorAdmin(admin.ModelAdmin): + pass + + +@admin.register(SpaceAPISensorProperties) +class SpaceAPISensorPropertiesAdmin(admin.ModelAdmin): + pass diff --git a/memberportal/api_spacedirectory/migrations/0001_initial.py b/memberportal/api_spacedirectory/migrations/0001_initial.py new file mode 100644 index 00000000..750ce0c8 --- /dev/null +++ b/memberportal/api_spacedirectory/migrations/0001_initial.py @@ -0,0 +1,99 @@ +# Generated by Django 3.2.21 on 2023-10-12 09:02 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="SpaceAPI", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("space_is_open", models.BooleanField(default=False)), + ( + "space_message", + models.CharField(blank=True, max_length=255, null=True), + ), + ("status_last_change", models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name="SpaceAPISensor", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "sensor_type", + models.CharField( + choices=[ + ("temperature", "Temperature"), + ("barometer", "Barometer"), + ("radiation", "Radiation"), + ("humidity", "Humidity"), + ("beverage_supply", "Beverage Supply"), + ("power_consumption", "Power Consumption"), + ("wind", "Wind Data"), + ("network_connections", "Network Connections"), + ("account_balance", "Account Balance"), + ("network_traffic", "Network Traffic"), + ], + max_length=100, + ), + ), + ("name", models.CharField(max_length=255)), + ("value", models.DecimalField(decimal_places=3, max_digits=10)), + ("unit", models.CharField(max_length=50)), + ("location", models.CharField(blank=True, max_length=255, null=True)), + ( + "description", + models.CharField(blank=True, max_length=255, null=True), + ), + ], + ), + migrations.CreateModel( + name="SpaceAPISensorProperties", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), + ("value", models.DecimalField(decimal_places=3, max_digits=10)), + ("unit", models.CharField(max_length=100)), + ( + "sensor_id", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="properties", + to="api_spacedirectory.spaceapisensor", + ), + ), + ], + ), + ] diff --git a/memberportal/api_spacedirectory/migrations/0002_auto_20231012_1914.py b/memberportal/api_spacedirectory/migrations/0002_auto_20231012_1914.py new file mode 100644 index 00000000..212f72a5 --- /dev/null +++ b/memberportal/api_spacedirectory/migrations/0002_auto_20231012_1914.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.21 on 2023-10-12 09:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("api_spacedirectory", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="spaceapisensor", + name="unit", + field=models.CharField(blank=True, max_length=50, null=True), + ), + migrations.AlterField( + model_name="spaceapisensor", + name="value", + field=models.DecimalField( + blank=True, decimal_places=3, max_digits=10, null=True + ), + ), + ] diff --git a/memberportal/api_spacedirectory/migrations/__init__.py b/memberportal/api_spacedirectory/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/memberportal/api_spacedirectory/models.py b/memberportal/api_spacedirectory/models.py new file mode 100644 index 00000000..75b39353 --- /dev/null +++ b/memberportal/api_spacedirectory/models.py @@ -0,0 +1,57 @@ +from django.db import models +from django.conf import settings + + +class SpaceAPI(models.Model): + # The Hackspace is always closed by default to prevent people from showing up by accident. + space_is_open = models.BooleanField(default=False) + space_message = models.CharField(max_length=255, blank=True, null=True) + status_last_change = models.DateTimeField(auto_now=True) + + def __str__(self): + open_text = "CLOSED" + if self.space_is_open is True: + open_text = "OPEN" + + return f"Currently {open_text} - last updated: {self.status_last_change}" + + +# Because Sensors are tied to a space, and we only have one space in the system for now +# We have the luxury of not requiring foreign keys here! +class SpaceAPISensor(models.Model): + SENSOR_TYPE_CHOICES = [ + ("temperature", "Temperature"), + ("barometer", "Barometer"), + ("radiation", "Radiation"), + ("humidity", "Humidity"), + ("beverage_supply", "Beverage Supply"), + ("power_consumption", "Power Consumption"), + ("wind", "Wind Data"), + ("network_connections", "Network Connections"), + ("account_balance", "Account Balance"), + ("network_traffic", "Network Traffic"), + ] + sensor_type = models.CharField(max_length=100, choices=SENSOR_TYPE_CHOICES) + name = models.CharField( + max_length=255 + ) # The sensor name ("main door", "external", etc) + value = models.DecimalField(max_digits=10, decimal_places=3, blank=True, null=True) + unit = models.CharField(max_length=50, blank=True, null=True) + location = models.CharField(max_length=255, blank=True, null=True) + description = models.CharField(max_length=255, blank=True, null=True) + + def __str__(self): + return f"{self.name} - {self.location} ({self.sensor_type})" + + +# Some sensors have properties, let's track those in a separate model +class SpaceAPISensorProperties(models.Model): + sensor_id = models.ForeignKey( + SpaceAPISensor, related_name="properties", on_delete=models.CASCADE + ) + name = models.CharField(max_length=100) + value = models.DecimalField(max_digits=10, decimal_places=3) + unit = models.CharField(max_length=100) + + def __str__(self): + return f"{self.sensor_id.name}/{self.name}" diff --git a/memberportal/api_spacedirectory/views.py b/memberportal/api_spacedirectory/views.py index 6776084a..6c7b21df 100644 --- a/memberportal/api_spacedirectory/views.py +++ b/memberportal/api_spacedirectory/views.py @@ -2,6 +2,7 @@ from rest_framework import status, permissions from rest_framework.views import APIView from constance import config +from .models import SpaceAPI, SpaceAPISensor, SpaceAPISensorProperties import json @@ -18,38 +19,72 @@ def get(self, request): ) else: - return Response( - { - "state": { - "open": config.SPACE_DIRECTORY_OPEN, - "message": config.SPACE_DIRECTORY_MESSAGE, - "icon": { - "open": config.SPACE_DIRECTORY_ICON_OPEN, - "closed": config.SPACE_DIRECTORY_ICON_CLOSED, - }, - }, - "api": "0.13", - "location": { - "address": config.SPACE_DIRECTORY_LOCATION_ADDRESS, - "lat": config.SPACE_DIRECTORY_LOCATION_LAT, - "lon": config.SPACE_DIRECTORY_LOCATION_LON, - }, - "space": config.SITE_OWNER, - "logo": config.SITE_LOGO, - "url": config.MAIN_SITE_URL, - "spacefed": { - "spacenet": config.SPACE_DIRECTORY_FED_SPACENET, - "spacesaml": config.SPACE_DIRECTORY_FED_SPACESAML, - "spacephone": config.SPACE_DIRECTORY_FED_SPACEPHONE, - }, - "cam": json.loads(config.SPACE_DIRECTORY_CAMS), - "contact": { - "email": config.SPACE_DIRECTORY_CONTACT_EMAIL, - "twitter": config.SPACE_DIRECTORY_CONTACT_TWITTER, - "phone": config.SPACE_DIRECTORY_CONTACT_PHONE, - "facebook": config.SPACE_DIRECTORY_CONTACT_FACEBOOK, - }, - "projects": json.loads(config.SPACE_DIRECTORY_PROJECTS), - "issue_report_channels": ["email"], + spaceapi = { + "space": config.SITE_OWNER, + "logo": config.SITE_LOGO, + "url": config.MAIN_SITE_URL, + "contact": { + "email": config.SPACE_DIRECTORY_CONTACT_EMAIL, + "twitter": config.SPACE_DIRECTORY_CONTACT_TWITTER, + "phone": config.SPACE_DIRECTORY_CONTACT_PHONE, + "facebook": config.SPACE_DIRECTORY_CONTACT_FACEBOOK, + }, + "spacefed": { + "spacenet": config.SPACE_DIRECTORY_FED_SPACENET, + "spacesaml": config.SPACE_DIRECTORY_FED_SPACESAML, + "spacephone": config.SPACE_DIRECTORY_FED_SPACEPHONE, + }, + "projects": json.loads(config.SPACE_DIRECTORY_PROJECTS), + "issue_report_channels": ["email"], + } + + spaceapi_data = SpaceAPI.objects.get() + + spaceapi_sensors = SpaceAPISensor.objects.all() + + sensor_data = {} + + for sensor in spaceapi_sensors: + sensor_details = {} + if sensor.sensor_type not in sensor_data: + sensor_data[sensor.sensor_type] = [] + sensor_details = { + "name": sensor.name, + "description": sensor.description, + "location": sensor.location, + "properties": {}, } - ) + if len(sensor.properties.all()) > 0: + for prop in sensor.properties.all(): + properties = { + prop.name: {"value": prop.value, "unit": prop.unit} + } + sensor_details["properties"].update(properties) + else: + sensor_details.update({"value": sensor.value, "unit": sensor.unit}) + + sensor_data[sensor.sensor_type].append(sensor_details) + + if not config.SPACE_DIRECTORY_CAMS: + spaceapi["cameras"] = config.SPACE_DIRECTORY_CAMS + + spaceapi["state"] = { + "open": spaceapi_data.space_is_open, + "message": spaceapi_data.space_message, + "lastchange": spaceapi_data.status_last_change.timestamp(), + } + spaceapi["icon"] = { + "open": config.SPACE_DIRECTORY_ICON_OPEN, + "closed": config.SPACE_DIRECTORY_ICON_CLOSED, + } + spaceapi["api_compatibility"] = ["0.14"] + + spaceapi["sensors"] = sensor_data + + spaceapi["location"] = { + "address": config.SPACE_DIRECTORY_LOCATION_ADDRESS, + "lat": config.SPACE_DIRECTORY_LOCATION_LAT, + "lon": config.SPACE_DIRECTORY_LOCATION_LON, + } + + return Response(spaceapi) From 93026e057fced0926f548bc2da3a31295ed0cc1b Mon Sep 17 00:00:00 2001 From: Matthew Macdonald-Wallace Date: Thu, 12 Oct 2023 11:28:16 +0100 Subject: [PATCH 2/9] Black on localhost does not appear to run the same rules as black in CI --- memberportal/api_spacedirectory/migrations/0001_initial.py | 1 - .../api_spacedirectory/migrations/0002_auto_20231012_1914.py | 1 - 2 files changed, 2 deletions(-) diff --git a/memberportal/api_spacedirectory/migrations/0001_initial.py b/memberportal/api_spacedirectory/migrations/0001_initial.py index 750ce0c8..3c32d48a 100644 --- a/memberportal/api_spacedirectory/migrations/0001_initial.py +++ b/memberportal/api_spacedirectory/migrations/0001_initial.py @@ -5,7 +5,6 @@ class Migration(migrations.Migration): - initial = True dependencies = [] diff --git a/memberportal/api_spacedirectory/migrations/0002_auto_20231012_1914.py b/memberportal/api_spacedirectory/migrations/0002_auto_20231012_1914.py index 212f72a5..3472feca 100644 --- a/memberportal/api_spacedirectory/migrations/0002_auto_20231012_1914.py +++ b/memberportal/api_spacedirectory/migrations/0002_auto_20231012_1914.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("api_spacedirectory", "0001_initial"), ] From ea9778e4b67ee988c6955b40a8842992e43225b6 Mon Sep 17 00:00:00 2001 From: Matthew Macdonald-Wallace Date: Fri, 13 Oct 2023 10:27:39 +0100 Subject: [PATCH 3/9] Add new authenticated endpoint to post information about the space --- memberportal/api_spacedirectory/urls.py | 5 ++ memberportal/api_spacedirectory/views.py | 90 ++++++++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/memberportal/api_spacedirectory/urls.py b/memberportal/api_spacedirectory/urls.py index ef50b158..b8229808 100644 --- a/memberportal/api_spacedirectory/urls.py +++ b/memberportal/api_spacedirectory/urls.py @@ -7,4 +7,9 @@ views.SpaceDirectoryStatus.as_view(), name="spacedirectory_status", ), + path( + "api/spacedirectory/update", + views.SpaceDirectoryUpdate.as_view(), + name="spacedirectory_update", + ), ] diff --git a/memberportal/api_spacedirectory/views.py b/memberportal/api_spacedirectory/views.py index 6c7b21df..768c6789 100644 --- a/memberportal/api_spacedirectory/views.py +++ b/memberportal/api_spacedirectory/views.py @@ -88,3 +88,93 @@ def get(self, request): } return Response(spaceapi) + + +class SpaceDirectoryUpdate(APIView): + + permissions_classes = permissions.IsAuthenticated + + def post(self, request): + # Get the current state of the space + current_status = SpaceAPI.objects.get() + + ## Do we need to update the open/closed status + if "is_open" in request.data: + current_status.space_is_open = request.data["is_open"] + + ## Do we need to update the message? + if "message" in request.data: + current_status.space_message = request.data["message"] + + ## Do we have any sensor data? + if "sensors" in request.data: + for sensor in request.data["sensors"]: + ### See if we have an existing sensor, if we do, update it, + ### if we don't, create it. + try: + current_sensor = ( + SpaceAPISensor.objects.all().filter(name=sensor["name"]).first() + ) + print(f"Found sensor: {current_sensor}") + if "type" in sensor: + current_sensor.sensor_type = sensor["type"] + if "value" in sensor: + current_sensor.value = sensor["value"] + if "unit" in sensor: + current_sensor.unit = sensor["unit"] + if "location" in sensor: + current_sensor.location = sensor["location"] + if "description" in sensor: + current_sensor.description = sensor["description"] + + if "properties" in sensor: + for prop in sensor["properties"]: + #### Does the property exist? If so, update, + #### if not, create + try: + current_prop = SpaceAPISensorProperties.objects.filter( + name=prop["name"], sensor_id=current_sensor + ).get() + print(f"Found property: {current_prop}") + if "name" in prop: + current_prop.name = prop["name"] + if "value" in prop: + current_prop.value = prop["value"] + if "unit" in prop: + current_prop.unit = prop["unit"] + current_prop.save() + except: + new_prop = SpaceAPISensorProperties() + new_prop.name = prop["name"] + new_prop.value = prop["value"] + new_prop.unit = prop["unit"] + new_prop.sensor_id = current_sensor + new_prop.save() + + current_sensor.save() + except Exception as e: + print(e) + new_sensor = SpaceAPISensor() + new_sensor.sensor_type = sensor["type"] + new_sensor.name = sensor["name"] + if "value" in sensor: + new_sensor.value = sensor["value"] + if "unit" in sensor: + new_sensor.unit = sensor["unit"] + new_sensor.location = sensor["location"] + new_sensor.description = sensor["description"] + new_sensor.save() + + if "properties" in sensor: + ## This is a new sensor, we will always want to create the properties + for prop in sensor["properties"]: + new_prop = SpaceAPISensorProperties() + new_prop.name = prop["name"] + new_prop.value = prop["value"] + new_prop.unit = prop["unit"] + new_prop.sensor_id = new_sensor + new_prop.save() + + current_status.save() + + return Response() From 31d68f0c57857f86e827b87c009a508bc65b0512 Mon Sep 17 00:00:00 2001 From: Matthew Macdonald-Wallace Date: Fri, 13 Oct 2023 11:07:41 +0100 Subject: [PATCH 4/9] Update documentation --- docs/POST_INSTALL_STEPS.md | 2 +- docs/SPACEDIRECTORY.md | 195 +++++++++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 docs/SPACEDIRECTORY.md diff --git a/docs/POST_INSTALL_STEPS.md b/docs/POST_INSTALL_STEPS.md index 6f9fe61e..6df5e9c0 100644 --- a/docs/POST_INSTALL_STEPS.md +++ b/docs/POST_INSTALL_STEPS.md @@ -158,7 +158,7 @@ You cannot currently enable specific events, you either get "all or nothing". * "TRELLO_ID_LIST" - [Deprecated] ### Space Directory - * "ENABLE_SPACE_DIRECTORY" - enable a space directory compliant API. The various configuration options in this section should be self explannatory. + * "ENABLE_SPACE_DIRECTORY" - enable a [space directory compliant API](https://spaceapi.io). The various configuration options in this section should be self explanatory, however there is also an [API Endpoint](/docs/SPACEDIRECTORY) to update certain fields. ### Theme Swipe Integration * "THEME_SWIPE_URL" - a URL to hit on each door/interlock swipe that can trigger a theme song played over your intercom system, or something else. diff --git a/docs/SPACEDIRECTORY.md b/docs/SPACEDIRECTORY.md new file mode 100644 index 00000000..95b8e314 --- /dev/null +++ b/docs/SPACEDIRECTORY.md @@ -0,0 +1,195 @@ +# Space Directory + +The Space Directory functionality is provided via the [SpaceAPI](https://spaceapi.io) format. + +## Reading the data + +Any system can read the current information about the space by calling `https:///api/spacedirectory/`. + +By default, this returns the following data: + +| Name | Description | Configuration Location | +|------|-------------|------------------------| +| space| The name of the space | Constance Config (Django Admin pages) | +| logo | The uri of the space logo | Constance Config (Django Admin Pages)| +| url | The URL of the membermatters installation | Django Base URL | +| contact | Contact information, including email and various social media handles | Constance Config (Django Admin Pages)| +| spacefed | Details of whether the Federated Hackspace Authentication service is running | Constance Config (Django Admin Pages)| +| projects | A list of projects that the space is involved in | Constance Config (Django Admin Pages)| +| issue_report_channels | What's the best way to report an issue? | Constance Config (Django Admin Pages)| +| state | Is the state open or closed, and what message should we display? | Dynamic (Django Admin or API Endpoint) | +| icon | An icon for each of "open" and "closed" | Constance Config (Django Admin Pages)| +| api_compatibility | The version of the [SpaceAPI](https://spaceapi.io) that we are compatible with (can be multiple values) | Hard-coded based on Member Matters release version | +| sensors | A dictionary of sensor data (and associated properties where relevant) as described in the [SpaceAPI JSON Schema documentation](https://spaceapi.io/docs/#schema-key-sensors) | Dynamic (Django Admin or API Endpoint) | +| location | The physical location of the space including latitude and longitude | Constance Config (Django Admin Pages)| + +The data is returned as a JSON document. + +For tools that can help you interact with the data to display your current state on your homepage etc, visit the [SpaceAPI Tools](https://spaceapi.io/how-to-use/) page and have a play! + +## Updating the data + +For anything in table above that is marked as "dynamic", you can update that information via the API. + +Simply `POST` a JSON document to `https:///api/spacedirectory/update` as an authenticated user, and the relevant fields will be updated. + +### Updating the status and the message + +Let's say you're opening the space and you want to ensure that everyone knows it's an "Open Night" (i.e. general public are welcome, not just members). + +You can do this with the following command: + +```bash +curl -X POST -d 'username=&password=' \ + 'https://' \ + -d '{"is_open": true, "message": "Open Night TONIGHT! All Welcome between 1800hrs and 2300hrs"}' \ + https:///api/spacedirectory/update +``` + +### Adding Sensor Data + +[Sensors](https://spaceapi.io/docs/#schema-key-sensors) are a really cool part of the SpaceAPI schema, as it allows you to publish all kinds of things from how many drinks are still in teh fridge through to environment readings such as temperature, windspeed, and humidity. + +By default, MemberMatters exposes the total number of active members and how many have "signed in" to the space, however you can add your own sensors as long as they conform to the appropriate type. + +To update a sensor or property value, you just need to POST the appropriate JSON to `https:///api/spacedirectory/update` as laid out below. + +**NOTE**: If a sensor or property is missing from the database then it will be created. If it exists but the value is not included or changed, it will not be updated. + +#### Sensors *without* properties + +Many of the sensors do not have any extra properties. Updating these is simple, as you just need to send a JSON array of dicts with the correct fields filled out: + +```json +{ + "sensors": [ + { + "type": "temperature", + "name": "test_sensor", + "location": "default", + "description": "This is a sensor", + "unit": "°C", + "value": 21.0 + } + ] +} +``` + +Want to update more than one sensor at a time? No worries, just add to the array: + +```json +{ + "sensors": [ + { + "type": "temperature", + "name": "test_sensor", + "location": "default", + "description": "This is a sensor", + "unit": "°C", + "value": 21.0 + }, + { + "type": "humidity", + "name": "test_sensor", + "location": "default", + "description": "This is a sensor", + "unit": "%H", + "value": 45.0 + } + ] +} +``` + +#### Sensors *with* properties + +For those sensors that do have properties, only a few extra fields are required: + +```json +{ + "sensors": [ + { + "type": "wind", + "name": "wind_sensor_01", + "location": "The Roof", + "description": "A weather station", + "properties": [ + { + "name": "gust", + "unit": "m/s", + "speed": 5.0 + } + ] + } + ] +} +``` + +As with the sensors, additional properties can be created by adding more dictionaries to the properties array: + + +```json +{ + "sensors": [ + { + "type": "wind", + "name": "wind_sensor_01", + "location": "The Roof", + "description": "A weather station", + "properties": [ + { + "name": "speed", + "unit": "m/s", + "speed": 5.0 + }, + { + "name": "gust", + "unit": "m/s", + "speed": 9.0 + } + ] + } + ] +} +``` + +### A full example + +The following JSON updates the space status to "Open", sets a message advising that there is a workshop running that evening, and sets the values for various sensors: + +```json + +{ "is_open": true, + "message": "Soldering workshop tonight - 8pm to 10pm" + "sensors": [ + { + "type": "temperature", + "name": "test_sensor", + "location": "default", + "description": "This is a sensor", + "unit": "°C", + "value": 21.0 + }, + { + "type": "wind", + "name": "wind_sensor_01", + "location": "The Roof", + "description": "A weather station", + "properties": [ + { + "name": "speed", + "unit": "m/s", + "speed": 5.0 + }, + { + "name": "gust", + "unit": "m/s", + "speed": 9.0 + } + ] + } + ] +} +``` From 861bdcf310755b18d9c18d15c8fbeb9ef5c894a3 Mon Sep 17 00:00:00 2001 From: Matthew Macdonald-Wallace Date: Fri, 13 Oct 2023 11:15:40 +0100 Subject: [PATCH 5/9] Formatting for black --- memberportal/api_spacedirectory/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/memberportal/api_spacedirectory/views.py b/memberportal/api_spacedirectory/views.py index 768c6789..721f5d19 100644 --- a/memberportal/api_spacedirectory/views.py +++ b/memberportal/api_spacedirectory/views.py @@ -91,6 +91,7 @@ def get(self, request): class SpaceDirectoryUpdate(APIView): + """Allows authenticated users to update the SpaceAPI information""" permissions_classes = permissions.IsAuthenticated From a5191e9adceb5dc7cd8435eec36ea8d8ea55fe01 Mon Sep 17 00:00:00 2001 From: Matthew Macdonald-Wallace Date: Fri, 13 Oct 2023 18:25:47 +0100 Subject: [PATCH 6/9] Add the total_member and people_now_present counters to the JSON document for SpaceAPI --- memberportal/api_spacedirectory/views.py | 30 +++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/memberportal/api_spacedirectory/views.py b/memberportal/api_spacedirectory/views.py index 721f5d19..f44ba906 100644 --- a/memberportal/api_spacedirectory/views.py +++ b/memberportal/api_spacedirectory/views.py @@ -3,6 +3,8 @@ from rest_framework.views import APIView from constance import config from .models import SpaceAPI, SpaceAPISensor, SpaceAPISensorProperties +from profile.models import Profile +from api_general.models import SiteSession import json @@ -38,23 +40,32 @@ def get(self, request): "issue_report_channels": ["email"], } + # Get the default data spaceapi_data = SpaceAPI.objects.get() + # Get a list of all the sensors spaceapi_sensors = SpaceAPISensor.objects.all() + # Create an empty dict to add the sensor data to sensor_data = {} + # Iterate over the sensors and update the dict as appropriate for sensor in spaceapi_sensors: sensor_details = {} + # Do we already have a sensor of this type? If not, create it now if sensor.sensor_type not in sensor_data: sensor_data[sensor.sensor_type] = [] + + ## Setup the basic details sensor_details = { "name": sensor.name, "description": sensor.description, "location": sensor.location, - "properties": {}, } + + ### Do we have properties? If so, let's add them if len(sensor.properties.all()) > 0: + sensor_details["properties"] = {} for prop in sensor.properties.all(): properties = { prop.name: {"value": prop.value, "unit": prop.unit} @@ -65,9 +76,23 @@ def get(self, request): sensor_data[sensor.sensor_type].append(sensor_details) + ## Add the user count and members on site count to the sensors + spaceapi_user_count = Profile.objects.all().filter(state="active").count() + spaceapi_members_on_site = SiteSession.objects.filter( + signout_date=None + ).order_by("-signin_date") + + sensor_data["total_member_count"] = {"value": spaceapi_user_count} + + sensor_data["people_now_present"] = { + "value": spaceapi_members_on_site.count() + } + + # Is the camera array empty? If not, add them if not config.SPACE_DIRECTORY_CAMS: spaceapi["cameras"] = config.SPACE_DIRECTORY_CAMS + # Set the STATE part of the schema, the icons, and the schema version spaceapi["state"] = { "open": spaceapi_data.space_is_open, "message": spaceapi_data.space_message, @@ -79,14 +104,17 @@ def get(self, request): } spaceapi["api_compatibility"] = ["0.14"] + ## Add the sensor data to the main body of the schema spaceapi["sensors"] = sensor_data + ## Add the location data based on the values in Constance spaceapi["location"] = { "address": config.SPACE_DIRECTORY_LOCATION_ADDRESS, "lat": config.SPACE_DIRECTORY_LOCATION_LAT, "lon": config.SPACE_DIRECTORY_LOCATION_LON, } + # Return the JSON document return Response(spaceapi) From cc744b1c3e02092bf5d739b3f8a13ebd964c21d1 Mon Sep 17 00:00:00 2001 From: Matthew Macdonald-Wallace Date: Sun, 22 Oct 2023 15:13:49 +0100 Subject: [PATCH 7/9] Resolve comments --- docs/SPACEDIRECTORY.md | 2 +- memberportal/api_spacedirectory/views.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/SPACEDIRECTORY.md b/docs/SPACEDIRECTORY.md index 95b8e314..f4be9af3 100644 --- a/docs/SPACEDIRECTORY.md +++ b/docs/SPACEDIRECTORY.md @@ -51,7 +51,7 @@ curl -X POST -H 'Authorization: Token ' \ ### Adding Sensor Data -[Sensors](https://spaceapi.io/docs/#schema-key-sensors) are a really cool part of the SpaceAPI schema, as it allows you to publish all kinds of things from how many drinks are still in teh fridge through to environment readings such as temperature, windspeed, and humidity. +[Sensors](https://spaceapi.io/docs/#schema-key-sensors) are a really cool part of the SpaceAPI schema, as it allows you to publish all kinds of things from how many drinks are still in the fridge through to environment readings such as temperature, windspeed, and humidity. By default, MemberMatters exposes the total number of active members and how many have "signed in" to the space, however you can add your own sensors as long as they conform to the appropriate type. diff --git a/memberportal/api_spacedirectory/views.py b/memberportal/api_spacedirectory/views.py index f44ba906..99a99373 100644 --- a/memberportal/api_spacedirectory/views.py +++ b/memberportal/api_spacedirectory/views.py @@ -1,5 +1,6 @@ from rest_framework.response import Response from rest_framework import status, permissions +from rest_framework_api_key.permissions import HasAPIKey from rest_framework.views import APIView from constance import config from .models import SpaceAPI, SpaceAPISensor, SpaceAPISensorProperties @@ -121,7 +122,7 @@ def get(self, request): class SpaceDirectoryUpdate(APIView): """Allows authenticated users to update the SpaceAPI information""" - permissions_classes = permissions.IsAuthenticated + permissions_classes = permissions.IsAuthenticated | HasAPIKey def post(self, request): # Get the current state of the space @@ -182,7 +183,6 @@ def post(self, request): current_sensor.save() except Exception as e: - print(e) new_sensor = SpaceAPISensor() new_sensor.sensor_type = sensor["type"] new_sensor.name = sensor["name"] From 9e20c8179b02715ee2f1f7df81ca970fd7a40a49 Mon Sep 17 00:00:00 2001 From: Matthew Macdonald-Wallace Date: Sun, 22 Oct 2023 17:16:54 +0100 Subject: [PATCH 8/9] Enable auth via API Key --- docs/SPACEDIRECTORY.md | 6 +----- memberportal/api_spacedirectory/views.py | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/SPACEDIRECTORY.md b/docs/SPACEDIRECTORY.md index f4be9af3..6ee1c5da 100644 --- a/docs/SPACEDIRECTORY.md +++ b/docs/SPACEDIRECTORY.md @@ -40,11 +40,7 @@ Let's say you're opening the space and you want to ensure that everyone knows it You can do this with the following command: ```bash -curl -X POST -d 'username=&password=' \ - 'https://' \ +curl -X POST -H 'Authorization: Api-Key ' \ -d '{"is_open": true, "message": "Open Night TONIGHT! All Welcome between 1800hrs and 2300hrs"}' \ https:///api/spacedirectory/update ``` diff --git a/memberportal/api_spacedirectory/views.py b/memberportal/api_spacedirectory/views.py index 99a99373..3bb15975 100644 --- a/memberportal/api_spacedirectory/views.py +++ b/memberportal/api_spacedirectory/views.py @@ -122,7 +122,7 @@ def get(self, request): class SpaceDirectoryUpdate(APIView): """Allows authenticated users to update the SpaceAPI information""" - permissions_classes = permissions.IsAuthenticated | HasAPIKey + permission_classes = (permissions.IsAdminUser | HasAPIKey,) def post(self, request): # Get the current state of the space From 0ff01151d68555888509b6be86ebeafd24403ef0 Mon Sep 17 00:00:00 2001 From: Matthew Macdonald-Wallace Date: Sun, 22 Oct 2023 17:22:45 +0100 Subject: [PATCH 9/9] Prevent the creation of more than one spacedir --- memberportal/api_spacedirectory/models.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/memberportal/api_spacedirectory/models.py b/memberportal/api_spacedirectory/models.py index 75b39353..78801649 100644 --- a/memberportal/api_spacedirectory/models.py +++ b/memberportal/api_spacedirectory/models.py @@ -1,3 +1,4 @@ +from django.core.exceptions import ValidationError from django.db import models from django.conf import settings @@ -8,6 +9,10 @@ class SpaceAPI(models.Model): space_message = models.CharField(max_length=255, blank=True, null=True) status_last_change = models.DateTimeField(auto_now=True) + def clean(self): + if SpaceAPI.objects.exists() and not self.pk: + raise ValidationError("You can only have one SpaceAPI Setting") + def __str__(self): open_text = "CLOSED" if self.space_is_open is True: