From 4c63e4e712124bc70e01b6c4fa42695ab7ff40ec Mon Sep 17 00:00:00 2001
From: Mattia Mancini <mancini@astron.nl>
Date: Fri, 26 Oct 2018 10:16:39 +0000
Subject: [PATCH] OSB-28: improved fields validation

---
 .../DBInterface/django_postgresql/settings.py |   4 +
 .../monitoringdb/views/controllers.py         | 292 +++++++++---------
 2 files changed, 147 insertions(+), 149 deletions(-)

diff --git a/LCU/Maintenance/DBInterface/django_postgresql/settings.py b/LCU/Maintenance/DBInterface/django_postgresql/settings.py
index ba6750a9c70..be5844ac85b 100644
--- a/LCU/Maintenance/DBInterface/django_postgresql/settings.py
+++ b/LCU/Maintenance/DBInterface/django_postgresql/settings.py
@@ -106,6 +106,10 @@ LOGGING = {
         'serializers': {
             'handlers': ['console'],
             'level': 'DEBUG',
+        },
+        'controllers': {
+            'handlers': ['console'],
+            'level': 'DEBUG'
         }
     },
 }
diff --git a/LCU/Maintenance/DBInterface/monitoringdb/views/controllers.py b/LCU/Maintenance/DBInterface/monitoringdb/views/controllers.py
index 06b580f380c..7f1c3478f57 100644
--- a/LCU/Maintenance/DBInterface/monitoringdb/views/controllers.py
+++ b/LCU/Maintenance/DBInterface/monitoringdb/views/controllers.py
@@ -1,11 +1,11 @@
 import datetime
+import logging
 from collections import OrderedDict
 from math import ceil
 
 import coreapi
 import coreschema
 from django.db.models import Count
-from django.db.models import Window, F
 from rest_framework import status
 from rest_framework.response import Response
 from rest_framework.schemas import ManualSchema
@@ -17,19 +17,96 @@ from lofar.maintenance.monitoringdb.models.rtsm import RTSMObservation
 from lofar.maintenance.monitoringdb.models.station import Station
 from lofar.maintenance.monitoringdb.models.station_test import StationTest
 
+logger = logging.getLogger(__name__)
 
-class ControllerStationOverview(APIView):
+
+def parse_date(date):
+    expected_format = '%Y-%m-%d'
+    try:
+        parsed_date = datetime.datetime.strptime(date, expected_format)
+        return parsed_date
+    except Exception as e:
+        raise ValueError('cannot parse %s with format %s - %s' % (date, expected_format, e))
+
+
+def parse_bool(boolean_value_str):
+    boolean_value_str = boolean_value_str.lower()
+    if boolean_value_str in ['t', 'true', '1']:
+        return True
+    elif boolean_value_str in ['f', 'false', '0']:
+        return False
+    else:
+        raise ValueError('%s is neither true or false' % boolean_value_str)
+
+
+class ValidableReadOnlyView(APIView):
+    """
+    Convenience APIView class to have the validation of the query parameters on a get http request
+    """
+
+    # Override this to make the schema validation work
+    fields = []
+
+    def compute_response(self):
+        raise NotImplementedError()
+
+    @property
+    def schema(self):
+        return ManualSchema(fields=self.fields)
+
+    def validate_query_parameters(self, request):
+        """
+        Validated the request parameters and stores them as fields
+        :param request: the http request to the api call
+        :type request: rest_framework.request.Request
+        :raises ValueError: if the parameter is not valid
+        :raises KeyError: if the requested parameter is missing
+        """
+        for field in self.fields:
+            if field.required and field.name not in request.query_params:
+                raise KeyError('%s parameter is missing' % field.name)
+            elif field.name not in request.query_params:
+                continue
+            else:
+                value = self.request.query_params.get(field.name)
+                if field.type:
+                    self.__setattr__(field.name, field.type(value))
+                else:
+                    self.__setattr__(field.name, value)
+                errors = field.schema.validate(self.__getattribute__(field.name))
+                for error in errors:
+                    raise ValueError(error.text)
+
+    def get(self, request):
+        try:
+            self.validate_query_parameters(request)
+        except ValueError as e:
+            return Response(status=status.HTTP_406_NOT_ACCEPTABLE,
+                            data='Please specify the correct parameters: %s' % (e,))
+        except KeyError as e:
+            return Response(status=status.HTTP_406_NOT_ACCEPTABLE,
+                            data='Please specify all the required parameters: %s' % (e,))
+
+        try:
+            response = self.compute_response()
+        except Exception as e:
+            logger.exception(e)
+            return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR,
+                            data='exception occurred: %s' % e)
+        return response
+
+
+class ControllerStationOverview(ValidableReadOnlyView):
     """
     Overview of the latest tests performed on the stations
     """
 
-    DEFAULT_STATION_GROUP = 'A'
-    DEFAULT_ONLY_ERRORS = True
-    DEFAULT_N_STATION_TESTS = 4
-    DEFAULT_N_RTSM = 4
+    station_group = 'A'
+    errors_only = 'true'
+    n_station_tests = 4
+    n_rtsm = 4
 
-    queryset = StationTest.objects.all()
-    schema = ManualSchema(fields=[
+    fields = [
         coreapi.Field(
             "station_group",
             required=False,
@@ -42,6 +119,7 @@ class ControllerStationOverview(APIView):
             "n_station_tests",
             required=False,
             location='query',
+            type=int,
             schema=coreschema.Integer(description='number of station tests to select',
                                       minimum=1)
         ),
@@ -49,6 +127,7 @@ class ControllerStationOverview(APIView):
             "n_rtsm",
             required=False,
             location='query',
+            type=int,
             schema=coreschema.Integer(description='number of station tests to select',
                                       minimum=1)
         ),
@@ -56,21 +135,16 @@ class ControllerStationOverview(APIView):
             "errors_only",
             required=False,
             location='query',
+            type=parse_bool,
             schema=coreschema.Boolean(
                 description='displays or not only the station with more than one error')
         )
     ]
-    )
 
-    def get(self, request, format=None):
-        errors_only = request.query_params.get('errors_only', self.DEFAULT_ONLY_ERRORS)
-        station_group = request.query_params.get('station_group', self.DEFAULT_STATION_GROUP)
-        n_station_tests = int(
-            request.query_params.get('n_station_tests', self.DEFAULT_N_STATION_TESTS))
-        n_rtsm = int(request.query_params.get('n_rtsm', self.DEFAULT_N_RTSM))
+    def compute_response(self):
 
         station_entities = Station.objects.all()
-        for group in station_group:
+        for group in self.station_group:
             if group is not 'A':
                 station_entities = station_entities.filter(type=group)
 
@@ -82,9 +156,9 @@ class ControllerStationOverview(APIView):
             station_payload['station_name'] = station_entity.name
 
             station_test_list = StationTest.objects.filter(
-                station__name=station_entity.name).order_by('-end_datetime')[:n_station_tests]
+                station__name=station_entity.name).order_by('-end_datetime')[:self.n_station_tests]
             rtsm_list = RTSMObservation.objects.filter(
-                station__name=station_entity.name).order_by('-end_datetime')[:n_rtsm]
+                station__name=station_entity.name).order_by('-end_datetime')[:self.n_rtsm]
 
             station_payload['station_tests'] = list()
             for station_test in station_test_list:
@@ -121,7 +195,8 @@ class ControllerStationOverview(APIView):
                 rtsm_payload['start_datetime'] = rtsm.start_datetime
                 rtsm_payload['end_datetime'] = rtsm.end_datetime
 
-                unique_modes = [item['mode'] for item in rtsm.errors_summary.values('mode').distinct()]
+                unique_modes = [item['mode'] for item in
+                                rtsm.errors_summary.values('mode').distinct()]
                 rtsm_payload['mode'] = unique_modes
                 rtsm_payload['total_component_errors'] = rtsm.errors_summary.count()
 
@@ -137,7 +212,7 @@ class ControllerStationOverview(APIView):
                 station_payload['rtsm'].append(rtsm_payload)
 
             response_payload.append(station_payload)
-        if errors_only and errors_only is not 'false':
+        if self.errors_only:
             response_payload = filter(
                 lambda station_entry:
                 len(station_entry['station_tests']) + len(station_entry['rtsm']) > 0,
@@ -146,17 +221,15 @@ class ControllerStationOverview(APIView):
         return Response(status=status.HTTP_200_OK, data=response_payload)
 
 
-class ControllerStationTestsSummary(APIView):
+class ControllerStationTestsSummary(ValidableReadOnlyView):
     """
     Overview of the latest station tests performed on the stations # lookback days before now
     """
+    station_group = 'A'
+    errors_only = 'true'
+    lookback_time = 7
 
-    DEFAULT_STATION_GROUP = 'A'
-    DEFAULT_ONLY_ERRORS = True
-    DEFAULT_LOOKBACK_TIME_IN_DAYS = 7
-
-    queryset = StationTest.objects.all()
-    schema = ManualSchema(fields=[
+    fields = [
         coreapi.Field(
             "station_group",
             required=False,
@@ -168,44 +241,22 @@ class ControllerStationTestsSummary(APIView):
             "errors_only",
             required=False,
             location='query',
+            type=parse_bool,
             schema=coreschema.Boolean(
                 description='displays or not only the station with more than one error')
         ),
         coreapi.Field(
             "lookback_time",
             required=False,
+            type=int,
             location='query',
             schema=coreschema.Integer(description='number of days from now (default 7)',
                                       minimum=1)
         )
     ]
-    )
-
-    @staticmethod
-    def parse_date(date):
-        expected_format = '%Y-%m-%d'
-        try:
-            parsed_date = datetime.datetime.strptime(date, expected_format)
-            return parsed_date
-        except Exception as e:
-            raise ValueError('cannot parse %s with format %s - %s' % (date, expected_format, e))
 
-    def validate_query_parameters(self, request):
-        self.errors_only = request.query_params.get('errors_only', self.DEFAULT_ONLY_ERRORS)
-        self.station_group = request.query_params.get('station_group', self.DEFAULT_STATION_GROUP)
-
-        self.lookback_time = datetime.timedelta(int(request.query_params.get('lookback_time',
-                                                                             self.DEFAULT_LOOKBACK_TIME_IN_DAYS)))
-
-    def get(self, request, format=None):
-        try:
-            self.validate_query_parameters(request)
-        except ValueError as e:
-            return Response(status=status.HTTP_406_NOT_ACCEPTABLE,
-                            data='Please specify the correct parameters: %s' % (e,))
-        except KeyError as e:
-            return Response(status=status.HTTP_406_NOT_ACCEPTABLE,
-                            data='Please specify all the required parameters: %s' % (e,))
+    def compute_response(self):
+        self.lookback_time = datetime.timedelta(days=self.lookback_time)
 
         station_test_list = StationTest.objects \
             .filter(start_datetime__gte=datetime.date.today() - self.lookback_time) \
@@ -246,7 +297,8 @@ class ControllerStationTestsSummary(APIView):
             station_test_payload['component_error_summary'] = component_errors_summary_dict
 
             response_payload.append(station_test_payload)
-        if self.errors_only and self.errors_only is not 'false':
+
+        if self.errors_only:
             response_payload = filter(
                 lambda station_test_entry:
                 station_test_entry['total_component_errors'] > 0,
@@ -255,15 +307,15 @@ class ControllerStationTestsSummary(APIView):
         return Response(status=status.HTTP_200_OK, data=response_payload)
 
 
-class ControllerLatestObservations(APIView):
+class ControllerLatestObservations(ValidableReadOnlyView):
     """
     Overview of the latest observations performed on the stations
     """
 
-    DEFAULT_STATION_GROUP = 'A'
-    DEFAULT_ONLY_ERRORS = True
+    station_group = 'A'
+    errors_only = 'true'
 
-    schema = ManualSchema(fields=[
+    fields = [
         coreapi.Field(
             "station_group",
             required=False,
@@ -276,6 +328,7 @@ class ControllerLatestObservations(APIView):
             "errors_only",
             required=False,
             location='query',
+            type=parse_bool,
             schema=coreschema.Boolean(
                 description='displays or not only the station with more than one error')
         ),
@@ -287,49 +340,26 @@ class ControllerLatestObservations(APIView):
                 description='select rtsm from date (ex. YYYY-MM-DD)')
         )
     ]
-    )
-
-    @staticmethod
-    def parse_date(date):
-        expected_format = '%Y-%m-%d'
-        try:
-            parsed_date = datetime.datetime.strptime(date, expected_format)
-            return parsed_date
-        except Exception as e:
-            raise ValueError('cannot parse %s with format %s - %s' % (date, expected_format, e))
-
-    def validate_query_parameters(self, request):
-        self.errors_only = request.query_params.get('errors_only', self.DEFAULT_ONLY_ERRORS)
-        self.station_group = request.query_params.get('station_group', self.DEFAULT_STATION_GROUP)
 
-        start_date = request.query_params.get('from_date')
-        self.from_date = ControllerLatestObservations.parse_date(start_date)
+    def compute_response(self):
+        self.from_date = parse_date(self.from_date)
 
-    def get(self, request, format=None):
-        try:
-            self.validate_query_parameters(request)
-        except ValueError as e:
-            return Response(status=status.HTTP_406_NOT_ACCEPTABLE,
-                            data='Please specify the date in the format YYYY-MM-DD: %s' % (e,))
-        except KeyError as e:
-            return Response(status=status.HTTP_406_NOT_ACCEPTABLE,
-                            data='Please specify both the start and the end date: %s' % (e,))
-        filtered_entities = RTSMObservation.objects\
+        filtered_entities = RTSMObservation.objects \
             .filter(start_datetime__gte=self.from_date)
         if self.station_group != 'A':
-            filtered_entities = filtered_entities\
+            filtered_entities = filtered_entities \
                 .filter(station__type=self.station_group)
         if self.errors_only:
             filtered_entities = filtered_entities.exclude(errors_summary__isnull=True)
 
-        errors_summary = filtered_entities\
+        errors_summary = filtered_entities \
             .values('observation_id',
                     'station__name',
                     'start_datetime',
                     'end_datetime',
                     'errors_summary__error_type',
-                    'errors_summary__mode')\
-            .annotate(total=Count('errors_summary__error_type'))\
+                    'errors_summary__mode') \
+            .annotate(total=Count('errors_summary__error_type')) \
             .order_by('observation_id', 'station__name')
         response = dict()
 
@@ -357,7 +387,7 @@ class ControllerLatestObservations(APIView):
 
             response[observation_id]['total_component_errors'] += total
             station_involved_summary = response[observation_id]['station_involved']
-            response[observation_id]['mode'] += [mode]\
+            response[observation_id]['mode'] += [mode] \
                 if mode not in response[observation_id]['mode'] else []
             if station_name not in station_involved_summary:
                 station_involved_summary[station_name] = OrderedDict()
@@ -369,12 +399,13 @@ class ControllerLatestObservations(APIView):
             station_involved_summary[station_name]['component_error_summary'][error_type] = total
 
         response_payload = sorted(response.values(),
-                                  key= lambda item: item['start_datetime'],
+                                  key=lambda item: item['start_datetime'],
                                   reverse=True)
 
         return Response(status=status.HTTP_200_OK, data=response_payload)
 
-class ControllerStationTestStatistics(APIView):
+
+class ControllerStationTestStatistics(ValidableReadOnlyView):
     """
 
 /views/ctrl_stationtest_statistics:
@@ -404,11 +435,10 @@ result:
  ....
 ]
     """
-    DEFAULT_STATION_GROUP = 'A'
-    DEFAULT_TEST_TYPE = 'B'
+    station_group = 'A'
+    test_type = 'B'
 
-    queryset = StationTest.objects.all()
-    schema = ManualSchema(fields=[
+    fields = [
         coreapi.Field(
             "test_type",
             required=False,
@@ -444,39 +474,11 @@ result:
             "averaging_interval",
             required=True,
             location='query',
+            type=int,
             schema=coreschema.Integer(
                 description='averaging interval in days')
         )
     ]
-    )
-
-    @staticmethod
-    def parse_date(date):
-        expected_format = '%Y-%m-%d'
-        try:
-            parsed_date = datetime.datetime.strptime(date, expected_format)
-            return parsed_date
-        except Exception as e:
-            raise ValueError('cannot parse %s with format %s - %s' % (date, expected_format, e))
-
-    def validate_query_parameters(self, request):
-        self.station_group = request.query_params.get('station_group', self.DEFAULT_STATION_GROUP)
-        if self.station_group not in ['C', 'R', 'I', 'A']:
-            raise ValueError('station_group is not one of [C,R,I,A]')
-
-        from_date = request.query_params.get('from_date')
-        self.from_date = ControllerLatestObservations.parse_date(from_date)
-
-        to_date = request.query_params.get('to_date')
-        self.to_date = ControllerLatestObservations.parse_date(to_date)
-
-        self.test_type = request.query_params.get('test_type', self.DEFAULT_TEST_TYPE)
-
-        if self.test_type not in ['R', 'S', 'B']:
-            raise ValueError('test_type is not one of [R,S,B]')
-
-        self.averaging_interval = datetime.timedelta(
-            int(request.query_params.get('averaging_interval')))
 
     def compute_errors_per_station(self, from_date, to_date, central_time, station_group,
                                    test_type):
@@ -572,25 +574,19 @@ result:
 
         return errors_per_error_type_in_bin.values()
 
-    def get(self, request, format=None):
-        try:
-            self.validate_query_parameters(request)
-        except ValueError as e:
-            return Response(status=status.HTTP_406_NOT_ACCEPTABLE,
-                            data='Error wrong format: %s' % (e,))
-        except KeyError as e:
-            return Response(status=status.HTTP_406_NOT_ACCEPTABLE,
-                            data='Please specify all the correct parameters: %s' % (e,))
-
+    def compute_response(self):
+        from_date = parse_date(self.from_date)
+        to_date = parse_date(self.to_date)
+        averaging_interval = datetime.timedelta(days=self.averaging_interval)
         response_payload = OrderedDict()
 
-        response_payload['start_date'] = self.from_date
-        response_payload['end_date'] = self.to_date
-        response_payload['averaging_interval'] = self.averaging_interval
+        response_payload['start_date'] = from_date
+        response_payload['end_date'] = to_date
+        response_payload['averaging_interval'] = averaging_interval
 
         errors_per_station = []
         errors_per_type = []
-        n_bins = int(ceil((self.to_date - self.from_date) / self.averaging_interval))
+        n_bins = int(ceil((to_date - from_date) / averaging_interval))
 
         for i in range(n_bins):
             if self.station_group is 'A':
@@ -598,16 +594,16 @@ result:
             else:
                 station_group = self.station_group
             errors_per_station += self.compute_errors_per_station(
-                from_date=self.from_date + i * self.averaging_interval,
-                to_date=self.from_date + (i + 1) * self.averaging_interval,
-                central_time=self.from_date + (i + .5) * self.averaging_interval,
+                from_date=from_date + i * averaging_interval,
+                to_date=from_date + (i + 1) * averaging_interval,
+                central_time=from_date + (i + .5) * averaging_interval,
                 station_group=station_group,
                 test_type=self.test_type)
 
             errors_per_type += self.compute_errors_per_type(
-                from_date=self.from_date + i * self.averaging_interval,
-                to_date=self.from_date + (i + 1) * self.averaging_interval,
-                central_time=self.from_date + (i + .5) * self.averaging_interval,
+                from_date=from_date + i * averaging_interval,
+                to_date=from_date + (i + 1) * averaging_interval,
+                central_time=from_date + (i + .5) * averaging_interval,
                 station_group=station_group,
                 test_type=self.test_type)
 
@@ -617,11 +613,9 @@ result:
         return Response(status=status.HTTP_200_OK, data=response_payload)
 
 
-class ControllerAllComponentErrorTypes(APIView):
-    """
-    Returns all distinct component errors
-    """
+class ControllerAllComponentErrorTypes(ValidableReadOnlyView):
+    schema = ManualSchema(description="Returns all distinct component errors", fields=[])
 
-    def get(self, request, format=None):
+    def compute_response(self):
         data = [item['type'] for item in ComponentError.objects.values('type').distinct()]
         return Response(status=status.HTTP_200_OK, data=data)
-- 
GitLab