diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..58ad9ec --- /dev/null +++ b/.flake8 @@ -0,0 +1,5 @@ +[flake8] + ignore = D203, E402, F403, F405, W503, W605 + exclude = .git,env,__pycache__,docs/source/conf.py,old,build,dist, *migrations*,env,settings.py,local_settings.py,example.local_settings.py + max-complexity = 10 + max-line-length = 119 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2125666 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8fc9635..783d3e6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,12 @@ myenv/ venv/ +env/ __pycache__ .coverage .idea .vscode .env +.env.example *.csv *.ipynb data diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 0000000..1041be8 --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,7 @@ +[settings] +multi_line_output=3 +include_trailing_comma=True +force_grid_wrap=0 +use_parentheses=True +line_length=88 +skip=env,migrations,local_settings.py \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index c41cde3..ec1bd07 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ WORKDIR /code # RUN apt-get install -y cron # Upgrade Pip -RUN pip install --upgrade pip +# RUN pip install --upgrade pip # Install dependencies COPY requirements.txt /code/ diff --git a/analysis/aggregate_data.py b/analysis/aggregate_data.py index 66c93d6..1fb4d70 100644 --- a/analysis/aggregate_data.py +++ b/analysis/aggregate_data.py @@ -1,9 +1,11 @@ import pandas as pd -from .models import DailyAggregate, HourlyAggregate + from devices.models import Device, location_category_information -class Aggregate: +from .models import DailyAggregate, HourlyAggregate + +class Aggregate: def __init__(self, data): self.data = data self.locations = self.get_device_locations() @@ -29,39 +31,50 @@ def aggregate_hourly(self, time_uploaded=None): grouped_data = df.groupby("device_id", sort=False) for device_name, device_df in grouped_data: if 6 <= hour < 22: - threshold = location_category_information[self.locations[device_name].category]["day_limit"] + threshold = location_category_information[ + self.locations[device_name].category + ]["day_limit"] else: - threshold = location_category_information[self.locations[device_name].category]["night_limit"] + threshold = location_category_information[ + self.locations[device_name].category + ]["night_limit"] HourlyAggregate.objects.create( - device = Device.objects.get(device_id=device_name), + device=Device.objects.get(device_id=device_name), date=time_uploaded, - hour = hour, - hourly_avg_db_level = device_df["db_level"].mean(), - hourly_median_db_level = device_df["db_level"].median(), - hourly_max_db_level = device_df["db_level"].max(), - hourly_no_of_exceedances = len(device_df[device_df["db_level"] > threshold]) + hour=hour, + hourly_avg_db_level=device_df["db_level"].mean(), + hourly_median_db_level=device_df["db_level"].median(), + hourly_max_db_level=device_df["db_level"].max(), + hourly_no_of_exceedances=len( + device_df[device_df["db_level"] > threshold] + ), ) return None - def aggregate_daily(self, time_period): df = self.prepare_data() grouped_data = df.groupby("device_id", sort=False) for device_name, device_df in grouped_data: if time_period == "daytime": - threshold = location_category_information[self.locations[device_name].category]["day_limit"] + threshold = location_category_information[ + self.locations[device_name].category + ]["day_limit"] else: - threshold = location_category_information[self.locations[device_name].category]["night_limit"] + threshold = location_category_information[ + self.locations[device_name].category + ]["night_limit"] DailyAggregate.objects.create( - device = Device.objects.get(device_id=device_name), - time_period = time_period, - daily_avg_db_level = device_df["db_level"].mean(), - daily_median_db_level = device_df["db_level"].median(), - daily_max_db_level = device_df["db_level"].max(), - daily_no_of_exceedances = len(device_df[device_df["db_level"] > threshold]) + device=Device.objects.get(device_id=device_name), + time_period=time_period, + daily_avg_db_level=device_df["db_level"].mean(), + daily_median_db_level=device_df["db_level"].median(), + daily_max_db_level=device_df["db_level"].max(), + daily_no_of_exceedances=len( + device_df[device_df["db_level"] > threshold] + ), ) return None diff --git a/analysis/apps.py b/analysis/apps.py index c2bedc0..7d4dfcb 100644 --- a/analysis/apps.py +++ b/analysis/apps.py @@ -2,5 +2,5 @@ class AnalysisConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'analysis' + default_auto_field = "django.db.models.BigAutoField" + name = "analysis" diff --git a/analysis/cron.py b/analysis/cron.py index 1667d82..5593492 100644 --- a/analysis/cron.py +++ b/analysis/cron.py @@ -1,18 +1,17 @@ import logging -import pytz from datetime import datetime +import pytz from django.db.models import Avg, Max, Sum -from noise_dashboard.settings import TIME_ZONE -from .models import DailyAnalysis -from devices.models import Device from device_metrics.models import DeviceMetrics +from devices.models import Device +from noise_dashboard.settings import TIME_ZONE +from .models import DailyAnalysis -logging.basicConfig(filename="app.log", - format="%(asctime)s - %(message)s", - level=logging.INFO +logging.basicConfig( + filename="app.log", format="%(asctime)s - %(message)s", level=logging.INFO ) @@ -27,19 +26,19 @@ def aggregate_daily_metrics(): device=device.id, time_uploaded__year=today.year, time_uploaded__month=today.month, - time_uploaded__day=today.day + time_uploaded__day=today.day, ) - daily_avg_db_level = device_metrics.aggregate(Avg('avg_db_level')) - daily_max_db_level = device_metrics.aggregate(Max('max_db_level')) - daily_no_of_exceedances = device_metrics.aggregate(Sum('no_of_exceedances')) + daily_avg_db_level = device_metrics.aggregate(Avg("avg_db_level")) + daily_max_db_level = device_metrics.aggregate(Max("max_db_level")) + daily_no_of_exceedances = device_metrics.aggregate(Sum("no_of_exceedances")) DailyAnalysis.objects.create( - date_analyzed = today, - daily_avg_db_level=daily_avg_db_level['avg_db_level__avg'], - daily_max_db_level=daily_max_db_level['max_db_level__max'], - daily_no_of_exceedances=daily_no_of_exceedances['no_of_exceedances__sum'], - device=device + date_analyzed=today, + daily_avg_db_level=daily_avg_db_level["avg_db_level__avg"], + daily_max_db_level=daily_max_db_level["max_db_level__max"], + daily_no_of_exceedances=daily_no_of_exceedances["no_of_exceedances__sum"], + device=device, ) - logging.info(f'Daily device metrics for device {device} aggregated') + logging.info(f"Daily device metrics for device {device} aggregated") diff --git a/analysis/models.py b/analysis/models.py index 81d6a39..197ad74 100644 --- a/analysis/models.py +++ b/analysis/models.py @@ -1,17 +1,18 @@ import uuid + +from django.core.files.storage import get_storage_class from django.db import models from django.utils import timezone -from django.core.files.storage import get_storage_class from devices.models import Device - media_storage = get_storage_class()() def recording_directory(instance, filename): time_uploaded = instance.time_uploaded.strftime("%Y-%m-%dT%H:%M:%S") - return f'metrics/{instance.device.device_id}/{instance.device.device_id}-{time_uploaded}-{filename}' + return f"metrics/{instance.device.device_id}/{instance.device.device_id}-{time_uploaded}-{filename}" + class MetricsTextFile(models.Model): time_uploaded = models.DateTimeField(auto_now_add=True) @@ -28,11 +29,7 @@ def filename(self): class HourlyAggregate(models.Model): - id = models.UUIDField( - primary_key=True, - default=uuid.uuid4, - editable=False - ) + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) date = models.DateTimeField(default=timezone.now) hour = models.PositiveIntegerField(null=True) hourly_avg_db_level = models.FloatField(null=True) @@ -43,16 +40,11 @@ class HourlyAggregate(models.Model): class DailyAggregate(models.Model): - class TimePeriod(models.TextChoices): - DAYTIME = 'daytime' - NIGHTTIME = 'nighttime' - - id = models.UUIDField( - primary_key=True, - default=uuid.uuid4, - editable=False - ) + DAYTIME = "daytime" + NIGHTTIME = "nighttime" + + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) date = models.DateTimeField(default=timezone.now) time_period = models.CharField(max_length=10, choices=TimePeriod.choices) daily_avg_db_level = models.FloatField(null=True) diff --git a/analysis/parse_file_metrics.py b/analysis/parse_file_metrics.py index f72340d..75965ed 100644 --- a/analysis/parse_file_metrics.py +++ b/analysis/parse_file_metrics.py @@ -4,7 +4,7 @@ def parse_file(file, device_id): """ metrics = [] missing_values = 0 - with file.open('r') as f: + with file.open("r") as f: lines = f.readlines() for line in lines: line.strip() @@ -14,7 +14,9 @@ def parse_file(file, device_id): try: decibel = float(decibel_string.split(":")[1]) time_uploaded = time_uploaded_string[3:] - metrics.append({'device_id': device_id, 'db_level': decibel, 'date': time_uploaded}) + metrics.append( + {"device_id": device_id, "db_level": decibel, "date": time_uploaded} + ) except ValueError as e: missing_values += 1 print(f"Error: {e}") diff --git a/analysis/serializers.py b/analysis/serializers.py index da32ebc..34011cf 100644 --- a/analysis/serializers.py +++ b/analysis/serializers.py @@ -1,44 +1,43 @@ from rest_framework import serializers -from devices.models import Device + import devices.serializers -from .models import HourlyAggregate, DailyAggregate, MetricsTextFile +from devices.models import Device + +from .models import DailyAggregate, HourlyAggregate, MetricsTextFile + class UploadMetricsTextFileSerializer(serializers.ModelSerializer): - device = serializers.SlugRelatedField(queryset=Device.objects.all(), slug_field='device_id') + device = serializers.SlugRelatedField( + queryset=Device.objects.all(), slug_field="device_id" + ) class Meta: model = MetricsTextFile - fields = [ - 'id', 'metrics_file', 'device' - ] - read_only_fields = ['time_uploaded'] + fields = ["id", "metrics_file", "device"] + read_only_fields = ["time_uploaded"] def to_representation(self, instance): - self.fields['device'] = devices.serializers.DeviceSerializer(read_only=True) - return { - "result": "success" - } + self.fields["device"] = devices.serializers.DeviceSerializer(read_only=True) + return {"result": "success"} class ListMetricsTextFileSerializer(serializers.ModelSerializer): - device = serializers.SlugRelatedField(queryset=Device.objects.all(), slug_field='device_id') + device = serializers.SlugRelatedField( + queryset=Device.objects.all(), slug_field="device_id" + ) class Meta: model = MetricsTextFile - fields = [ - 'id', 'time_uploaded', 'device', 'text_file_url' - ] + fields = ["id", "time_uploaded", "device", "text_file_url"] class HourlyAnalysisSerializer(serializers.ModelSerializer): - class Meta: model = HourlyAggregate fields = "__all__" class DailyAnalysisSerializer(serializers.ModelSerializer): - class Meta: model = DailyAggregate fields = "__all__" diff --git a/analysis/urls.py b/analysis/urls.py index d4c8d63..8579fc3 100644 --- a/analysis/urls.py +++ b/analysis/urls.py @@ -1,16 +1,15 @@ -from django.urls import path, include -from .views import ( +from django.urls import include, path +from rest_framework.routers import SimpleRouter + +from .views import ( # DailyAnalysisView,; ReceiveIoTDataView, + AggregateMetricsView, HourlyAnalysisView, - # DailyAnalysisView, - # ReceiveIoTDataView, + ListMetricsFilesView, ReceiveMetricsFileViewSet, - AggregateMetricsView, - ListMetricsFilesView ) -from rest_framework.routers import SimpleRouter router = SimpleRouter() -router.register('metrics-file', ReceiveMetricsFileViewSet) +router.register("metrics-file", ReceiveMetricsFileViewSet) # urlpatterns = [ # path('hourly/', HourlyAnalysisView.as_view(), name='hourly_analysis'), @@ -18,8 +17,16 @@ # path('receive_iot_data/', ReceiveIoTDataView.as_view(), name='iot_data'), # ] urlpatterns = [ - path('aggregate-metrics/', AggregateMetricsView.as_view(), name='aggregate_metrics'), - path('list-metrics/', ListMetricsFilesView.as_view(), name='list_metric_files'), - path('hourly/', HourlyAnalysisView.as_view(), name='hourly_analysis'), - path('', include(router.urls)) + path( + "aggregate-metrics/", AggregateMetricsView.as_view(), name="aggregate_metrics" + ), + path( + "list-metrics/", + ListMetricsFilesView.as_view(), + name="list_metric_files", + ), + path( + "hourly/", HourlyAnalysisView.as_view(), name="hourly_analysis" + ), + path("", include(router.urls)), ] diff --git a/analysis/views.py b/analysis/views.py index d136612..ca102d1 100644 --- a/analysis/views.py +++ b/analysis/views.py @@ -1,27 +1,24 @@ -import pytz - from datetime import datetime, timedelta -from rest_framework import viewsets, parsers +import pytz +from rest_framework import parsers, viewsets from rest_framework.generics import ListAPIView from rest_framework.response import Response from rest_framework.views import APIView -from .aggregate_data import Aggregate -from .models import MetricsTextFile, HourlyAggregate, DailyAggregate +from devices.models import Device +from noise_dashboard.settings import TIME_ZONE +from .aggregate_data import Aggregate +from .models import DailyAggregate, HourlyAggregate, MetricsTextFile +from .parse_file_metrics import parse_file from .serializers import ( - UploadMetricsTextFileSerializer, - ListMetricsTextFileSerializer, + DailyAnalysisSerializer, HourlyAnalysisSerializer, - DailyAnalysisSerializer + ListMetricsTextFileSerializer, + UploadMetricsTextFileSerializer, ) -from devices.models import Device -from .parse_file_metrics import parse_file - -from noise_dashboard.settings import TIME_ZONE - timezone = pytz.timezone(TIME_ZONE) today = datetime.today() today = timezone.localize(today) @@ -32,15 +29,17 @@ class ReceiveMetricsFileViewSet(viewsets.ModelViewSet): queryset = MetricsTextFile.objects.all() serializer_class = UploadMetricsTextFileSerializer parser_classes = [parsers.MultiPartParser] - http_method_names = ['post'] + http_method_names = ["post"] def create(self, request, *args, **kwargs): response = super().create(request, *args, **kwargs) # TODO: The code below writes the aggregate of the file received. This should probably be done asynchronously, since it makes the request take a long time. - device_id = request.data.get('device') + device_id = request.data.get("device") device = Device.objects.get(device_id=device_id) now = datetime.now() - metric_files = device.metricstextfile_set.filter(time_uploaded__range=[now - timedelta(minutes=30), now]) + metric_files = device.metricstextfile_set.filter( + time_uploaded__range=[now - timedelta(minutes=30), now] + ) for metric_file in metric_files: metrics_data = parse_file(metric_file.metrics_file.file, device_id) if len(metrics_data) > 0: @@ -53,25 +52,23 @@ class ListMetricsFilesView(ListAPIView): serializer_class = ListMetricsTextFileSerializer def list(self, request, *args, **kwargs): - past_days = request.query_params.get('past_days', 1) + past_days = request.query_params.get("past_days", 1) queryset = self.get_queryset(int(past_days)) serializer = self.get_serializer(queryset, many=True) num_files = len(serializer.data) - result = { - "number_of_files": num_files, - "metric_files": serializer.data - } + result = {"number_of_files": num_files, "metric_files": serializer.data} return Response(result) def get_queryset(self, past_days=1): - device_id = self.kwargs['device_id'] + device_id = self.kwargs["device_id"] queryset = MetricsTextFile.objects.filter(device__device_id=device_id) - queryset = queryset.filter(time_uploaded__range=[today - timedelta(days=past_days), today]) - return queryset.order_by('-time_uploaded') + queryset = queryset.filter( + time_uploaded__range=[today - timedelta(days=past_days), today] + ) + return queryset.order_by("-time_uploaded") class AggregateMetricsView(APIView): - def post(self, request): device_id = request.data["device_id"] start = int(request.data["start"]) @@ -79,7 +76,9 @@ def post(self, request): start_hours = timedelta(hours=start) end_hours = timedelta(hours=end) device = Device.objects.get(device_id=device_id) - metric_files = device.metricstextfile_set.filter(time_uploaded__range=[today - end_hours, today - start_hours]) + metric_files = device.metricstextfile_set.filter( + time_uploaded__range=[today - end_hours, today - start_hours] + ) processed_files = [] for metric_file in metric_files: metrics_data = parse_file(metric_file.metrics_file.file, device_id) @@ -91,25 +90,29 @@ def post(self, request): metric_files_dict = { "number_of_files": len(processed_files), "files": [ - {'time_uploaded': metric_file.time_uploaded, - 'device': metric_file.device.device_id, - 'filename': metric_file.filename - } for metric_file in processed_files - ] + { + "time_uploaded": metric_file.time_uploaded, + "device": metric_file.device.device_id, + "filename": metric_file.filename, + } + for metric_file in processed_files + ], } return Response(metric_files_dict) def get_analysis_queryset(api_view_object: ListAPIView, hourly=True): device_id = api_view_object.kwargs["device_id"] - past_days = int(api_view_object.request.query_params.get('past_days', 1)) + past_days = int(api_view_object.request.query_params.get("past_days", 1)) if hourly: queryset = HourlyAggregate.objects.filter(device__device_id=device_id) else: queryset = DailyAggregate.objects.filter(device__device_id=device_id) - queryset = queryset.filter(date__range=[datetime.now() - timedelta(days=past_days), datetime.now()]) - return queryset.order_by('-date') + queryset = queryset.filter( + date__range=[datetime.now() - timedelta(days=past_days), datetime.now()] + ) + return queryset.order_by("-date") class HourlyAnalysisView(ListAPIView): diff --git a/device_metrics/admin.py b/device_metrics/admin.py index 89a40c1..7f903fb 100644 --- a/device_metrics/admin.py +++ b/device_metrics/admin.py @@ -1,5 +1,31 @@ from django.contrib import admin -from .models import DeviceMetrics +from .models import DeviceMetrics, EnvironmentalParameter, SoundInferenceData admin.site.register(DeviceMetrics) + + +@admin.register(EnvironmentalParameter) +class EnvironmentalParameterAdmin(admin.ModelAdmin): + list_display = ( + "device", + "temperature", + "pressure", + "humidity", + "air_quality", + "ram_value", + "system_temperature", + "power_usage", + ) + search_fields = ("device__device_id",) + + +@admin.register(SoundInferenceData) +class SoundInferenceDataAdmin(admin.ModelAdmin): + list_display = ( + "device", + "inference_probability", + "inference_class", + "inferred_audio_name", + ) + search_fields = ("device__device_id", "inference_class") diff --git a/device_metrics/apps.py b/device_metrics/apps.py index 312cef5..e3bd7b6 100644 --- a/device_metrics/apps.py +++ b/device_metrics/apps.py @@ -2,5 +2,5 @@ class DeviceMetricsConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'device_metrics' + default_auto_field = "django.db.models.BigAutoField" + name = "device_metrics" diff --git a/device_metrics/forms.py b/device_metrics/forms.py index cdc71cd..68b42c7 100644 --- a/device_metrics/forms.py +++ b/device_metrics/forms.py @@ -1,4 +1,5 @@ from django.forms import ModelForm + from .models import DeviceMetrics @@ -6,7 +7,15 @@ class DeviceMetricsForm(ModelForm): class Meta: model = DeviceMetrics fields = [ - 'device', 'sig_strength', 'db_level', 'avg_db_level', 'max_db_level', - 'no_of_exceedances', 'last_rec', 'last_upl', 'panel_voltage', - 'battery_voltage', 'data_balance' - ] + "device", + "sig_strength", + "db_level", + "avg_db_level", + "max_db_level", + "no_of_exceedances", + "last_rec", + "last_upl", + "panel_voltage", + "battery_voltage", + "data_balance", + ] diff --git a/device_metrics/migrations/0005_environmentalparameter_soundinferencedata.py b/device_metrics/migrations/0005_environmentalparameter_soundinferencedata.py new file mode 100644 index 0000000..137421c --- /dev/null +++ b/device_metrics/migrations/0005_environmentalparameter_soundinferencedata.py @@ -0,0 +1,42 @@ +# Generated by Django 3.2.6 on 2024-07-17 18:43 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('devices', '0020_remove_device_lastseen'), + ('device_metrics', '0004_auto_20220223_2307'), + ] + + operations = [ + migrations.CreateModel( + name='SoundInferenceData', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('inference_probability', models.FloatField(default=0.0, help_text='Probability of predicted class')), + ('inference_class', models.CharField(help_text='Name of predicted class', max_length=255)), + ('inferred_audio_name', models.CharField(help_text='Name of inferred sound file', max_length=255)), + ('device', models.ForeignKey(help_text='Device ID', on_delete=django.db.models.deletion.CASCADE, related_name='inferences', related_query_name='sound_data', to='devices.device')), + ], + options={ + 'verbose_name_plural': 'Sound Inference Data', + }, + ), + migrations.CreateModel( + name='EnvironmentalParameter', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('temperature', models.FloatField(default=0.0, help_text='Environmental temperature')), + ('pressure', models.FloatField(default=0.0, help_text='Atmospheric pressure')), + ('humidity', models.FloatField(default=0.0, help_text='Atmospheric humidity')), + ('air_quality', models.FloatField(default=0.0, help_text='Volatile organic compounds vary resistance')), + ('ram_value', models.FloatField(default=0.0, help_text='Memory usage of the PI')), + ('system_temperature', models.FloatField(default=0.0, help_text='Temperature of the PI')), + ('power_usage', models.FloatField(default=0.0, help_text='Power utilization of the PI')), + ('device', models.ForeignKey(help_text='Device ID', on_delete=django.db.models.deletion.CASCADE, related_name='environmental_parameters', related_query_name='environmental_param', to='devices.device')), + ], + ), + ] diff --git a/device_metrics/models.py b/device_metrics/models.py index 80a159f..ec088b1 100644 --- a/device_metrics/models.py +++ b/device_metrics/models.py @@ -1,20 +1,15 @@ import uuid -from django.db import models from django.core.validators import MaxValueValidator +from django.db import models from devices.models import Device + class DeviceMetrics(models.Model): - id = models.UUIDField( - primary_key=True, - default=uuid.uuid4, - editable=False - ) + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) device = models.ForeignKey(Device, on_delete=models.CASCADE) - sig_strength = models.PositiveSmallIntegerField( - validators = [MaxValueValidator(31)] - ) + sig_strength = models.PositiveSmallIntegerField(validators=[MaxValueValidator(31)]) db_level = models.FloatField() avg_db_level = models.FloatField(null=True) max_db_level = models.FloatField(null=True) @@ -25,3 +20,54 @@ class DeviceMetrics(models.Model): battery_voltage = models.FloatField() data_balance = models.PositiveIntegerField() time_uploaded = models.DateTimeField(auto_now_add=True) + + +class EnvironmentalParameter(models.Model): + device = models.ForeignKey( + Device, + related_name="environmental_parameters", + related_query_name="environmental_param", + on_delete=models.CASCADE, + help_text="Device ID", + ) + temperature = models.FloatField(help_text="Environmental temperature", default=0.0) + pressure = models.FloatField(help_text="Atmospheric pressure", default=0.0) + humidity = models.FloatField(help_text="Atmospheric humidity", default=0.0) + air_quality = models.FloatField( + help_text="Volatile organic compounds vary resistance", default=0.0 + ) + ram_value = models.FloatField(help_text="Memory usage of the PI", default=0.0) + system_temperature = models.FloatField( + help_text="Temperature of the PI", default=0.0 + ) + power_usage = models.FloatField( + help_text="Power utilization of the PI", default=0.0 + ) + + def __str__(self): + return f"{self.device.device_id} - Temp: {self.temperature}, Pressure: {self.pressure}, Humidity: {self.humidity}" + + +class SoundInferenceData(models.Model): + device = models.ForeignKey( + Device, + related_name="inferences", + related_query_name="sound_data", + on_delete=models.CASCADE, + help_text="Device ID", + ) + inference_probability = models.FloatField( + help_text="Probability of predicted class", default=0.0 + ) + inference_class = models.CharField( + max_length=255, help_text="Name of predicted class" + ) + inferred_audio_name = models.CharField( + max_length=255, help_text="Name of inferred sound file" + ) + + class Meta: + verbose_name_plural = "Sound Inference Data" + + def __str__(self): + return f"{self.device.device_id} - {self.inference_class}: {self.inference_probability}" diff --git a/device_metrics/serializers.py b/device_metrics/serializers.py index f872bbc..d9b6560 100644 --- a/device_metrics/serializers.py +++ b/device_metrics/serializers.py @@ -1,18 +1,66 @@ from rest_framework import serializers -from .models import DeviceMetrics + from devices.models import Device +from .models import DeviceMetrics, EnvironmentalParameter, SoundInferenceData + class DeviceMetricsSerializer(serializers.ModelSerializer): # device = serializers.PrimaryKeyRelatedField(queryset=Device.objects.all()) - device = serializers.SlugRelatedField(queryset=Device.objects.all(), - slug_field='device_id') + device = serializers.SlugRelatedField( + queryset=Device.objects.all(), slug_field="device_id" + ) class Meta: model = DeviceMetrics fields = [ - 'device', 'sig_strength', 'db_level', 'avg_db_level', 'max_db_level', - 'no_of_exceedances', 'last_rec', 'last_upl', 'panel_voltage', - 'battery_voltage', 'data_balance', 'time_uploaded' - ] - read_only_fields = ['time_uploaded'] + "device", + "sig_strength", + "db_level", + "avg_db_level", + "max_db_level", + "no_of_exceedances", + "last_rec", + "last_upl", + "panel_voltage", + "battery_voltage", + "data_balance", + "time_uploaded", + ] + read_only_fields = ["time_uploaded"] + + +class EnvironmentalParameterSerializer(serializers.ModelSerializer): + device = serializers.SlugRelatedField( + queryset=Device.objects.all(), slug_field="device_id" + ) + + class Meta: + model = EnvironmentalParameter + fields = [ + "id", + "device", + "temperature", + "pressure", + "humidity", + "air_quality", + "ram_value", + "system_temperature", + "power_usage", + ] + + +class SoundInferenceDataSerializer(serializers.ModelSerializer): + device = serializers.SlugRelatedField( + queryset=Device.objects.all(), slug_field="device_id" + ) + + class Meta: + model = SoundInferenceData + fields = [ + "id", + "device", + "inference_probability", + "inference_class", + "inferred_audio_name", + ] diff --git a/device_metrics/tests.py b/device_metrics/tests.py index 7ce503c..a720f20 100644 --- a/device_metrics/tests.py +++ b/device_metrics/tests.py @@ -1,3 +1,70 @@ from django.test import TestCase -# Create your tests here. +from devices.models import Device + +from .models import EnvironmentalParameter, SoundInferenceData + + +class DeviceModelTest(TestCase): + def setUp(self): + self.device = Device.objects.create(device_id="device_001") + + def test_device_str(self): + self.assertEqual(str(self.device), "device_001") + + +class EnvironmentalParameterModelTest(TestCase): + def setUp(self): + self.device = Device.objects.create(device_id="device_001") + self.env_param = EnvironmentalParameter.objects.create( + device=self.device, + temperature=25.0, + pressure=1013.25, + humidity=45.0, + air_quality=0.5, + ram_value=512.0, + system_temperature=50.0, + power_usage=10.0, + ) + + def test_environmental_parameter_creation(self): + self.assertIsInstance(self.env_param, EnvironmentalParameter) + self.assertEqual(self.env_param.device.device_id, "device_001") + self.assertEqual(self.env_param.temperature, 25.0) + self.assertEqual(self.env_param.pressure, 1013.25) + self.assertEqual(self.env_param.humidity, 45.0) + self.assertEqual(self.env_param.air_quality, 0.5) + self.assertEqual(self.env_param.ram_value, 512.0) + self.assertEqual(self.env_param.system_temperature, 50.0) + self.assertEqual(self.env_param.power_usage, 10.0) + + def test_environmental_parameter_str(self): + expected_str = "device_001 - Temp: 25.0, Pressure: 1013.25, Humidity: 45.0" + self.assertEqual(str(self.env_param), expected_str) + + +class SoundInferenceDataModelTest(TestCase): + def setUp(self): + self.device = Device.objects.create(device_id="device_001") + self.sound_data = SoundInferenceData.objects.create( + device=self.device, + inference_probability=0.95, + inference_class="Birdsong", + inferred_audio_name="birdsong_001.wav", + ) + + def test_sound_inference_data_creation(self): + self.assertIsInstance(self.sound_data, SoundInferenceData) + self.assertEqual(self.sound_data.device.device_id, "device_001") + self.assertEqual(self.sound_data.inference_probability, 0.95) + self.assertEqual(self.sound_data.inference_class, "Birdsong") + self.assertEqual(self.sound_data.inferred_audio_name, "birdsong_001.wav") + + def test_sound_inference_data_str(self): + expected_str = "device_001 - Birdsong: 0.95" + self.assertEqual(str(self.sound_data), expected_str) + + def test_verbose_name_plural(self): + self.assertEqual( + str(SoundInferenceData._meta.verbose_name_plural), "Sound Inference Data" + ) diff --git a/device_metrics/urls.py b/device_metrics/urls.py index 6a5464f..a2ac29f 100644 --- a/device_metrics/urls.py +++ b/device_metrics/urls.py @@ -1,13 +1,19 @@ -from django.urls import path, include - -from .views import ReceiveDeviceMetricsViewSet, ListDeviceMetrics - +from django.urls import include, path from rest_framework.routers import SimpleRouter +from .views import ( + EnvironmentalParameterViewSet, + ListDeviceMetrics, + ReceiveDeviceMetricsViewSet, + SoundInferenceDataViewSet, +) + router = SimpleRouter() -router.register('', ReceiveDeviceMetricsViewSet) +router.register(r"devices", ReceiveDeviceMetricsViewSet) +router.register(r"environmental-parameters", EnvironmentalParameterViewSet) +router.register(r"sound-inference-data", SoundInferenceDataViewSet) urlpatterns = [ - path('device/', ListDeviceMetrics.as_view(), name='device_device_metrics'), - path('', include(router.urls)) + path("device/", ListDeviceMetrics.as_view(), name="device_device_metrics"), + path("", include(router.urls)), ] diff --git a/device_metrics/views.py b/device_metrics/views.py index f61a7e0..7791e27 100644 --- a/device_metrics/views.py +++ b/device_metrics/views.py @@ -1,8 +1,12 @@ from rest_framework import viewsets from rest_framework.generics import ListAPIView -from .models import DeviceMetrics -from .serializers import DeviceMetricsSerializer +from .models import DeviceMetrics, EnvironmentalParameter, SoundInferenceData +from .serializers import ( + DeviceMetricsSerializer, + EnvironmentalParameterSerializer, + SoundInferenceDataSerializer, +) class ReceiveDeviceMetricsViewSet(viewsets.ModelViewSet): @@ -15,6 +19,14 @@ class ListDeviceMetrics(ListAPIView): serializer_class = DeviceMetricsSerializer def get_queryset(self): - return self.queryset.filter( - device__id=self.kwargs['pk'] - ) + return self.queryset.filter(device__id=self.kwargs["pk"]) + + +class EnvironmentalParameterViewSet(viewsets.ModelViewSet): + queryset = EnvironmentalParameter.objects.all() + serializer_class = EnvironmentalParameterSerializer + + +class SoundInferenceDataViewSet(viewsets.ModelViewSet): + queryset = SoundInferenceData.objects.all() + serializer_class = SoundInferenceDataSerializer diff --git a/devices/admin.py b/devices/admin.py index 00a409d..3b44709 100644 --- a/devices/admin.py +++ b/devices/admin.py @@ -4,7 +4,13 @@ class DeviceAdmin(admin.ModelAdmin): - list_display = ("device_id", "imei", "device_name", "phone_number", "production_stage") + list_display = ( + "device_id", + "imei", + "device_name", + "phone_number", + "production_stage", + ) admin.site.register(Device, DeviceAdmin) diff --git a/devices/apps.py b/devices/apps.py index f1dad42..b06641c 100644 --- a/devices/apps.py +++ b/devices/apps.py @@ -2,5 +2,5 @@ class DevicesConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'devices' + default_auto_field = "django.db.models.BigAutoField" + name = "devices" diff --git a/devices/forms.py b/devices/forms.py index 31550fb..eb3f92c 100644 --- a/devices/forms.py +++ b/devices/forms.py @@ -1,41 +1,66 @@ +import re + from django import forms from django.forms import ModelForm, ValidationError + from .models import Device, Location -import re class DeviceForm(ModelForm): class Meta: model = Device - fields = ['device_id', 'imei', 'device_name', 'phone_number', 'version_number', - 'production_stage', 'tags', 'metrics_url'] + fields = [ + "device_id", + "imei", + "device_name", + "phone_number", + "version_number", + "production_stage", + "tags", + "metrics_url", + ] def clean_imei(self, *args, **kwargs): - imei = self.cleaned_data.get('imei') + imei = self.cleaned_data.get("imei") if len(imei) != 15: - raise ValidationError('IMEI must be 15-digit number') + raise ValidationError("IMEI must be 15-digit number") return imei def clean_phone_number(self, *args, **kwargs): # this particular validation is a placeholder and should be updated later - phone_number = self.cleaned_data.get('phone_number') - pattern = '^(07)([0-9]{8})$' + phone_number = self.cleaned_data.get("phone_number") + pattern = "^(07)([0-9]{8})$" if not re.match(pattern, phone_number): - raise ValidationError('Please enter a valid phone number') + raise ValidationError("Please enter a valid phone number") return phone_number class LocationForm(ModelForm): class Meta: model = Location - fields = ['latitude', 'longitude', 'city', 'division', 'parish', 'village', 'category'] + fields = [ + "latitude", + "longitude", + "city", + "division", + "parish", + "village", + "category", + ] class DeviceConfigurationForm(ModelForm): class Meta: model = Device - fields = ['configured', 'mode', 'dbLevel', 'recLength', 'recInterval', 'uploadAddr'] + fields = [ + "configured", + "mode", + "dbLevel", + "recLength", + "recInterval", + "uploadAddr", + ] class UptimeDurationForm(ModelForm): - duration_weeks = forms.IntegerField() \ No newline at end of file + duration_weeks = forms.IntegerField() diff --git a/devices/models.py b/devices/models.py index a52d5b9..e12fdbd 100644 --- a/devices/models.py +++ b/devices/models.py @@ -1,17 +1,16 @@ -import uuid import calendar -from django.utils.translation import gettext_lazy as _ +import uuid +from datetime import datetime, timedelta +import pytz from django.db import models from django.urls import reverse -import pytz -from noise_dashboard.settings import TIME_ZONE - +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ from taggit.managers import TaggableManager from taggit.models import GenericUUIDTaggedItemBase, TaggedItemBase -from datetime import datetime, timedelta -from django.utils import timezone +from noise_dashboard.settings import TIME_ZONE class UUIDTaggedItem(GenericUUIDTaggedItemBase, TaggedItemBase): @@ -22,11 +21,11 @@ class Meta: class Device(models.Model): class ProductionStage(models.TextChoices): - DEPLOYED = 'Deployed', _('Deployed') - TESTING = 'Testing', _('Testing') - SHELVED = 'Shelved', _('Shelved') - MAINTENANCE = 'Maintenance', _('Maintenance') - RETIRED = 'Retired', _('Retired') + DEPLOYED = "Deployed", _("Deployed") + TESTING = "Testing", _("Testing") + SHELVED = "Shelved", _("Shelved") + MAINTENANCE = "Maintenance", _("Maintenance") + RETIRED = "Retired", _("Retired") class Configured(models.IntegerChoices): CONFIGURED = (1, _("Configured")) @@ -36,31 +35,29 @@ class Mode(models.IntegerChoices): AUTO_MODE = (1, _("Auto")) MANUAL_MODE = (2, _("Manual")) - id = models.UUIDField( - primary_key=True, - default=uuid.uuid4, - editable=False - ) + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) device_id = models.CharField(max_length=200, unique=True) imei = models.CharField(max_length=15) device_name = models.CharField(max_length=200) phone_number = models.CharField(max_length=10) version_number = models.CharField(max_length=10) production_stage = models.CharField( - max_length=50, - choices=ProductionStage.choices, - default=ProductionStage.TESTING + max_length=50, choices=ProductionStage.choices, default=ProductionStage.TESTING ) tags = TaggableManager(through=UUIDTaggedItem) metrics_url = models.URLField(max_length=255, default="http://localhost:3000/") # Configuration fields - configured = models.IntegerField(choices=Configured.choices, default=Configured.NOT_CONFIGURED) + configured = models.IntegerField( + choices=Configured.choices, default=Configured.NOT_CONFIGURED + ) mode = models.IntegerField(choices=Mode.choices, default=Mode.AUTO_MODE) dbLevel = models.IntegerField(default=50) recLength = models.IntegerField(default=10) recInterval = models.IntegerField(default=10) - uploadAddr = models.CharField(default="http://localhost:8000/audio/", max_length=100) + uploadAddr = models.CharField( + default="http://localhost:8000/audio/", max_length=100 + ) @property def lastseen(self): @@ -73,7 +70,7 @@ def lastseen(self): def get_last_metric_text_file(self): try: # Retrieve the last metricstextfile uploaded for this device - return self.metricstextfile_set.order_by('-time_uploaded').first() + return self.metricstextfile_set.order_by("-time_uploaded").first() except self.metricstextfile_set.model.DoesNotExist: # Handle the case where there are no metricstextfiles return None @@ -95,17 +92,15 @@ def get_metric_files(self): def get_absolute_url(self): return reverse("device_detail", args=[str(self.id)]) - + @property def uptime(self): """ Calculate and return the uptime of the device. """ return self.calculate_uptime(False) - - - timezone = pytz.timezone(TIME_ZONE) + timezone = pytz.timezone(TIME_ZONE) def calculate_uptime(self, audio=True): """ @@ -127,60 +122,70 @@ def calculate_uptime(self, audio=True): if not uploaded_times: return { - 'upload_gaps': [], - 'upload_gaps_len': 0, - 'uptime': 0, - 'previous_downtime': None, - 'uptime_percentages': [], + "upload_gaps": [], + "upload_gaps_len": 0, + "uptime": 0, + "previous_downtime": None, + "uptime_percentages": [], } for date_from, date_to in zip(uploaded_times[:-1], uploaded_times[1:]): gap_time = date_to - date_from - gap = round(gap_time.total_seconds() / 3600,2) + gap = round(gap_time.total_seconds() / 3600, 2) formatted_date_from = date_from.strftime("%Y-%m-%d %H:%M") formatted_date_to = date_to.strftime("%Y-%m-%d %H:%M") if gap > 3.0: - big_gaps.append((gap, f"From {formatted_date_from} to {formatted_date_to}")) + big_gaps.append( + (gap, f"From {formatted_date_from} to {formatted_date_to}") + ) if gap >= 24 and not went_online: went_online = date_to previous_downtime = f"From {formatted_date_from} to {formatted_date_to}" if not went_online: - went_online = start_time + went_online = start_time def_time = now - went_online - uptime = round(max(0, def_time.total_seconds() / 3600),2) + uptime = round(max(0, def_time.total_seconds() / 3600), 2) return { - 'upload_gaps': big_gaps, - 'upload_gaps_len': len(big_gaps), - 'uptime': uptime, - 'previous_downtime': previous_downtime, - 'uptime_percentages': zip(months, uptime_percentages) + "upload_gaps": big_gaps, + "upload_gaps_len": len(big_gaps), + "uptime": uptime, + "previous_downtime": previous_downtime, + "uptime_percentages": zip(months, uptime_percentages), } def get_uploaded_times(self, audio, now, start_time): query_set = self.recording_set if audio else self.metricstextfile_set try: - files_dates = (query_set.filter(time_uploaded__range=[start_time, now]) - .values_list('time_uploaded', flat=True) - .order_by('time_uploaded')) + files_dates = ( + query_set.filter(time_uploaded__range=[start_time, now]) + .values_list("time_uploaded", flat=True) + .order_by("time_uploaded") + ) return list(files_dates) except query_set.model.DoesNotExist: # Handle the case where there are no metricstextfiles return [] - + def get_next_12_months(self): - current_month = datetime.now().replace(day=1) # Get the first day of the current month + current_month = datetime.now().replace( + day=1 + ) # Get the first day of the current month months = [current_month] for _ in range(11): - current_month = current_month + timedelta(days=31) # Add an arbitrary number of days to move to the next month - current_month = current_month.replace(day=1) # Set the day to 1 to get the first day of the month + current_month = current_month + timedelta( + days=31 + ) # Add an arbitrary number of days to move to the next month + current_month = current_month.replace( + day=1 + ) # Set the day to 1 to get the first day of the month months.append(current_month) return months @@ -197,10 +202,19 @@ def calculate_monthly_uptime(self, uploaded_times): current_month_index = (current_month - 1) % total_months # Calculate uptime percentages - uptime_percentages = [(count / (days * 24)) * 100 for count, days in zip(uptime_per_month, [calendar.monthrange(2024, i)[1] for i in range(1, 13)])] + uptime_percentages = [ + (count / (days * 24)) * 100 + for count, days in zip( + uptime_per_month, + [calendar.monthrange(2024, i)[1] for i in range(1, 13)], + ) + ] # Shift the list so that the current month is at the beginning - uptime_percentages = uptime_percentages[current_month_index:] + uptime_percentages[:current_month_index] + uptime_percentages = ( + uptime_percentages[current_month_index:] + + uptime_percentages[:current_month_index] + ) return uptime_percentages @@ -218,7 +232,6 @@ def filter_time_by_month_and_year(self, time_formats, month, year): # Return the filtered list return filtered_time_formats - def calculate_uptime_percentage(self, duration_weeks=4): """ Calculate and return the uptime percentage of the device for a given duration. @@ -229,54 +242,45 @@ def calculate_uptime_percentage(self, duration_weeks=4): uptime_data = self.calculate_uptime(False) total_hours_in_duration = duration_weeks * 7 * 24 - return round((uptime_data['uptime'] / total_hours_in_duration) * 100, 2) - + return round((uptime_data["uptime"] / total_hours_in_duration) * 100, 2) location_category_information = { - 'A': { - 'description': 'Category A: hospital, convalescence home, sanatorium, home for the aged and ' - 'higher learning institute, conference rooms, public library, ' - 'environmental or recreational sites', - 'day_limit': 45, - 'night_limit': 35 + "A": { + "description": "Category A: hospital, convalescence home, sanatorium, home for the aged and " + "higher learning institute, conference rooms, public library, " + "environmental or recreational sites", + "day_limit": 45, + "night_limit": 35, }, - 'B': { - 'description': 'Category B: Residential buildings', - 'day_limit': 50, - 'night_limit': 35 + "B": { + "description": "Category B: Residential buildings", + "day_limit": 50, + "night_limit": 35, }, - 'C': { - 'description': 'Category C: Mixed residential (with some commercial and entertainment)', - 'day_limit': 55, - 'night_limit': 45 + "C": { + "description": "Category C: Mixed residential (with some commercial and entertainment)", + "day_limit": 55, + "night_limit": 45, }, - 'D': { - 'description': 'Category D: Residential + industry or small-scale production + commerce', - 'day_limit': 60, - 'night_limit': 50 + "D": { + "description": "Category D: Residential + industry or small-scale production + commerce", + "day_limit": 60, + "night_limit": 50, }, - 'E': { - 'description': 'Category E: Industrial', - 'day_limit': 70, - 'night_limit': 60 - } + "E": {"description": "Category E: Industrial", "day_limit": 70, "night_limit": 60}, } class Location(models.Model): class Category(models.TextChoices): - A = 'A', _(f'{location_category_information["A"]["description"]}') - B = 'B', _(f'{location_category_information["B"]["description"]}') - C = 'C', _(f'{location_category_information["C"]["description"]}') - D = 'D', _(f'{location_category_information["D"]["description"]}') - E = 'E', _(f'{location_category_information["E"]["description"]}') - - id = models.UUIDField( - primary_key=True, - default=uuid.uuid4, - editable=False - ) + A = "A", _(f'{location_category_information["A"]["description"]}') + B = "B", _(f'{location_category_information["B"]["description"]}') + C = "C", _(f'{location_category_information["C"]["description"]}') + D = "D", _(f'{location_category_information["D"]["description"]}') + E = "E", _(f'{location_category_information["E"]["description"]}') + + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) latitude = models.FloatField() longitude = models.FloatField() city = models.CharField(max_length=200) @@ -284,7 +288,9 @@ class Category(models.TextChoices): parish = models.CharField(max_length=200, blank=True, default="N/A") village = models.CharField(max_length=200, blank=True, default="N/A") device = models.OneToOneField(Device, on_delete=models.CASCADE, null=True) - category = models.CharField(max_length=50, choices=Category.choices, default=Category.E) + category = models.CharField( + max_length=50, choices=Category.choices, default=Category.E + ) @property def device_name(self): @@ -292,16 +298,16 @@ def device_name(self): @property def location_description(self): - return location_category_information[self.category]['description'] + return location_category_information[self.category]["description"] @property def night_limit(self): - return location_category_information[self.category]['night_limit'] + return location_category_information[self.category]["night_limit"] @property def day_limit(self): - return location_category_information[self.category]['day_limit'] - + return location_category_information[self.category]["day_limit"] + @property def latest_audio(self): return self.device.location_recordings.order_by("-date")[0] @@ -312,15 +318,15 @@ def latest_audio(self): @property def location_hourly_metrics(self): - return (self.device.hourlyaggregate_set - .filter(date__range=[datetime.today() - timedelta(days=2), datetime.today()]) - .order_by("-date")) + return self.device.hourlyaggregate_set.filter( + date__range=[datetime.today() - timedelta(days=2), datetime.today()] + ).order_by("-date") @property def location_daily_metrics(self): - return (self.device.dailyaggregate_set - .filter(date__range=[datetime.today() - timedelta(weeks=4), datetime.today()]) - .order_by("-date")) + return self.device.dailyaggregate_set.filter( + date__range=[datetime.today() - timedelta(weeks=4), datetime.today()] + ).order_by("-date") @property def location_recordings(self): diff --git a/devices/serializers.py b/devices/serializers.py index 6201ba0..c97d8ce 100644 --- a/devices/serializers.py +++ b/devices/serializers.py @@ -1,46 +1,64 @@ from rest_framework import serializers -from analysis.serializers import HourlyAnalysisSerializer, DailyAnalysisSerializer +from analysis.serializers import DailyAnalysisSerializer, HourlyAnalysisSerializer from recordings.models import Recording + from .models import Device, Location class DeviceSerializer(serializers.ModelSerializer): - class Meta: model = Device - fields = ['device_id'] + fields = ["device_id"] class DeviceLocationSerializer(serializers.ModelSerializer): - id = serializers.CharField(source='device_id') + id = serializers.CharField(source="device_id") # latest_metric = HourlyAnalysisSerializer(read_only=True) class Meta: model = Location fields = [ - 'id', 'latitude', 'longitude', 'city', - 'division', 'parish', 'village', 'location_description', 'day_limit', 'night_limit', + "id", + "latitude", + "longitude", + "city", + "division", + "parish", + "village", + "location_description", + "day_limit", + "night_limit", ] class LocationMetricsSerializer(serializers.ModelSerializer): - id = serializers.CharField(source='device_id') + id = serializers.CharField(source="device_id") location_hourly_metrics = HourlyAnalysisSerializer(read_only=True, many=True) location_daily_metrics = DailyAnalysisSerializer(read_only=True, many=True) + class Meta: model = Location fields = [ - 'id', 'device_name', 'location_hourly_metrics', 'location_daily_metrics' + "id", + "device_name", + "location_hourly_metrics", + "location_daily_metrics", ] class RecordingSerializer(serializers.ModelSerializer): - class Meta: model = Recording - fields = ['id', 'time_recorded','device', 'category', 'audio', 'triggering_threshold'] - read_only_fields = ['time_uploaded'] + fields = [ + "id", + "time_recorded", + "device", + "category", + "audio", + "triggering_threshold", + ] + read_only_fields = ["time_uploaded"] class LocationRecordingsSerializer(serializers.ModelSerializer): @@ -48,14 +66,10 @@ class LocationRecordingsSerializer(serializers.ModelSerializer): class Meta: model = Location - fields = [ - 'id', 'location_recordings' - ] + fields = ["id", "location_recordings"] class DeviceConfigSerializer(serializers.ModelSerializer): - class Meta: model = Device - fields = ['configured', 'device_id', - 'dbLevel', 'recLength', 'uploadAddr'] + fields = ["configured", "device_id", "dbLevel", "recLength", "uploadAddr"] diff --git a/devices/tasks.py b/devices/tasks.py index a7a682f..bdc4cd0 100644 --- a/devices/tasks.py +++ b/devices/tasks.py @@ -1,11 +1,11 @@ - -from celery.decorators import task import smtplib from email.mime.text import MIMEText -from devices.models import Device + import pytz -from noise_dashboard.settings import TIME_ZONE +from celery.decorators import task +from devices.models import Device +from noise_dashboard.settings import TIME_ZONE @task(name="send_email_alert") @@ -32,20 +32,23 @@ def send_email_alert(): def send_critical_email(device): - subject = 'Critical Uptime Alert' - message = f'The device {device.device_name} has been offline for more than 5 days.' + subject = "Critical Uptime Alert" + message = f"The device {device.device_name} has been offline for more than 5 days." send_email(subject, message) + def send_flagged_email(device): - subject = 'Flagged Uptime Alert' - message = f'The device {device.device_name} has been offline for more than 3 days.' + subject = "Flagged Uptime Alert" + message = f"The device {device.device_name} has been offline for more than 3 days." send_email(subject, message) + def send_device_off_email(device): - subject = 'Device Offline Alert' - message = f'The device {device.device_name} has been offline for 24 hours.' + subject = "Device Offline Alert" + message = f"The device {device.device_name} has been offline for 24 hours." send_email(subject, message) + def send_email(subject, message): # Modify the email sending logic based on your project's email settings # send_mail(subject, message, 'vigidaiz15@gmail.com', ['gilbertyiga15@gmail.com'], fail_silently=False,) @@ -54,18 +57,18 @@ def send_email(subject, message): password = "mfww omdz cowe ipgt" try: - server = smtplib.SMTP('smtp.gmail.com', 587) + server = smtplib.SMTP("smtp.gmail.com", 587) server.starttls() server.login(from_email, password) message = MIMEText(message) - message['From'] = from_email - message['To'] = to_email - message['Subject'] = subject + message["From"] = from_email + message["To"] = to_email + message["Subject"] = subject server.sendmail(from_email, to_email, message.as_string()) server.quit() print("Email sent!") except Exception as e: - print(f"Error sending email: {e}") \ No newline at end of file + print(f"Error sending email: {e}") diff --git a/devices/tests.py b/devices/tests.py index bd5fa35..e809997 100644 --- a/devices/tests.py +++ b/devices/tests.py @@ -1,81 +1,78 @@ +from django.contrib.auth import get_user_model from django.test import TestCase from django.urls import reverse -from django.contrib.auth import get_user_model from .models import Device class DeviceTests(TestCase): - def setUp(self) -> None: self.user = get_user_model().objects.create_user( - username='lsanyu', - email='lsanyu@email.com', - password='pass123' + username="lsanyu", email="lsanyu@email.com", password="pass123" ) self.client.force_login(self.user) - self.metrics_url = 'http://localhost:3000/d/Fmi1Q1qGk/sensor-metrics-dashboard?orgId=1&refresh=30s' + self.metrics_url = "http://localhost:3000/d/Fmi1Q1qGk/sensor-metrics-dashboard?orgId=1&refresh=30s" self.device = Device.objects.create( - device_id='SB1001', - imei='33414214123', - device_name='First sensor', - phone_number='0700443425', - version_number='1.0.0', - production_stage='Testing', - metrics_url=self.metrics_url + device_id="SB1001", + imei="33414214123", + device_name="First sensor", + phone_number="0700443425", + version_number="1.0.0", + production_stage="Testing", + metrics_url=self.metrics_url, ) def test_device_listing(self): - self.assertEqual(f'{self.device.device_id}', 'SB1001') - self.assertEqual(f'{self.device.imei}', '33414214123'), - self.assertEqual(f'{self.device.phone_number}', '0700443425') - self.assertEqual(f'{self.device.production_stage}', 'Testing') - self.assertEqual(f'{self.device.version_number}', '1.0.0') - self.assertEqual(f'{self.device.metrics_url}', self.metrics_url) + self.assertEqual(f"{self.device.device_id}", "SB1001") + self.assertEqual(f"{self.device.imei}", "33414214123"), + self.assertEqual(f"{self.device.phone_number}", "0700443425") + self.assertEqual(f"{self.device.production_stage}", "Testing") + self.assertEqual(f"{self.device.version_number}", "1.0.0") + self.assertEqual(f"{self.device.metrics_url}", self.metrics_url) def test_device_list_view(self): - response = self.client.get(reverse('device_list')) + response = self.client.get(reverse("device_list")) self.assertEqual(response.status_code, 200) - self.assertContains(response, 'SB1001') - self.assertTemplateUsed(response, 'devices/device_list.html') + self.assertContains(response, "SB1001") + self.assertTemplateUsed(response, "devices/device_list.html") def test_device_detail_view(self): response = self.client.get(self.device.get_absolute_url()) - no_response = self.client.get('/devices/122143') + no_response = self.client.get("/devices/122143") self.assertEqual(response.status_code, 200) self.assertEqual(no_response.status_code, 404) - self.assertContains(response, 'SB1001') - self.assertTemplateUsed(response, 'devices/device_detail.html') + self.assertContains(response, "SB1001") + self.assertTemplateUsed(response, "devices/device_detail.html") def test_device_edit_view(self): response = self.client.post( - reverse('edit_device', kwargs={'pk': self.device.id}), + reverse("edit_device", kwargs={"pk": self.device.id}), { - 'device_name': 'First sensor - edited', - 'imei': '123456789012345', - 'phone_number': '0771234567' - } + "device_name": "First sensor - edited", + "imei": "123456789012345", + "phone_number": "0771234567", + }, ) self.assertEqual(response.status_code, 200) - self.assertContains(response, 'First sensor - edited') + self.assertContains(response, "First sensor - edited") def test_device_edit_view_short_phone_no(self): response = self.client.post( - reverse('edit_device', kwargs={'pk': self.device.id}), - {'phone_number': '077123456'} + reverse("edit_device", kwargs={"pk": self.device.id}), + {"phone_number": "077123456"}, ) - self.assertContains(response, 'Please enter a valid phone number') + self.assertContains(response, "Please enter a valid phone number") def test_device_edit_view_wrong_phone_no(self): response = self.client.post( - reverse('edit_device', kwargs={'pk': self.device.id}), - {'phone_number': '0990123456'} + reverse("edit_device", kwargs={"pk": self.device.id}), + {"phone_number": "0990123456"}, ) - self.assertContains(response, 'Please enter a valid phone number') + self.assertContains(response, "Please enter a valid phone number") def test_device_edit_view_invalid_imei(self): response = self.client.post( - reverse('edit_device', kwargs={'pk': self.device.id}), - {'imei': '1234567890'} + reverse("edit_device", kwargs={"pk": self.device.id}), + {"imei": "1234567890"}, ) - self.assertContains(response, 'IMEI must be 15-digit number') + self.assertContains(response, "IMEI must be 15-digit number") diff --git a/devices/uptime_calculation.py b/devices/uptime_calculation.py index a10315a..8c772c4 100644 --- a/devices/uptime_calculation.py +++ b/devices/uptime_calculation.py @@ -1,8 +1,11 @@ -from .models import Device from datetime import datetime, timedelta + import pytz + from noise_dashboard.settings import TIME_ZONE +from .models import Device + timezone = pytz.timezone(TIME_ZONE) @@ -25,9 +28,11 @@ def timedelta_to_hours(delta: timedelta): def get_uploaded_times(device, audio, now, start_time): query_set = device.recording_set if audio else device.metricstextfile_set - files_dates = (query_set.filter(time_uploaded__range=[start_time, now]) - .values_list('time_uploaded', flat=True) - .order_by('-time_uploaded')) + files_dates = ( + query_set.filter(time_uploaded__range=[start_time, now]) + .values_list("time_uploaded", flat=True) + .order_by("-time_uploaded") + ) return list(files_dates) @@ -37,11 +42,7 @@ def get_gaps(uploaded_times, now, start_time): previous_downtime = None if not uploaded_times: - return { - 'big_gaps': [], - 'uptime': 0, - 'previous_downtime': None - } + return {"big_gaps": [], "uptime": 0, "previous_downtime": None} for date_from, date_to in zip(uploaded_times[:-1], uploaded_times[1:]): gap = timedelta_to_hours(date_to - date_from) @@ -57,9 +58,9 @@ def get_gaps(uploaded_times, now, start_time): went_online = start_time uptime = max(0, timedelta_to_hours(now - went_online)) - + return { - 'upload_gaps': big_gaps, - 'uptime': uptime, - 'previous_downtime': previous_downtime + "upload_gaps": big_gaps, + "uptime": uptime, + "previous_downtime": previous_downtime, } diff --git a/devices/urls.py b/devices/urls.py index 6464948..5812786 100644 --- a/devices/urls.py +++ b/devices/urls.py @@ -1,29 +1,45 @@ -from django.urls import path, include +from django.urls import include, path +from rest_framework.routers import SimpleRouter from .views import ( - DeviceListView, DeviceDetailView, DeviceCreateView, DeviceRecordingsAPIView, DeviceUpdateView, - DeviceLocationListAPIView, DeviceConfigurationUpdateView, - DeviceConfigurationViewSet, LocationCreateView, LocationUpdateView, - LocationMetricsViewSet, LocationRecordingsViewSet, UptimeAPIView + DeviceConfigurationUpdateView, + DeviceConfigurationViewSet, + DeviceCreateView, + DeviceDetailView, + DeviceListView, + DeviceLocationListAPIView, + DeviceRecordingsAPIView, + DeviceUpdateView, + LocationCreateView, + LocationMetricsViewSet, + LocationRecordingsViewSet, + LocationUpdateView, + UptimeAPIView, ) -from rest_framework.routers import SimpleRouter - router = SimpleRouter() -router.register('config', DeviceConfigurationViewSet) -router.register('location_metrics', LocationMetricsViewSet) -router.register('location_recordings', LocationRecordingsViewSet) +router.register("config", DeviceConfigurationViewSet) +router.register("location_metrics", LocationMetricsViewSet) +router.register("location_recordings", LocationRecordingsViewSet) urlpatterns = [ - path('', DeviceListView.as_view(), name='device_list'), - path('locations/', DeviceLocationListAPIView.as_view(), name='device_locations'), - path('', DeviceDetailView.as_view(), name='device_detail'), - path('create_device/', DeviceCreateView.as_view(), name='create_device'), - path('/edit/', DeviceUpdateView.as_view(), name='edit_device'), - path('/config_edit', DeviceConfigurationUpdateView.as_view(), name='edit_config'), - path('create_location', LocationCreateView.as_view(), name='create_location'), - path('/edit_location', LocationUpdateView.as_view(), name='edit_location'), - path('uptime/', UptimeAPIView.as_view(), name='uptime_info'), - path('device//recordings/', DeviceRecordingsAPIView.as_view(), name='device_recordings_api'), - path('', include(router.urls)) + path("", DeviceListView.as_view(), name="device_list"), + path("locations/", DeviceLocationListAPIView.as_view(), name="device_locations"), + path("", DeviceDetailView.as_view(), name="device_detail"), + path("create_device/", DeviceCreateView.as_view(), name="create_device"), + path("/edit/", DeviceUpdateView.as_view(), name="edit_device"), + path( + "/config_edit", + DeviceConfigurationUpdateView.as_view(), + name="edit_config", + ), + path("create_location", LocationCreateView.as_view(), name="create_location"), + path("/edit_location", LocationUpdateView.as_view(), name="edit_location"), + path("uptime/", UptimeAPIView.as_view(), name="uptime_info"), + path( + "device//recordings/", + DeviceRecordingsAPIView.as_view(), + name="device_recordings_api", + ), + path("", include(router.urls)), ] diff --git a/devices/views.py b/devices/views.py index 2f86be0..03d1645 100644 --- a/devices/views.py +++ b/devices/views.py @@ -1,23 +1,26 @@ +from django.contrib.auth.mixins import LoginRequiredMixin from django.shortcuts import get_object_or_404 -from django.urls import reverse -from django.views.generic import ListView, DetailView -from django.views.generic.edit import UpdateView, CreateView from django.template.response import TemplateResponse - -from recordings.models import Recording -from .models import Device, Location -from .uptime_calculation import calculate_uptime -from .serializers import ( - DeviceLocationSerializer, DeviceConfigSerializer, - LocationMetricsSerializer, LocationRecordingsSerializer, RecordingSerializer - ) +from django.urls import reverse +from django.views.generic import DetailView, ListView +from django.views.generic.edit import CreateView, UpdateView from rest_framework import viewsets from rest_framework.generics import ListAPIView -from rest_framework.views import APIView from rest_framework.response import Response -from django.contrib.auth.mixins import LoginRequiredMixin +from rest_framework.views import APIView -from .forms import DeviceForm, DeviceConfigurationForm +from recordings.models import Recording + +from .forms import DeviceConfigurationForm, DeviceForm +from .models import Device, Location +from .serializers import ( + DeviceConfigSerializer, + DeviceLocationSerializer, + LocationMetricsSerializer, + LocationRecordingsSerializer, + RecordingSerializer, +) +from .uptime_calculation import calculate_uptime class DeviceLocationListAPIView(ListAPIView): @@ -27,90 +30,100 @@ class DeviceLocationListAPIView(ListAPIView): class DeviceListView(LoginRequiredMixin, ListView): model = Device - context_object_name = 'device_list' - template_name = 'devices/device_list.html' + context_object_name = "device_list" + template_name = "devices/device_list.html" class LocationMetricsViewSet(viewsets.ModelViewSet): serializer_class = LocationMetricsSerializer queryset = Location.objects.all() - lookup_field = 'device' + lookup_field = "device" class LocationRecordingsViewSet(viewsets.ModelViewSet): serializer_class = LocationRecordingsSerializer queryset = Location.objects.all() - lookup_field = 'id' + lookup_field = "id" class DeviceDetailView(LoginRequiredMixin, DetailView): model = Device - context_object_name = 'device' - template_name = 'devices/device_detail.html' + context_object_name = "device" + template_name = "devices/device_detail.html" class DeviceCreateView(LoginRequiredMixin, CreateView): model = Device form_class = DeviceForm - success_url = '/devices/' - template_name = 'devices/create_device.html' + success_url = "/devices/" + template_name = "devices/create_device.html" class DeviceUpdateView(LoginRequiredMixin, UpdateView): model = Device form_class = DeviceForm - success_url = '/devices/' - template_name = 'devices/edit_device.html' + success_url = "/devices/" + template_name = "devices/edit_device.html" + class LocationCreateView(LoginRequiredMixin, CreateView): model = Location - fields = '__all__' - success_url = '/devices/' - template_name = 'devices/update_location.html' + fields = "__all__" + success_url = "/devices/" + template_name = "devices/update_location.html" class LocationUpdateView(LoginRequiredMixin, UpdateView): model = Location - fields = ['latitude', 'longitude', 'city', 'division', 'parish', 'village', 'category'] - success_url = '/devices/' - template_name = 'devices/update_location.html' + fields = [ + "latitude", + "longitude", + "city", + "division", + "parish", + "village", + "category", + ] + success_url = "/devices/" + template_name = "devices/update_location.html" class DeviceConfigurationUpdateView(LoginRequiredMixin, UpdateView): model = Device form_class = DeviceConfigurationForm - template_name = 'devices/edit_configuration.html' + template_name = "devices/edit_configuration.html" def get_success_url(self): - return reverse('device_detail', kwargs={'pk': self.object.pk}) + return reverse("device_detail", kwargs={"pk": self.object.pk}) class DeviceConfigurationViewSet(viewsets.ModelViewSet): serializer_class = DeviceConfigSerializer queryset = Device.objects.all() - lookup_field = 'imei' + lookup_field = "imei" class UptimeAPIView(APIView): - def get(self, request, *args, **kwargs): - device_id = self.kwargs['device_id'] - past_weeks = int(request.query_params.get('past_weeks', 4)) + device_id = self.kwargs["device_id"] + past_weeks = int(request.query_params.get("past_weeks", 4)) # Assuming you have a Device model, fetch the device object device = get_object_or_404(Device, device_id=device_id) # Call the calculate_uptime function with the device object uptime_based_on_audio_uploads = calculate_uptime(device, past_weeks, audio=True) - uptime_based_on_metrics_uploads = calculate_uptime(device, past_weeks, audio=False) + uptime_based_on_metrics_uploads = calculate_uptime( + device, past_weeks, audio=False + ) context = { - 'device': device, # Add the device to the context if needed in the template - 'uptime_based_on_audio_uploads': uptime_based_on_audio_uploads, - 'uptime_based_on_metrics_uploads': uptime_based_on_metrics_uploads + "device": device, # Add the device to the context if needed in the template + "uptime_based_on_audio_uploads": uptime_based_on_audio_uploads, + "uptime_based_on_metrics_uploads": uptime_based_on_metrics_uploads, } - return TemplateResponse(request, 'devices/device_detail.html', context) + return TemplateResponse(request, "devices/device_detail.html", context) class DeviceRecordingsAPIView(APIView): @@ -122,4 +135,4 @@ def get(self, request, pk): serializer = RecordingSerializer(recordings, many=True) return Response(serializer.data, status=200) except Device.DoesNotExist: - return Response({'error': 'Device not found'}, status=404) \ No newline at end of file + return Response({"error": "Device not found"}, status=404) diff --git a/manage.py b/manage.py index 1e3713a..686a2de 100755 --- a/manage.py +++ b/manage.py @@ -6,7 +6,7 @@ def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'noise_dashboard.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "noise_dashboard.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +18,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/mqtt/publish.py b/mqtt/publish.py index f2d00f8..44b0ea5 100644 --- a/mqtt/publish.py +++ b/mqtt/publish.py @@ -1,11 +1,11 @@ -import os import json +import os import paho.mqtt.client as mqtt def publish_device_configuration(device_configuration, device_imei): - mqtt_broker = os.environ['MOSQUITTO_URL'] + mqtt_broker = os.environ["MOSQUITTO_URL"] client = mqtt.Client("SB_Server_Publish") client.connect(mqtt_broker) client.publish(f"sb/sensor/{device_imei}", json.dumps(device_configuration)) diff --git a/mqtt/subscribe.py b/mqtt/subscribe.py index 21f87a5..00039a0 100644 --- a/mqtt/subscribe.py +++ b/mqtt/subscribe.py @@ -1,20 +1,19 @@ import json import os - -from dotenv import load_dotenv - from json import JSONDecodeError -from noise_sensors_monitoring.repository.dynamodbrepo import DynamodbRepo -from noise_sensors_monitoring.use_cases.sensor_reading import add_new_sensor_reading -from noise_sensors_monitoring.requests.sensor_reading import build_sensor_reading_request +from dotenv import load_dotenv -from noise_sensors_monitoring.requests.device_config import build_device_config_request +from mqtt.publish import publish_device_configuration from noise_sensors_monitoring.repository.devices_repo import DevicesRepo +from noise_sensors_monitoring.repository.dynamodbrepo import DynamodbRepo from noise_sensors_monitoring.repository.in_memory_repo import InMemoryRepo +from noise_sensors_monitoring.requests.device_config import build_device_config_request +from noise_sensors_monitoring.requests.sensor_reading import ( + build_sensor_reading_request, +) from noise_sensors_monitoring.use_cases.devices_config import get_device_config - -from mqtt.publish import publish_device_configuration +from noise_sensors_monitoring.use_cases.sensor_reading import add_new_sensor_reading load_dotenv() @@ -32,16 +31,16 @@ def on_message(_, __, message): topic = message.topic try: - message_content = json.loads(message.payload.decode('utf-8')) + message_content = json.loads(message.payload.decode("utf-8")) print(message_content) except JSONDecodeError: print("Received Invalid json.") return - if topic == 'sb/sensor/logs': + if topic == "sb/sensor/logs": request = build_sensor_reading_request(message_content) response = add_new_sensor_reading(request, repo, in_memory_repo) print(response.value) # TODO: Use logs for this - elif topic == 'sb/sensor/configs': + elif topic == "sb/sensor/configs": request = build_device_config_request(message_content) response = get_device_config(request, devices_repo) if response: diff --git a/mqtt_client.py b/mqtt_client.py index 6db81e8..0b85b7f 100644 --- a/mqtt_client.py +++ b/mqtt_client.py @@ -2,15 +2,15 @@ import time import paho.mqtt.client as mqtt +from dotenv import load_dotenv from mqtt.subscribe import on_message -from dotenv import load_dotenv load_dotenv() TOPICS = [("sb/sensor/logs", 1), ("sb/sensor/configs", 1)] -mqttBroker = os.environ['MOSQUITTO_URL'] +mqttBroker = os.environ["MOSQUITTO_URL"] client = mqtt.Client(os.environ["MQTT_CLIENT_NAME"]) client.connect(mqttBroker) diff --git a/noise_dashboard/__init__.py b/noise_dashboard/__init__.py index d0508c4..5568b6d 100644 --- a/noise_dashboard/__init__.py +++ b/noise_dashboard/__init__.py @@ -2,5 +2,4 @@ # Django starts so that shared_task will use this app. from .celery import app as celery_app - __all__ = ("celery_app",) diff --git a/noise_dashboard/asgi.py b/noise_dashboard/asgi.py index 862a0d6..05e7e3f 100644 --- a/noise_dashboard/asgi.py +++ b/noise_dashboard/asgi.py @@ -11,6 +11,6 @@ from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'noise_dashboard.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "noise_dashboard.settings") application = get_asgi_application() diff --git a/noise_dashboard/celery.py b/noise_dashboard/celery.py index 24f6e1d..3333612 100644 --- a/noise_dashboard/celery.py +++ b/noise_dashboard/celery.py @@ -6,32 +6,32 @@ from celery.schedules import crontab # set the default Django settings module for the 'celery' program. -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'noise_dashboard.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "noise_dashboard.settings") -app = Celery('noise_dashboard') +app = Celery("noise_dashboard") # Using a string here means the worker doesn't have to serialize # the configuration object to child processes. # - namespace='CELERY' means all celery-related configuration keys # should have a `CELERY_` prefix. -app.config_from_object('django.conf:settings', namespace='CELERY') +app.config_from_object("django.conf:settings", namespace="CELERY") # Load task modules from all registered Django app configs. app.autodiscover_tasks() app.conf.beat_schedule = { - 'send_email_alert': { - 'task': 'send_email_alert', - 'schedule': crontab(minute='*'), + "send_email_alert": { + "task": "send_email_alert", + "schedule": crontab(minute="*"), }, - 'send_email_alert': { - 'task': 'send_email_alert', + "send_email_alert": { + "task": "send_email_alert", # http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html - 'schedule': crontab(0, 0), - } + "schedule": crontab(0, 0), + }, } @app.task(bind=True) def debug_task(self): - print('Request: {0!r}'.format(self.request)) \ No newline at end of file + print("Request: {0!r}".format(self.request)) diff --git a/noise_dashboard/settings.py b/noise_dashboard/settings.py index 1d94edf..d5e7f4e 100644 --- a/noise_dashboard/settings.py +++ b/noise_dashboard/settings.py @@ -10,9 +10,9 @@ https://docs.djangoproject.com/en/3.2/ref/settings/ """ import os -import dj_database_url from pathlib import Path +import dj_database_url from dotenv import load_dotenv load_dotenv() @@ -20,12 +20,12 @@ # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent -ENVIRONMENT = os.getenv('ENVIRONMENT', default='development') +ENVIRONMENT = os.getenv("ENVIRONMENT", default="development") if ENVIRONMENT == "production": SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True - STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' + STATICFILES_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" # SECURE_SSL_REDIRECT = True # SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') @@ -33,38 +33,41 @@ # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.getenv('SECRET_KEY', default="django-insecure-xf6&3^n9d1@43akuqxnyr!%v40mg3a367#oin@z=cxngue$2m!") +SECRET_KEY = os.getenv( + "SECRET_KEY", + default="django-insecure-xf6&3^n9d1@43akuqxnyr!%v40mg3a367#oin@z=cxngue$2m!", +) # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = os.getenv('DEBUG') != 'True' +DEBUG = os.getenv("DEBUG") != "True" -ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', default="localhost 127.0.0.1").split(" ") +ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", default="localhost 127.0.0.1").split(" ") # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", # Third-party - 'django_crontab', - 'crispy_forms', - 'taggit', - 'rest_framework', - 'corsheaders', - 'django_celery_beat', - + "django_crontab", + "crispy_forms", + "taggit", + "rest_framework", + "corsheaders", + "django_celery_beat", + "django_extensions", + "drf_spectacular", # Local - 'users.apps.UsersConfig', - 'pages.apps.PagesConfig', - 'devices.apps.DevicesConfig', - 'recordings.apps.RecordingsConfig', - 'device_metrics.apps.DeviceMetricsConfig', - 'analysis.apps.AnalysisConfig' + "users.apps.UsersConfig", + "pages.apps.PagesConfig", + "devices.apps.DevicesConfig", + "recordings.apps.RecordingsConfig", + "device_metrics.apps.DeviceMetricsConfig", + "analysis.apps.AnalysisConfig", ] # Daily metrics aggregation job @@ -73,56 +76,53 @@ # ] # django-crispy-forms -CRISPY_TEMPLATE_PACK = 'bootstrap4' +CRISPY_TEMPLATE_PACK = "bootstrap4" MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'corsheaders.middleware.CorsMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "corsheaders.middleware.CorsMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] CORS_ALLOW_ALL_ORIGINS = False -CORS_ALLOWED_ORIGINS = [ - 'http://localhost:3000', - 'https://noise.sunbird.ai' -] +CORS_ALLOWED_ORIGINS = ["http://localhost:3000", "https://noise.sunbird.ai"] -ROOT_URLCONF = 'noise_dashboard.urls' +ROOT_URLCONF = "noise_dashboard.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [BASE_DIR / 'templates'], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [BASE_DIR / "templates"], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'noise_dashboard.wsgi.application' +WSGI_APPLICATION = "noise_dashboard.wsgi.application" # Database # https://docs.djangoproject.com/en/3.2/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.environ.get("POSTGRES_DB", "postgres"), - 'USER': os.environ.get("POSTGRES_USER", "postgres"), - 'PASSWORD': os.environ.get("POSTGRES_PASSWORD", "postgres"), - 'HOST': 'postgres_db', - 'PORT': 5432 + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": os.environ.get("POSTGRES_DB", "postgres"), + "USER": os.environ.get("POSTGRES_USER", "postgres"), + "PASSWORD": os.environ.get("POSTGRES_PASSWORD", "postgres"), + "HOST": "postgres_db", + "PORT": 5432, } } @@ -131,25 +131,25 @@ AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] # Internationalization # https://docs.djangoproject.com/en/3.2/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'Africa/Kampala' +TIME_ZONE = "Africa/Kampala" USE_I18N = True @@ -160,9 +160,11 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.2/howto/static-files/ -STATIC_URL = '/static/' -STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static'), ] -STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') +STATIC_URL = "/static/" +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, "static"), +] +STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") STATICFILES_FINDERS = [ "django.contrib.staticfiles.finders.FileSystemFinder", "django.contrib.staticfiles.finders.AppDirectoriesFinder", @@ -171,46 +173,60 @@ # Default primary key field type # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" -AUTH_USER_MODEL = 'users.CustomUser' +AUTH_USER_MODEL = "users.CustomUser" -LOGIN_REDIRECT_URL = 'home' -LOGOUT_REDIRECT_URL = 'home' +LOGIN_REDIRECT_URL = "home" +LOGOUT_REDIRECT_URL = "home" -DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' -AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID') -AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY') -AWS_STORAGE_BUCKET_NAME = os.getenv('AWS_STORAGE_BUCKET_NAME') +DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" +AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID") +AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY") +AWS_STORAGE_BUCKET_NAME = os.getenv("AWS_STORAGE_BUCKET_NAME") AWS_QUERYSTRING_AUTH = False db_from_env = dj_database_url.config(conn_max_age=500) -DATABASES['default'].update(db_from_env) +DATABASES["default"].update(db_from_env) -if 'RDS_HOSTNAME' in os.environ: +if "RDS_HOSTNAME" in os.environ: DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.environ['RDS_DB_NAME'], - 'USER': os.environ['RDS_USERNAME'], - 'PASSWORD': os.environ['RDS_PASSWORD'], - 'HOST': os.environ['RDS_HOSTNAME'], - 'PORT': os.environ['RDS_PORT'], + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": os.environ["RDS_DB_NAME"], + "USER": os.environ["RDS_USERNAME"], + "PASSWORD": os.environ["RDS_PASSWORD"], + "HOST": os.environ["RDS_HOSTNAME"], + "PORT": os.environ["RDS_PORT"], } } # Celery settings -CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler' -CELERY_BROKER_URL = os.getenv('CELERY_BROKER',"redis://redis:6379/0") -CELERY_RESULT_BACKEND = os.getenv('CELERY_BACKEND',"redis://redis:6379/0") -CELERY_ACCEPT_CONTENT = ['application/json'] -CELERY_TASK_SERIALIZER = 'json' -CELERY_RESULT_SERIALIZER = 'json' +CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler" +CELERY_BROKER_URL = os.getenv("CELERY_BROKER", "redis://redis:6379/0") +CELERY_RESULT_BACKEND = os.getenv("CELERY_BACKEND", "redis://redis:6379/0") +CELERY_ACCEPT_CONTENT = ["application/json"] +CELERY_TASK_SERIALIZER = "json" +CELERY_RESULT_SERIALIZER = "json" CELERY_TIMEZONE = TIME_ZONE # email settings. -EMAIL_BACKEND = 'django.core.mail.backends.smpt.EmailBackend' -EMAIL_HOST = 'smtp.gmail.com' +EMAIL_BACKEND = "django.core.mail.backends.smpt.EmailBackend" +EMAIL_HOST = "smtp.gmail.com" EMAIL_USE_TLS = True EMAIL_PORT = 587 EMAIL_HOST_USER = os.getenv("EMAIL_ADDRESS", "vigidaiz15@gmail.com") -EMAIL_HOST_PASSWORD = os.getenv("EMAIL_PASSWORD", "mfww omdz cowe ipgt") \ No newline at end of file +EMAIL_HOST_PASSWORD = os.getenv("EMAIL_PASSWORD", "mfww omdz cowe ipgt") + +REST_FRAMEWORK = { + # YOUR SETTINGS + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "PAGE_SIZE": 25, # Number of items per page +} + +SPECTACULAR_SETTINGS = { + "TITLE": "Sunbird Noise Monitoring API", + "DESCRIPTION": "A backend system for monitoring sound sensors.", + "VERSION": "1.0.0", + "SERVE_INCLUDE_SCHEMA": False, +} diff --git a/noise_dashboard/urls.py b/noise_dashboard/urls.py index 38bc3e8..87b4eb1 100644 --- a/noise_dashboard/urls.py +++ b/noise_dashboard/urls.py @@ -13,23 +13,29 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ -from django.contrib import admin -from django.urls import path, include from django.conf import settings from django.conf.urls.static import static +from django.contrib import admin +from django.urls import include, path +from drf_spectacular.views import ( + SpectacularAPIView, + SpectacularRedocView, + SpectacularSwaggerView, +) urlpatterns = [ # Django admin - path('admin/', admin.site.urls), - + path("admin/", admin.site.urls), # User Management - path('accounts/', include('django.contrib.auth.urls')), - + path("accounts/", include("django.contrib.auth.urls")), # Local apps - path('accounts/', include('users.urls')), - path('', include('pages.urls')), - path('devices/', include('devices.urls')), - path('audio/', include('recordings.urls')), - path('device_metrics/', include('device_metrics.urls')), - path('analysis/', include('analysis.urls')) + path("accounts/", include("users.urls")), + path("", include("pages.urls")), + path("devices/", include("devices.urls")), + path("audio/", include("recordings.urls")), + path("device_metrics/", include("device_metrics.urls")), + path("analysis/", include("analysis.urls")), + path("api/schema/", SpectacularAPIView.as_view(), name="schema"), + path("api/docs/", SpectacularSwaggerView.as_view(url_name="schema"), name="docs"), + path("api/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/noise_dashboard/wsgi.py b/noise_dashboard/wsgi.py index 1a5cb47..d3e707f 100644 --- a/noise_dashboard/wsgi.py +++ b/noise_dashboard/wsgi.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'noise_dashboard.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "noise_dashboard.settings") application = get_wsgi_application() diff --git a/noise_sensors_monitoring/domain/sensor.py b/noise_sensors_monitoring/domain/sensor.py index affa479..f50aceb 100644 --- a/noise_sensors_monitoring/domain/sensor.py +++ b/noise_sensors_monitoring/domain/sensor.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass, asdict +from dataclasses import asdict, dataclass from typing import Dict diff --git a/noise_sensors_monitoring/repository/devices_repo.py b/noise_sensors_monitoring/repository/devices_repo.py index b9ab0d9..7d36e40 100644 --- a/noise_sensors_monitoring/repository/devices_repo.py +++ b/noise_sensors_monitoring/repository/devices_repo.py @@ -1,11 +1,11 @@ import os -from dotenv import load_dotenv import requests +from dotenv import load_dotenv load_dotenv() -BASE_URL = os.getenv('HTTP_APP_HOST') +BASE_URL = os.getenv("HTTP_APP_HOST") class DevicesRepo: @@ -22,6 +22,4 @@ def get_device_configuration_by_imei(self, imei_dict): response = requests.request("GET", url) if response.status_code == 200: return response.json() - return { - "error": response.status_code - } + return {"error": response.status_code} diff --git a/noise_sensors_monitoring/repository/dynamodbrepo.py b/noise_sensors_monitoring/repository/dynamodbrepo.py index 6004dcb..1021404 100644 --- a/noise_sensors_monitoring/repository/dynamodbrepo.py +++ b/noise_sensors_monitoring/repository/dynamodbrepo.py @@ -1,39 +1,46 @@ +import json +import os from datetime import datetime +from decimal import Decimal from typing import Dict + import boto3 -from noise_sensors_monitoring.domain.sensor import Sensor -from noise_sensors_monitoring.repository.time_series_repo_interface import SensorReadingRepo from dotenv import load_dotenv -from decimal import Decimal -import os -import json + +from noise_sensors_monitoring.domain.sensor import Sensor +from noise_sensors_monitoring.repository.time_series_repo_interface import ( + SensorReadingRepo, +) load_dotenv() -ACCESS_KEY_ID = os.getenv('DYNAMO_ACCESS_KEY_ID') -SECRET_ACCESS_KEY = os.getenv('DYNAMO_SECRET_ACCESS_KEY') -dynamodb = boto3.resource('dynamodb', aws_access_key_id=ACCESS_KEY_ID, aws_secret_access_key=SECRET_ACCESS_KEY, - region_name='eu-west-1') -table = dynamodb.Table('sensor-metrics') +ACCESS_KEY_ID = os.getenv("DYNAMO_ACCESS_KEY_ID") +SECRET_ACCESS_KEY = os.getenv("DYNAMO_SECRET_ACCESS_KEY") +dynamodb = boto3.resource( + "dynamodb", + aws_access_key_id=ACCESS_KEY_ID, + aws_secret_access_key=SECRET_ACCESS_KEY, + region_name="eu-west-1", +) +table = dynamodb.Table("sensor-metrics") class DynamodbRepo(SensorReadingRepo): - def __init__(self): pass def add_new_sensor_reading(self, sensor_reading: Dict): sensor = Sensor.from_dict(sensor_reading) data = { - 'deviceId': sensor.deviceId, - 'pV': sensor.pV, - 'bV': sensor.bV, - 'signalStrength': sensor.sigStrength, - 'dataBalance': sensor.DataBalance, - 'dbLevel': sensor.dbLevel, - 'lastRecorded': sensor.LastRec, - 'lastUploaded': sensor.LastUpl, - 'date': str(datetime.now()) + "deviceId": sensor.deviceId, + "pV": sensor.pV, + "bV": sensor.bV, + "signalStrength": sensor.sigStrength, + "dataBalance": sensor.DataBalance, + "dbLevel": sensor.dbLevel, + "lastRecorded": sensor.LastRec, + "lastUploaded": sensor.LastUpl, + "date": str(datetime.now()), } table.put_item(Item=json.loads(json.dumps(data), parse_float=Decimal)) diff --git a/noise_sensors_monitoring/repository/in_memory_repo.py b/noise_sensors_monitoring/repository/in_memory_repo.py index fdc1c1a..ce6b425 100644 --- a/noise_sensors_monitoring/repository/in_memory_repo.py +++ b/noise_sensors_monitoring/repository/in_memory_repo.py @@ -1,18 +1,20 @@ -import os import json +import os +from typing import Dict, List + import requests from dotenv import load_dotenv -from noise_sensors_monitoring.domain.sensor import Sensor, AggregateSensorMetric -from noise_sensors_monitoring.repository.time_series_repo_interface import SensorReadingRepo -from typing import Dict, List +from noise_sensors_monitoring.domain.sensor import AggregateSensorMetric, Sensor +from noise_sensors_monitoring.repository.time_series_repo_interface import ( + SensorReadingRepo, +) load_dotenv() -BASE_URL = os.getenv('HTTP_APP_HOST') +BASE_URL = os.getenv("HTTP_APP_HOST") class InMemoryRepo(SensorReadingRepo): - def __init__(self): self.metrics: Dict[str, List[Sensor]] = {} @@ -52,27 +54,27 @@ def calculate_aggregate_stats(self, device_id: str) -> AggregateSensorMetric: db_avg //= len(sensor_metrics) last_sensor = sensor_metrics[-1] - return AggregateSensorMetric.from_dict({ - "device": device_id, - "db_level": last_sensor.dbLevel, - "avg_db_level": db_avg, - "max_db_level": max_db_level, - "no_of_exceedances": exceedances, - "sig_strength": last_sensor.sigStrength, - "last_rec": last_sensor.LastRec, - "last_upl": last_sensor.LastUpl, - "panel_voltage": last_sensor.pV, - "battery_voltage": last_sensor.bV, - "data_balance": last_sensor.DataBalance - }) + return AggregateSensorMetric.from_dict( + { + "device": device_id, + "db_level": last_sensor.dbLevel, + "avg_db_level": db_avg, + "max_db_level": max_db_level, + "no_of_exceedances": exceedances, + "sig_strength": last_sensor.sigStrength, + "last_rec": last_sensor.LastRec, + "last_upl": last_sensor.LastUpl, + "panel_voltage": last_sensor.pV, + "battery_voltage": last_sensor.bV, + "data_balance": last_sensor.DataBalance, + } + ) @staticmethod def send_data(aggregate_metric: AggregateSensorMetric): url = f"{BASE_URL}/device_metrics/" payload = json.dumps(aggregate_metric.to_dict()) - headers = { - 'Content-Type': 'application/json' - } + headers = {"Content-Type": "application/json"} requests.request("POST", url, headers=headers, data=payload) def get_latest_sensor_reading(self) -> Sensor: diff --git a/noise_sensors_monitoring/repository/influx_db_repo.py b/noise_sensors_monitoring/repository/influx_db_repo.py index 86e25a2..2aef458 100644 --- a/noise_sensors_monitoring/repository/influx_db_repo.py +++ b/noise_sensors_monitoring/repository/influx_db_repo.py @@ -1,20 +1,21 @@ -from noise_sensors_monitoring.repository.time_series_repo_interface import SensorReadingRepo -from noise_sensors_monitoring.domain.sensor import Sensor +from datetime import datetime +from typing import Dict from influxdb_client import InfluxDBClient, Point, WritePrecision from influxdb_client.client.write_api import ASYNCHRONOUS, SYNCHRONOUS -from datetime import datetime -from typing import Dict +from noise_sensors_monitoring.domain.sensor import Sensor +from noise_sensors_monitoring.repository.time_series_repo_interface import ( + SensorReadingRepo, +) class InfluxDBRepo(SensorReadingRepo): - def __init__(self, configuration): - self.db_url = configuration['influx_url'] - self.db_token = configuration['influx_token'] - self.org = configuration['influx_org'] - self.bucket = configuration['influx_bucket'] + self.db_url = configuration["influx_url"] + self.db_token = configuration["influx_token"] + self.org = configuration["influx_org"] + self.bucket = configuration["influx_bucket"] self.db_client = InfluxDBClient(url=self.db_url, token=self.db_token) self.write_api = self.db_client.write_api(write_options=SYNCHRONOUS) @@ -23,8 +24,12 @@ def write_measurement(self, measurement: str, value: int, sensor_reading: Sensor # TODO: Perhaps replace latitude and longitude with location point = Point(measurement).tag("deviceId", sensor_reading.deviceId) if sensor_reading.latitude != 0 and sensor_reading.longitude != 0: - point = point.tag("latitude", sensor_reading.latitude).tag("longitude", sensor_reading.longitude) - point = point.field(measurement.lower(), value).time(datetime.utcnow(), WritePrecision.NS) + point = point.tag("latitude", sensor_reading.latitude).tag( + "longitude", sensor_reading.longitude + ) + point = point.field(measurement.lower(), value).time( + datetime.utcnow(), WritePrecision.NS + ) try: self.write_api.write(self.bucket, self.org, point) diff --git a/noise_sensors_monitoring/repository/time_series_repo_interface.py b/noise_sensors_monitoring/repository/time_series_repo_interface.py index 83bcbb4..a38806d 100644 --- a/noise_sensors_monitoring/repository/time_series_repo_interface.py +++ b/noise_sensors_monitoring/repository/time_series_repo_interface.py @@ -1,8 +1,8 @@ -from abc import ABC -from abc import abstractmethod -from noise_sensors_monitoring.domain.sensor import Sensor +from abc import ABC, abstractmethod from typing import Dict +from noise_sensors_monitoring.domain.sensor import Sensor + class SensorReadingRepo(ABC): @abstractmethod diff --git a/noise_sensors_monitoring/requests/device_config.py b/noise_sensors_monitoring/requests/device_config.py index ceaed84..a9f1245 100644 --- a/noise_sensors_monitoring/requests/device_config.py +++ b/noise_sensors_monitoring/requests/device_config.py @@ -1,5 +1,9 @@ from typing import Dict, Optional -from noise_sensors_monitoring.requests.generic_requests import InvalidRequest, ValidRequest + +from noise_sensors_monitoring.requests.generic_requests import ( + InvalidRequest, + ValidRequest, +) def build_device_config_request(config_dict: Optional[Dict] = None): @@ -13,7 +17,9 @@ def build_device_config_request(config_dict: Optional[Dict] = None): imei = str(config_dict["imei"]) if len(imei) != 15 or not imei.isnumeric(): - invalid_req.add_error("Invalid value", "The imei value should be a 15-digit numeric value") + invalid_req.add_error( + "Invalid value", "The imei value should be a 15-digit numeric value" + ) if invalid_req.has_errors(): return invalid_req diff --git a/noise_sensors_monitoring/requests/generic_requests.py b/noise_sensors_monitoring/requests/generic_requests.py index 8dcde1b..0625eab 100644 --- a/noise_sensors_monitoring/requests/generic_requests.py +++ b/noise_sensors_monitoring/requests/generic_requests.py @@ -1,4 +1,4 @@ -from abc import abstractmethod, ABC +from abc import ABC, abstractmethod from typing import Dict, Optional diff --git a/noise_sensors_monitoring/requests/sensor_reading.py b/noise_sensors_monitoring/requests/sensor_reading.py index 7b85146..c4ccf26 100644 --- a/noise_sensors_monitoring/requests/sensor_reading.py +++ b/noise_sensors_monitoring/requests/sensor_reading.py @@ -1,8 +1,20 @@ -from typing import Optional, Dict +from typing import Dict, Optional -from noise_sensors_monitoring.requests.generic_requests import Request, InvalidRequest, ValidRequest +from noise_sensors_monitoring.requests.generic_requests import ( + InvalidRequest, + Request, + ValidRequest, +) -REQUIRED_FIELDS = ["deviceId", "dbLevel", "LastRec", "LastUpl", "pV", "bV", "sigStrength"] +REQUIRED_FIELDS = [ + "deviceId", + "dbLevel", + "LastRec", + "LastUpl", + "pV", + "bV", + "sigStrength", +] OPTIONAL_FIELDS = ["latitude", "longitude", "connected", "DataBalance"] REQUIRED_TYPES = { "deviceId": str, @@ -15,14 +27,9 @@ "pV": float, "bV": float, "sigStrength": float, - "DataBalance": float -} -TYPE_TO_WORD = { - str: "string", - int: "integer", - bool: "boolean", - float: "floating point" + "DataBalance": float, } +TYPE_TO_WORD = {str: "string", int: "integer", bool: "boolean", float: "floating point"} def build_sensor_reading_request(sensor_reading_dict: Optional[Dict] = None) -> Request: @@ -39,16 +46,22 @@ def build_sensor_reading_request(sensor_reading_dict: Optional[Dict] = None) -> for (key, value) in sensor_reading_dict.items(): if key not in REQUIRED_FIELDS and key not in OPTIONAL_FIELDS: - invalid_req.add_error("Invalid field", f"{key} is not a valid field for sensor data") + invalid_req.add_error( + "Invalid field", f"{key} is not a valid field for sensor data" + ) elif REQUIRED_TYPES[key] == float: try: value = float(value) cleaned_sensor_reading_dict[key] = value except ValueError: - invalid_req.add_error("Invalid type", f"Field '{key}' should have numeric data type.") + invalid_req.add_error( + "Invalid type", f"Field '{key}' should have numeric data type." + ) elif type(value) != REQUIRED_TYPES[key]: req_type = TYPE_TO_WORD[REQUIRED_TYPES[key]] - invalid_req.add_error("Invalid type", f"Field '{key}' should have {req_type} data type.") + invalid_req.add_error( + "Invalid type", f"Field '{key}' should have {req_type} data type." + ) else: cleaned_sensor_reading_dict[key] = value diff --git a/noise_sensors_monitoring/responses.py b/noise_sensors_monitoring/responses.py index de78b44..70f442a 100644 --- a/noise_sensors_monitoring/responses.py +++ b/noise_sensors_monitoring/responses.py @@ -8,7 +8,6 @@ class ResponseTypes: class ResponseSuccess: - def __init__(self, value=None): self.type = ResponseTypes.SUCCESS self.value = value @@ -18,7 +17,6 @@ def __bool__(self): class ResponseFailure: - def __init__(self, type_, message): self.type = type_ self.message = self._format_message(message) @@ -38,9 +36,7 @@ def value(self): def build_response_from_invalid_request(invalid_request: Request): message = "\n".join( - [ - f"{err['type']}: {err['message']}" for err in invalid_request.errors - ] + [f"{err['type']}: {err['message']}" for err in invalid_request.errors] ) return ResponseFailure(ResponseTypes.INVALID_INPUT_ERROR, message) diff --git a/noise_sensors_monitoring/use_cases/devices_config.py b/noise_sensors_monitoring/use_cases/devices_config.py index 3ea405a..8435b68 100644 --- a/noise_sensors_monitoring/use_cases/devices_config.py +++ b/noise_sensors_monitoring/use_cases/devices_config.py @@ -1,7 +1,10 @@ from noise_sensors_monitoring.repository.devices_repo import DevicesRepo from noise_sensors_monitoring.requests.generic_requests import Request from noise_sensors_monitoring.responses import ( - ResponseSuccess, ResponseFailure, ResponseTypes, build_response_from_invalid_request + ResponseFailure, + ResponseSuccess, + ResponseTypes, + build_response_from_invalid_request, ) @@ -11,6 +14,8 @@ def get_device_config(request: Request, repo: DevicesRepo): device_config = repo.get_device_configuration_by_imei(request.request_dict) if "errors" in device_config: - return ResponseFailure(ResponseTypes.INVALID_INPUT_ERROR, device_config["errors"]) + return ResponseFailure( + ResponseTypes.INVALID_INPUT_ERROR, device_config["errors"] + ) else: return ResponseSuccess(device_config) diff --git a/noise_sensors_monitoring/use_cases/sensor_reading.py b/noise_sensors_monitoring/use_cases/sensor_reading.py index 7dae719..7c48cee 100644 --- a/noise_sensors_monitoring/use_cases/sensor_reading.py +++ b/noise_sensors_monitoring/use_cases/sensor_reading.py @@ -1,13 +1,19 @@ from noise_sensors_monitoring.domain.sensor import Sensor -from noise_sensors_monitoring.repository.time_series_repo_interface import SensorReadingRepo - +from noise_sensors_monitoring.repository.time_series_repo_interface import ( + SensorReadingRepo, +) from noise_sensors_monitoring.requests.sensor_reading import Request from noise_sensors_monitoring.responses import ( - ResponseSuccess, ResponseFailure, ResponseTypes, build_response_from_invalid_request + ResponseFailure, + ResponseSuccess, + ResponseTypes, + build_response_from_invalid_request, ) -def add_new_sensor_reading(request: Request, repo: SensorReadingRepo, in_memory_repo: SensorReadingRepo): +def add_new_sensor_reading( + request: Request, repo: SensorReadingRepo, in_memory_repo: SensorReadingRepo +): if not request: return build_response_from_invalid_request(request) try: diff --git a/pages/apps.py b/pages/apps.py index cdd024b..4b6237c 100644 --- a/pages/apps.py +++ b/pages/apps.py @@ -2,5 +2,5 @@ class PagesConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'pages' + default_auto_field = "django.db.models.BigAutoField" + name = "pages" diff --git a/pages/tests.py b/pages/tests.py index fd008fc..57cddff 100644 --- a/pages/tests.py +++ b/pages/tests.py @@ -1,24 +1,20 @@ from django.test import TestCase -from django.urls import reverse, resolve +from django.urls import resolve, reverse + from .views import HomePageView + class HomepageTests(TestCase): def setUp(self): - url = reverse('home') + url = reverse("home") self.response = self.client.get(url) def test_homepage_status_code(self): self.assertEqual(self.response.status_code, 200) def test_homepage_template(self): - self.assertTemplateUsed(self.response, 'home.html') + self.assertTemplateUsed(self.response, "home.html") def test_homepage_url_resolves_homepageview(self): - view = resolve('/') - self.assertEqual( - view.func.__name__, - HomePageView.as_view().__name__ - ) - - - + view = resolve("/") + self.assertEqual(view.func.__name__, HomePageView.as_view().__name__) diff --git a/pages/urls.py b/pages/urls.py index 3551759..9dadbe0 100644 --- a/pages/urls.py +++ b/pages/urls.py @@ -1,6 +1,7 @@ from django.urls import path + from .views import HomePageView urlpatterns = [ - path('', HomePageView.as_view(), name='home'), + path("", HomePageView.as_view(), name="home"), ] diff --git a/pages/views.py b/pages/views.py index 659cc0e..4038df8 100644 --- a/pages/views.py +++ b/pages/views.py @@ -1,38 +1,39 @@ -import pytz from datetime import datetime -from django.views.generic import TemplateView +import pytz from django.contrib.auth.mixins import LoginRequiredMixin +from django.views.generic import TemplateView -from noise_dashboard.settings import TIME_ZONE from devices.models import Device +from noise_dashboard.settings import TIME_ZONE from recordings.models import Recording + class HomePageView(TemplateView): - template_name = 'home.html' + template_name = "home.html" def devices_count(self): - """ Count of all devices """ + """Count of all devices""" return Device.objects.count() def deployed_devices_count(self): - """ + """ Count of all devices with - production_stage as 'Deployed' + production_stage as 'Deployed' """ - return Device.objects.filter(production_stage='Deployed').count() + return Device.objects.filter(production_stage="Deployed").count() def recordings_count(self): - """ Count of all recordings """ + """Count of all recordings""" return Recording.objects.count() def recordings_count_today(self): - """ Count of all recordings """ + """Count of all recordings""" timezone = pytz.timezone(TIME_ZONE) today = datetime.today() today = timezone.localize(today) return Recording.objects.filter( time_recorded__year=today.year, time_recorded__month=today.month, - time_recorded__day=today.day + time_recorded__day=today.day, ).count() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..abc1647 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[tool.black] +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | env + | _build + | buck-out + | build + | dist + | migrations +)/ +''' \ No newline at end of file diff --git a/recordings/admin.py b/recordings/admin.py index 946b985..1711929 100644 --- a/recordings/admin.py +++ b/recordings/admin.py @@ -1,4 +1,5 @@ from django.contrib import admin + from .models import Recording admin.site.register(Recording) diff --git a/recordings/apps.py b/recordings/apps.py index e8c589d..8feb9ee 100644 --- a/recordings/apps.py +++ b/recordings/apps.py @@ -2,5 +2,5 @@ class RecordingsConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'recordings' + default_auto_field = "django.db.models.BigAutoField" + name = "recordings" diff --git a/recordings/models.py b/recordings/models.py index 87651c1..015e473 100644 --- a/recordings/models.py +++ b/recordings/models.py @@ -1,22 +1,18 @@ import uuid -from django.utils import timezone from django.core.files.storage import get_storage_class -from django.core.validators import ( - MinValueValidator, MaxValueValidator -) - +from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models +from django.utils import timezone from devices.models import Device - media_storage = get_storage_class()() def recording_directory(instance, filename): time_recorded = instance.time_recorded.strftime("%Y-%m-%dT%H:%M:%S") - return f'audio/{instance.device.device_id}/{instance.device.device_id}-{time_recorded}-{filename}' + return f"audio/{instance.device.device_id}/{instance.device.device_id}-{time_recorded}-{filename}" class Recording(models.Model): @@ -26,8 +22,7 @@ class Recording(models.Model): audio = models.FileField(upload_to=recording_directory) triggering_threshold = models.IntegerField(default=50) category = models.PositiveSmallIntegerField( - validators = [MinValueValidator(1), MaxValueValidator(19)], - null=True + validators=[MinValueValidator(1), MaxValueValidator(19)], null=True ) @property diff --git a/recordings/serializers.py b/recordings/serializers.py index e2c566e..b3bb0d5 100644 --- a/recordings/serializers.py +++ b/recordings/serializers.py @@ -1,23 +1,28 @@ from rest_framework import serializers -from .models import Recording + from devices.models import Device from devices.serializers import DeviceSerializer +from .models import Recording + class UploadRecordingSerializer(serializers.ModelSerializer): - device = serializers.SlugRelatedField(queryset=Device.objects.all(), - slug_field='device_id') + device = serializers.SlugRelatedField( + queryset=Device.objects.all(), slug_field="device_id" + ) class Meta: model = Recording fields = [ - 'id', 'time_recorded', 'audio', 'category', 'device', - 'triggering_threshold' + "id", + "time_recorded", + "audio", + "category", + "device", + "triggering_threshold", ] - read_only_fields = ['time_uploaded'] + read_only_fields = ["time_uploaded"] def to_representation(self, instance): - self.fields['device'] = DeviceSerializer(read_only=True) - return { - "result": "success" - } + self.fields["device"] = DeviceSerializer(read_only=True) + return {"result": "success"} diff --git a/recordings/urls.py b/recordings/urls.py index e6883f5..9f073ea 100644 --- a/recordings/urls.py +++ b/recordings/urls.py @@ -1,7 +1,8 @@ -from .views import ReceiveAudioViewSet, UpdateAudioViewSet from rest_framework.routers import SimpleRouter +from .views import ReceiveAudioViewSet, UpdateAudioViewSet + router = SimpleRouter() -router.register('', ReceiveAudioViewSet) -router.register('update', UpdateAudioViewSet) +router.register("", ReceiveAudioViewSet) +router.register("update", UpdateAudioViewSet) urlpatterns = router.urls diff --git a/recordings/views.py b/recordings/views.py index cfc4cb2..43a1eaf 100644 --- a/recordings/views.py +++ b/recordings/views.py @@ -1,21 +1,23 @@ import contextlib -from devices.models import Device -from rest_framework import viewsets, parsers + +from rest_framework import parsers, viewsets from rest_framework.response import Response +from devices.models import Device +from devices.serializers import RecordingSerializer + from .models import Recording from .serializers import UploadRecordingSerializer -from devices.serializers import RecordingSerializer class UpdateAudioViewSet(viewsets.ModelViewSet): queryset = Recording.objects.all() serializer_class = RecordingSerializer - http_method_names = ['patch'] - lookup_field = 'id' + http_method_names = ["patch"] + lookup_field = "id" def partial_update(self, request, *args, **kwargs): - instance = self.queryset.get(id=kwargs.get('id')) + instance = self.queryset.get(id=kwargs.get("id")) serializer = self.serializer_class(instance, data=request.data, partial=True) serializer.is_valid(raise_exception=True) serializer.save() @@ -26,7 +28,7 @@ class ReceiveAudioViewSet(viewsets.ModelViewSet): queryset = Recording.objects.all() serializer_class = UploadRecordingSerializer parser_classes = [parsers.MultiPartParser] - http_method_names = ['post'] + http_method_names = ["post"] # def perform_create(self, serializer): # if device_id := self.request.data.get('device', None): diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..614c34d --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,3 @@ +black==22.3.0 +isort==5.12.0 +flake8==6.1.0 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 801134e..5bbf3ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,11 +2,9 @@ amqp==2.6.1 asgiref==3.4.1 attrs==21.2.0 autopep8==1.5.7 -backports.zoneinfo==0.2.1 billiard==3.6.4.0 boto3==1.18.29 botocore==1.21.29 -celery==4.4.7 certifi==2021.5.30 charset-normalizer==2.0.7 click==8.1.7 @@ -21,6 +19,8 @@ django-celery-beat==2.1.0 django-cors-headers==3.11.0 django-crispy-forms==1.12.0 django-crontab==0.7.1 +django-extensions==3.2.3 +drf-spectacular==0.27.2 django-storages==1.11.1 django-taggit==1.5.1 django-timezone-field==4.2.3 @@ -31,10 +31,9 @@ influxdb-client==1.18.0 iniconfig==1.1.1 jmespath==0.10.0 kombu==4.6.11 -numpy==1.22.3 +pandas==2.0.3 packaging==20.9 paho-mqtt==1.5.1 -pandas==2.0.3 pluggy==0.13.1 prompt-toolkit==3.0.43 psycopg2-binary==2.9.1 diff --git a/tests/domain/test_sensor.py b/tests/domain/test_sensor.py index ad1b7f1..a9aa28d 100644 --- a/tests/domain/test_sensor.py +++ b/tests/domain/test_sensor.py @@ -13,7 +13,7 @@ def test_sensor_model_init(): LastRec=3, LastUpl=2, sigStrength=26, - DataBalance=56.0 + DataBalance=56.0, ) assert sensor.deviceId == "SB1001" diff --git a/tests/requests/test_sensor_reading.py b/tests/requests/test_sensor_reading.py index 0dd0784..037fce1 100644 --- a/tests/requests/test_sensor_reading.py +++ b/tests/requests/test_sensor_reading.py @@ -1,4 +1,6 @@ -from noise_sensors_monitoring.requests.sensor_reading import build_sensor_reading_request +from noise_sensors_monitoring.requests.sensor_reading import ( + build_sensor_reading_request, +) def test_build_request_without_data(): @@ -11,10 +13,7 @@ def test_build_request_without_data(): def test_build_request_without_all_fields(): - request = build_sensor_reading_request({ - "deviceId": "SB1001", - "dbLevel": 76 - }) + request = build_sensor_reading_request({"deviceId": "SB1001", "dbLevel": 76}) assert bool(request) is False assert request.has_errors() @@ -23,20 +22,22 @@ def test_build_request_without_all_fields(): def test_build_request_with_invalid_fields(): - request = build_sensor_reading_request({ - "deviceId": "SB1001", - "dbLevel": 76, - "connected": True, - "longitude": 1.034, - "latitude": 0.564, - "bV": 30, - "pV": 30, - "LastRec": 3, - "LastUpl": 2, - "sigStrength": 26, - "randomField": 26, - "DataBalance": 67.0 - }) + request = build_sensor_reading_request( + { + "deviceId": "SB1001", + "dbLevel": 76, + "connected": True, + "longitude": 1.034, + "latitude": 0.564, + "bV": 30, + "pV": 30, + "LastRec": 3, + "LastUpl": 2, + "sigStrength": 26, + "randomField": 26, + "DataBalance": 67.0, + } + ) assert bool(request) is False assert request.has_errors() @@ -45,25 +46,30 @@ def test_build_request_with_invalid_fields(): def test_build_request_with_invalid_types(): - request = build_sensor_reading_request({ - "deviceId": "SB1001", - "dbLevel": 76, - "connected": 78, - "longitude": 1.034, - "latitude": 0.564, - "bV": 30, - "pV": 30, - "LastRec": 3, - "LastUpl": 2, - "sigStrength": 26, - "DataBalance": 67.0 - }) + request = build_sensor_reading_request( + { + "deviceId": "SB1001", + "dbLevel": 76, + "connected": 78, + "longitude": 1.034, + "latitude": 0.564, + "bV": 30, + "pV": 30, + "LastRec": 3, + "LastUpl": 2, + "sigStrength": 26, + "DataBalance": 67.0, + } + ) assert bool(request) is False assert request.has_errors() assert len(request.errors) == 1 assert request.errors[0]["type"] == "Invalid type" - assert request.errors[0]["message"] == "Field 'connected' should have boolean data type." + assert ( + request.errors[0]["message"] + == "Field 'connected' should have boolean data type." + ) def test_build_valid_request(): @@ -78,7 +84,7 @@ def test_build_valid_request(): "LastRec": 3, "LastUpl": 2, "sigStrength": 26, - "DataBalance": 67.0 + "DataBalance": 67.0, } request = build_sensor_reading_request(sensor_reading) diff --git a/tests/test_responses.py b/tests/test_responses.py index acbe8d5..7f536cd 100644 --- a/tests/test_responses.py +++ b/tests/test_responses.py @@ -1,9 +1,9 @@ from noise_sensors_monitoring.requests.generic_requests import InvalidRequest from noise_sensors_monitoring.responses import ( - ResponseTypes, - ResponseSuccess, ResponseFailure, - build_response_from_invalid_request + ResponseSuccess, + ResponseTypes, + build_response_from_invalid_request, ) SUCCESS_VALUE = {"key": ["value1", "value2"]} @@ -31,15 +31,13 @@ def test_response_failure_is_false(): def test_response_failure_has_type_and_message(): - response = ResponseFailure( - GENERIC_RESPONSE_TYPE, GENERIC_RESPONSE_MESSAGE - ) + response = ResponseFailure(GENERIC_RESPONSE_TYPE, GENERIC_RESPONSE_MESSAGE) assert response.type == GENERIC_RESPONSE_TYPE assert response.message == GENERIC_RESPONSE_MESSAGE assert response.value == { "type": GENERIC_RESPONSE_TYPE, - "message": GENERIC_RESPONSE_MESSAGE + "message": GENERIC_RESPONSE_MESSAGE, } @@ -62,5 +60,7 @@ def test_response_failure_from_invalid_request(): assert bool(response) is False assert response.type == ResponseTypes.INVALID_INPUT_ERROR - assert response.message == "invalid field: randomField is not a valid field\n" \ - "invalid type: connected should be boolean data type" + assert ( + response.message == "invalid field: randomField is not a valid field\n" + "invalid type: connected should be boolean data type" + ) diff --git a/tests/use_cases/test_sensor_reading.py b/tests/use_cases/test_sensor_reading.py index aeb88d1..bb7c563 100644 --- a/tests/use_cases/test_sensor_reading.py +++ b/tests/use_cases/test_sensor_reading.py @@ -1,10 +1,18 @@ -from typing import Optional, Dict +from typing import Dict, Optional import pytest + from noise_sensors_monitoring.domain.sensor import Sensor -from noise_sensors_monitoring.repository.time_series_repo_interface import SensorReadingRepo -from noise_sensors_monitoring.use_cases.sensor_reading import add_new_sensor_reading, get_latest_sensor_reading -from noise_sensors_monitoring.requests.sensor_reading import build_sensor_reading_request +from noise_sensors_monitoring.repository.time_series_repo_interface import ( + SensorReadingRepo, +) +from noise_sensors_monitoring.requests.sensor_reading import ( + build_sensor_reading_request, +) +from noise_sensors_monitoring.use_cases.sensor_reading import ( + add_new_sensor_reading, + get_latest_sensor_reading, +) class InMemorySensorDb(SensorReadingRepo): @@ -39,7 +47,7 @@ def test_new_sensor_reading(sensor_repo): LastRec=3, LastUpl=2, sigStrength=26.0, - DataBalance=67 + DataBalance=67, ) request = build_sensor_reading_request(sensor_reading.to_dict()) diff --git a/users/admin.py b/users/admin.py index 2ea5577..06c7f41 100644 --- a/users/admin.py +++ b/users/admin.py @@ -2,8 +2,7 @@ from django.contrib.auth import get_user_model from django.contrib.auth.admin import UserAdmin -from .forms import CustomUserCreationForm, CustomUserChangeForm - +from .forms import CustomUserChangeForm, CustomUserCreationForm CustomUser = get_user_model() @@ -12,8 +11,7 @@ class CustomUserAdmin(UserAdmin): add_form = CustomUserCreationForm form = CustomUserChangeForm model = CustomUser - list_display = ['email', 'username'] + list_display = ["email", "username"] admin.site.register(CustomUser, UserAdmin) - diff --git a/users/apps.py b/users/apps.py index 72b1401..88f7b17 100644 --- a/users/apps.py +++ b/users/apps.py @@ -2,5 +2,5 @@ class UsersConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'users' + default_auto_field = "django.db.models.BigAutoField" + name = "users" diff --git a/users/forms.py b/users/forms.py index 0e1a765..10f9601 100644 --- a/users/forms.py +++ b/users/forms.py @@ -1,14 +1,14 @@ from django.contrib.auth import get_user_model -from django.contrib.auth.forms import UserCreationForm, UserChangeForm +from django.contrib.auth.forms import UserChangeForm, UserCreationForm class CustomUserCreationForm(UserCreationForm): class Meta: model = get_user_model() - fields = ('email', 'username') + fields = ("email", "username") class CustomUserChangeForm(UserChangeForm): class Meta: model = get_user_model() - fields = ('email', 'username') + fields = ("email", "username") diff --git a/users/tests.py b/users/tests.py index 3bb57bc..e6d8d53 100644 --- a/users/tests.py +++ b/users/tests.py @@ -1,21 +1,19 @@ from django.contrib.auth import get_user_model from django.test import TestCase -from django.urls import reverse, resolve +from django.urls import resolve, reverse + from .forms import CustomUserCreationForm from .views import SignupPageView class CustomUserTests(TestCase): - def test_create_user(self): User = get_user_model() user = User.objects.create_user( - username='io', - email='io@email.com', - password='testpass' + username="io", email="io@email.com", password="testpass" ) - self.assertEqual(user.username, 'io') - self.assertEqual(user.email, 'io@email.com') + self.assertEqual(user.username, "io") + self.assertEqual(user.email, "io@email.com") self.assertTrue(user.is_active) self.assertFalse(user.is_staff) self.assertFalse(user.is_superuser) @@ -23,12 +21,10 @@ def test_create_user(self): def test_create_superuser(self): User = get_user_model() admin_user = User.objects.create_superuser( - username='superadmin', - email='superadmin@email.com', - password='testpass' + username="superadmin", email="superadmin@email.com", password="testpass" ) - self.assertEqual(admin_user.username, 'superadmin') - self.assertEqual(admin_user.email, 'superadmin@email.com') + self.assertEqual(admin_user.username, "superadmin") + self.assertEqual(admin_user.email, "superadmin@email.com") self.assertTrue(admin_user.is_active) self.assertTrue(admin_user.is_staff) self.assertTrue(admin_user.is_superuser) @@ -36,24 +32,20 @@ def test_create_superuser(self): class SignupPageTests(TestCase): def setUp(self): - url = reverse('signup') + url = reverse("signup") self.response = self.client.get(url) def test_signup_template(self): self.assertEqual(self.response.status_code, 200) - self.assertTemplateUsed(self.response, 'signup.html') - self.assertContains(self.response, 'Sign Up') - self.assertNotContains( - self.response, 'Not on the page') + self.assertTemplateUsed(self.response, "signup.html") + self.assertContains(self.response, "Sign Up") + self.assertNotContains(self.response, "Not on the page") def test_signup_form(self): - form = self.response.context.get('form') + form = self.response.context.get("form") self.assertIsInstance(form, CustomUserCreationForm) - self.assertContains(self.response,'csrfmiddlewaretoken') - + self.assertContains(self.response, "csrfmiddlewaretoken") + def test_signup_view(self): - view = resolve('/accounts/signup/') - self.assertEqual( - view.func.__name__, - SignupPageView.as_view().__name__ - ) + view = resolve("/accounts/signup/") + self.assertEqual(view.func.__name__, SignupPageView.as_view().__name__) diff --git a/users/urls.py b/users/urls.py index 895292c..df7f073 100644 --- a/users/urls.py +++ b/users/urls.py @@ -1,7 +1,7 @@ from django.urls import path -from .views import SignupPageView +from .views import SignupPageView urlpatterns = [ - path('signup/', SignupPageView.as_view(), name='signup'), + path("signup/", SignupPageView.as_view(), name="signup"), ] diff --git a/users/views.py b/users/views.py index 52c4623..bb12e77 100644 --- a/users/views.py +++ b/users/views.py @@ -1,9 +1,10 @@ from django.urls import reverse_lazy from django.views import generic + from .forms import CustomUserCreationForm class SignupPageView(generic.CreateView): form_class = CustomUserCreationForm - success_url = reverse_lazy('login') - template_name = 'signup.html' + success_url = reverse_lazy("login") + template_name = "signup.html"