Skip to content
Snippets Groups Projects
Commit 9f470180 authored by Artan SADIKU (HE-Arc)'s avatar Artan SADIKU (HE-Arc)
Browse files

- Update with latest modifications

parent 33e8e9ae
No related branches found
No related tags found
No related merge requests found
# lustra-django-app # lustra-django-app
## Running the Django server on Windows This micro-service is the API used by multiple micro-services in the infrastructure. It is used by the Grafana dashboard as a data source, by the MQTT Client as a data input and by the Godot game as both input and output.
## Requirements
Python >=3.11
Other project dependencies are listed in the [`requirements.txt`](./requirements.txt) file.
## Seting up the development environment
Create a Python virtual environment. Create a Python virtual environment.
...@@ -10,11 +18,19 @@ python -m venv env ...@@ -10,11 +18,19 @@ python -m venv env
Activate the virtual enviornment. Activate the virtual enviornment.
- On Linux based systems
``` ```
env\Scripts\activate.bat source ./env/bin/activate
``` ```
Install the requirements. - On Windows
```
.\env\Scripts\activate
```
Install the requirements using pip.
``` ```
pip install -r requirements.txt pip install -r requirements.txt
...@@ -22,6 +38,8 @@ pip install -r requirements.txt ...@@ -22,6 +38,8 @@ pip install -r requirements.txt
Apply the database migrations. Apply the database migrations.
> By default, SQLite3 is used as a development database. Refer to Djang's documentation if you wish to modify the database driver used.
``` ```
python lustraDjangoApp\manage.py migrate python lustraDjangoApp\manage.py migrate
``` ```
...@@ -34,14 +52,9 @@ python lustraDjangoApp\manage.py runserver ...@@ -34,14 +52,9 @@ python lustraDjangoApp\manage.py runserver
Run the server on default route. Run the server on default route.
``` ## API Documentation
python lustraDjangoApp\manage.py 0.0.0.0:8000
```
## Running the Django server with the launch script on Windows The API documentation is served on two endpoints after running the app:
Run the `launch-script-windows.bat` through a terminal. - `/api/schema/swagger-ui` using Swagger
- `/api/schema/redoc` using Redoc
```
C:\...> launch-script-windows.bat
```
...@@ -4,10 +4,14 @@ from django.db import models ...@@ -4,10 +4,14 @@ from django.db import models
class Message(models.Model): class Message(models.Model):
"""Messages that transit between the broker and the other micro-services.
"""
payload = models.JSONField() payload = models.JSONField()
class RecyclingCounters(models.Model): class RecyclingCounters(models.Model):
"""Numerical counters used by the game.
"""
pet_counter = models.IntegerField() pet_counter = models.IntegerField()
alu_counter = models.IntegerField() alu_counter = models.IntegerField()
bad_recycling_counter = models.IntegerField(default=0) bad_recycling_counter = models.IntegerField(default=0)
...@@ -15,17 +19,23 @@ class RecyclingCounters(models.Model): ...@@ -15,17 +19,23 @@ class RecyclingCounters(models.Model):
class UserFeedback(models.Model): class UserFeedback(models.Model):
"""Feedback send by the user when a recycling was deemed as a bad.
"""
container_type = models.IntegerField() container_type = models.IntegerField()
is_system_wrong = models.BooleanField() is_system_wrong = models.BooleanField()
timestamp = models.DateTimeField(auto_now_add=True) timestamp = models.DateTimeField(auto_now_add=True)
class DeviceMonitoringData(models.Model): class DeviceMonitoringData(models.Model):
"""Pings sent by the devices to monitor their activity.
"""
device_id = models.IntegerField() device_id = models.IntegerField()
timestamp = models.DateTimeField(auto_now_add=True) timestamp = models.DateTimeField(auto_now_add=True)
class RecyclingData(models.Model): class RecyclingData(models.Model):
"""Recycling data class used primarly for showing data.
"""
device_id = models.IntegerField() device_id = models.IntegerField()
container_type = models.IntegerField() container_type = models.IntegerField()
game_code = models.BooleanField() game_code = models.BooleanField()
......
...@@ -2,7 +2,11 @@ from lustraApi.models import Message, UserFeedback, RecyclingCounters, DeviceMon ...@@ -2,7 +2,11 @@ from lustraApi.models import Message, UserFeedback, RecyclingCounters, DeviceMon
from lustraApi.serializers import MessagesSerializer, UserFeedbackSerializer, RecyclingCountersSerializer, DeviceMonitoringDataSerializer, RecyclingDataSerializer from lustraApi.serializers import MessagesSerializer, UserFeedbackSerializer, RecyclingCountersSerializer, DeviceMonitoringDataSerializer, RecyclingDataSerializer
from rest_framework import viewsets, status from rest_framework import viewsets, status
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.request import Request
from rest_framework.decorators import action from rest_framework.decorators import action
from datetime import datetime
from itertools import groupby
import ciso8601
# Create your views here. # Create your views here.
...@@ -11,6 +15,19 @@ class MessageViewSet(viewsets.ModelViewSet): ...@@ -11,6 +15,19 @@ class MessageViewSet(viewsets.ModelViewSet):
queryset = Message.objects.all() queryset = Message.objects.all()
serializer_class = MessagesSerializer serializer_class = MessagesSerializer
@action(detail=False, methods=["GET"], url_path='get-and-delete')
def get_and_delete(self, request):
"""Retrieves and removes all messages.
"""
try:
response = self.list(request=request)
messages = self.queryset.all()
for message in messages:
message.delete()
return response
except RecyclingCounters.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
class UserFeedbackViewSet(viewsets.ModelViewSet): class UserFeedbackViewSet(viewsets.ModelViewSet):
queryset = UserFeedback.objects.all() queryset = UserFeedback.objects.all()
...@@ -21,10 +38,22 @@ class RecyclingCountersViewSet(viewsets.ModelViewSet): ...@@ -21,10 +38,22 @@ class RecyclingCountersViewSet(viewsets.ModelViewSet):
queryset = RecyclingCounters.objects.all() queryset = RecyclingCounters.objects.all()
serializer_class = RecyclingCountersSerializer serializer_class = RecyclingCountersSerializer
def create(self, request: Request, *args, **kwargs):
response = self.latest(request)
if response.status_code == status.HTTP_200_OK:
if 'pet_counter' in response.data and 'alu_counter' in response.data and 'bad_recycling_counter' in response.data:
if request.data['pet_counter'] >= response.data['pet_counter'] and \
request.data['alu_counter'] >= response.data['alu_counter'] and \
request.data['bad_recycling_counter'] >= response.data['bad_recycling_counter']:
return super().create(request, *args, **kwargs)
return Response(status=status.HTTP_204_NO_CONTENT)
@action( @action(
detail=False, methods=["GET"], url_path="latest" detail=False, methods=["GET"], url_path="latest"
) )
def latest(self, request): def latest(self, request):
"""Returns the latest record in the database.
"""
try: try:
latest_recycling_counters = self.queryset.latest('timestamp') latest_recycling_counters = self.queryset.latest('timestamp')
serializer = self.get_serializer(latest_recycling_counters) serializer = self.get_serializer(latest_recycling_counters)
...@@ -41,6 +70,8 @@ class DeviceMonitoringDataViewSet(viewsets.ModelViewSet): ...@@ -41,6 +70,8 @@ class DeviceMonitoringDataViewSet(viewsets.ModelViewSet):
detail=False, methods=["GET"], url_path=r'latest/(?P<device_id>[^/.]+)' detail=False, methods=["GET"], url_path=r'latest/(?P<device_id>[^/.]+)'
) )
def latest(self, request, device_id=None): def latest(self, request, device_id=None):
"""Returns the latest record in the database.
"""
try: try:
device_monitoring_data = self.queryset.filter( device_monitoring_data = self.queryset.filter(
device_id__exact=device_id).latest('timestamp') device_id__exact=device_id).latest('timestamp')
...@@ -53,3 +84,52 @@ class DeviceMonitoringDataViewSet(viewsets.ModelViewSet): ...@@ -53,3 +84,52 @@ class DeviceMonitoringDataViewSet(viewsets.ModelViewSet):
class RecyclingDataViewSet(viewsets.ModelViewSet): class RecyclingDataViewSet(viewsets.ModelViewSet):
queryset = RecyclingData.objects.all() queryset = RecyclingData.objects.all()
serializer_class = RecyclingDataSerializer serializer_class = RecyclingDataSerializer
@action(
detail=False, methods=["GET"], url_path='metrics'
)
def metrics(self, request):
"""Get recycling data formatted in order to display it on a Grafana dashboard.
Each recycling data is grouped by day, which is extracted from the timestamp. The data is expanded with additional counters that are computed in order to sepearte good / bad recycling for PET / ALU.
"""
recycling_data_list = list(self.queryset)
def transform_data(data):
serialized_data = self.get_serializer(data).data
serialized_data['timestamp'] = datetime.fromtimestamp(
serialized_data['timestamp']).strftime('%Y-%m-%d')
return serialized_data
transformed_data = map(transform_data, recycling_data_list)
grouped_by_timestamp_data = groupby(
transformed_data, lambda x: x['timestamp'])
return_data = list()
# Might need improving because this will be slow with a lot of data...
for key, value in grouped_by_timestamp_data:
good_pet_recycling = 0
good_alu_recycling = 0
bad_pet_recycling = 0
bad_alu_recycling = 0
for element in list(value):
if element['container_type'] == 0 and element['game_code']:
good_pet_recycling += 1
elif element['container_type'] == 1 and element['game_code']:
good_alu_recycling += 1
elif element['container_type'] == 0 and not element['game_code']:
bad_pet_recycling += 1
elif element['container_type'] == 1 and not element['game_code']:
bad_alu_recycling += 1
return_data.append(
{
'good_pet_recycling': good_pet_recycling,
'good_alu_recycling': good_alu_recycling,
'bad_pet_recycling': bad_pet_recycling,
'bad_alu_recycling': bad_alu_recycling,
'total_pet_recycling': good_pet_recycling + bad_pet_recycling,
'total_alu_recycling': good_alu_recycling + bad_alu_recycling,
'time': ciso8601.parse_datetime(key).timestamp()
}
)
return Response(return_data)
...@@ -48,6 +48,8 @@ INSTALLED_APPS = [ ...@@ -48,6 +48,8 @@ INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'rest_framework', 'rest_framework',
'drf_spectacular',
'drf_spectacular_sidecar',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
...@@ -130,7 +132,8 @@ AUTH_PASSWORD_VALIDATORS = [ ...@@ -130,7 +132,8 @@ AUTH_PASSWORD_VALIDATORS = [
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': ( 'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer', 'rest_framework.renderers.JSONRenderer',
) ),
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
} }
# Internationalization # Internationalization
...@@ -155,4 +158,14 @@ STATIC_ROOT = os.path.join(BASE_DIR, "static/") ...@@ -155,4 +158,14 @@ STATIC_ROOT = os.path.join(BASE_DIR, "static/")
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.Bi gAutoField'
SPECTACULAR_SETTINGS = {
'TITLE': 'Lustra Django API',
'DESCRIPTION': 'This API serves as a data source for the Godot game and the Grafana dashboard, and as a data input for the information coming from the MQTT Broker.',
'VERSION': '1.0.0',
'SERVE_INCLUDE_SCHEMA': False,
'SWAGGER_UI_DIST': 'SIDECAR', # shorthand to use the sidecar instead
'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
'REDOC_DIST': 'SIDECAR',
}
...@@ -14,10 +14,10 @@ Including another URLconf ...@@ -14,10 +14,10 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path 1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.contrib import admin
from rest_framework import routers from rest_framework import routers
from django.urls import path, include from django.urls import path, include
from lustraApi import views from lustraApi import views
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.register(r'messages', views.MessageViewSet) router.register(r'messages', views.MessageViewSet)
...@@ -29,5 +29,12 @@ router.register(r'recycling-data', views.RecyclingDataViewSet) ...@@ -29,5 +29,12 @@ router.register(r'recycling-data', views.RecyclingDataViewSet)
urlpatterns = [ urlpatterns = [
path('', include(router.urls)), path('', include(router.urls)),
# YOUR PATTERNS
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
# Optional UI:
path('api/schema/swagger-ui/',
SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
path('api/schema/redoc/',
SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
# path('admin/', admin.site.urls), # path('admin/', admin.site.urls),
] ]
This diff is collapsed.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment