-
Notifications
You must be signed in to change notification settings - Fork 55
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3141 from SEED-platform/Add-Create-Sensor-Reading…
…s-button Add create sensor readings button
- Loading branch information
Showing
19 changed files
with
7,846 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
# !/usr/bin/env python | ||
# encoding: utf-8 | ||
|
||
from config.settings.common import TIME_ZONE | ||
|
||
from pytz import timezone | ||
|
||
from seed.lib.mcm import reader | ||
from seed.models import Sensor | ||
|
||
|
||
class SensorsReadingsParser(object): | ||
""" | ||
This class parses and validates different details about sensor readings | ||
Import File - including sensor types & units - to be created before execution. | ||
The expected input is a csv/xlsx. The columns headers should be: | ||
- timestamp : datetime (required, unique) | ||
then any number of column where the header is a Sensor column_name, and the rows are doubles | ||
It's able to create a collection of Sensor Readings object details. | ||
""" | ||
|
||
_tz = timezone(TIME_ZONE) | ||
|
||
def __init__(self, org_id, sensor_readings_details, property_id): | ||
# defaulted to None to show it hasn't been cached yet | ||
self.sensor_readings_details = sensor_readings_details | ||
self._org_id = org_id | ||
self._property_id = property_id | ||
|
||
@classmethod | ||
def factory(cls, sensor_readings_file, org_id, property_id): | ||
"""Factory function for sensorReadingsParser | ||
:param sensor_readings_file: File | ||
:param org_id: int | ||
:param property_id: int, id of property - required if sensor data is for a specific property | ||
:return: SensorReadingsParser | ||
""" | ||
parser = reader.MCMParser(sensor_readings_file) | ||
raw_sensor_readings_data = list(parser.data) | ||
|
||
try: | ||
keys = list(raw_sensor_readings_data[0].keys()) | ||
except IndexError: | ||
raise ValueError("File has no rows") | ||
|
||
if "timestamp" not in keys: | ||
raise ValueError("File does not contain correct columns") | ||
|
||
sensor_names = keys | ||
sensor_names.remove("timestamp") | ||
|
||
sensor_readings_by_sensor_name = {sensor_name: {} for sensor_name in sensor_names} | ||
|
||
for reading in raw_sensor_readings_data: | ||
timestamp = reading["timestamp"] | ||
for sensor_name in sensor_names: | ||
sensor_readings_by_sensor_name[sensor_name][timestamp] = reading[sensor_name] | ||
|
||
return cls(org_id, sensor_readings_by_sensor_name, property_id=property_id) | ||
|
||
def get_validation_report(self): | ||
sensor_names = list(Sensor.objects.filter(sensor_property_id=self._property_id).values_list('column_name', flat=True)) | ||
|
||
result = [ | ||
{ | ||
"column_name": sensor_name, | ||
"exists": sensor_name in sensor_names, | ||
"num_readings": sum((v != "0" and v is not None) for v in readings.values()) | ||
} | ||
for sensor_name, readings in self.sensor_readings_details.items() | ||
] | ||
|
||
return result |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
177 changes: 177 additions & 0 deletions
177
seed/static/seed/js/controllers/sensor_readings_upload_modal_controller.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
angular.module('BE.seed.controller.sensor_readings_upload_modal', []) | ||
.controller('sensor_readings_upload_modal_controller', [ | ||
'$scope', | ||
'$state', | ||
'$uibModalInstance', | ||
'uiGridConstants', | ||
'filler_cycle', | ||
'dataset_service', | ||
'organization_id', | ||
'uploader_service', | ||
'view_id', | ||
'datasets', | ||
function ( | ||
$scope, | ||
$state, | ||
$uibModalInstance, | ||
uiGridConstants, | ||
filler_cycle, | ||
dataset_service, | ||
organization_id, | ||
uploader_service, | ||
view_id, | ||
datasets | ||
) { | ||
$scope.step = { | ||
number: 1 | ||
}; | ||
$scope.view_id = view_id; | ||
$scope.selectedCycle = filler_cycle; | ||
$scope.organization_id = organization_id; | ||
$scope.datasets = datasets; | ||
|
||
if (datasets.length) $scope.selectedDataset = datasets[0]; | ||
|
||
$scope.uploader = { | ||
invalid_file_contents: false, | ||
invalid_csv_extension_alert: false, | ||
progress: 0, | ||
status_message: '' | ||
}; | ||
|
||
$scope.datasetChanged = function (dataset) { | ||
// set selectedDataset to null to rerender button | ||
$scope.selectedDataset = null; | ||
$scope.selectedDataset = dataset; | ||
}; | ||
|
||
$scope.cancel = function () { | ||
// If step 2, GB import confirmation was not accepted by user, so delete file | ||
if ($scope.step.number === 2) { | ||
dataset_service.delete_file($scope.file_id).then(function (/*results*/) { | ||
$uibModalInstance.dismiss('cancel'); | ||
}); | ||
} else { | ||
$uibModalInstance.dismiss('cancel'); | ||
} | ||
}; | ||
|
||
$scope.uploaderfunc = function (event_message, file/*, progress*/) { | ||
switch (event_message) { | ||
case 'invalid_extension': | ||
$scope.$apply(function () { | ||
$scope.uploader.invalid_csv_extension_alert = true; | ||
$scope.uploader.invalid_file_contents = false; | ||
}); | ||
break; | ||
|
||
case 'upload_complete': | ||
$scope.file_id = file.file_id; | ||
$scope.filename = file.filename; | ||
show_confirmation_info(); | ||
break; | ||
} | ||
}; | ||
|
||
var saveFailure = function (error) { | ||
// Delete file and present error message | ||
|
||
// file_id source varies depending on which step the error occurs | ||
var file_id = $scope.file_id || error.config.data.file_id; | ||
dataset_service.delete_file(file_id); | ||
|
||
$scope.uploader.invalid_csv_extension_alert = false; | ||
$scope.uploader.invalid_file_contents = true; | ||
|
||
// Be sure user is back to step 1 where the error is shown and they can upload another file | ||
$scope.step.number = 1; | ||
}; | ||
|
||
var base_sensor_readings_col_defs = [{ | ||
field: 'column_name', | ||
displayName: 'column name', | ||
enableHiding: false, | ||
type: 'string' | ||
}, { | ||
field: 'exists', | ||
enableHiding: false | ||
}, { | ||
field: 'num_readings', | ||
displayName: 'number of readings', | ||
enableHiding: false | ||
} | ||
]; | ||
|
||
var successfully_imported_col_def = { | ||
field: 'successfully_imported', | ||
enableHiding: false | ||
}; | ||
|
||
var grid_rows_to_display = function (data) { | ||
return Math.min(data.length, 5); | ||
}; | ||
|
||
var show_confirmation_info = function () { | ||
uploader_service.sensor_readings_preview($scope.file_id, $scope.organization_id, $scope.view_id).then(function (result) { | ||
$scope.proposed_imports_options = { | ||
data: result, | ||
columnDefs: base_sensor_readings_col_defs, | ||
enableColumnResizing: true, | ||
enableHorizontalScrollbar: uiGridConstants.scrollbars.NEVER, | ||
enableVerticalScrollbar: result.length <= 5 ? uiGridConstants.scrollbars.NEVER : uiGridConstants.scrollbars.WHEN_NEEDED, | ||
minRowsToShow: grid_rows_to_display(result) | ||
}; | ||
|
||
var modal_element = angular.element(document.getElementsByClassName('modal-dialog')); | ||
modal_element.addClass('modal-lg'); | ||
|
||
$scope.step.number = 2; | ||
}).catch(saveFailure); | ||
}; | ||
|
||
var saveSuccess = function (progress_data) { | ||
// recheck progress in order to ensure message has been appended to progress_data | ||
uploader_service.check_progress(progress_data.progress_key).then(function (data) { | ||
$scope.uploader.status_message = 'saving complete'; | ||
$scope.uploader.progress = 100; | ||
buildImportResults(data.message); | ||
$scope.step.number = 4; | ||
}); | ||
}; | ||
|
||
var buildImportResults = function (message) { | ||
$scope.import_result_options = { | ||
data: message, | ||
columnDefs: base_sensor_readings_col_defs, | ||
enableColumnResizing: true, | ||
enableHorizontalScrollbar: uiGridConstants.scrollbars.NEVER, | ||
enableVerticalScrollbar: message.length <= 5 ? uiGridConstants.scrollbars.NEVER : uiGridConstants.scrollbars.WHEN_NEEDED, | ||
minRowsToShow: grid_rows_to_display(message) | ||
}; | ||
}; | ||
|
||
$scope.accept_sensor_readings = function () { | ||
uploader_service.save_raw_data($scope.file_id, $scope.selectedCycle).then(function (data) { | ||
$scope.uploader.status_message = 'saving data'; | ||
$scope.uploader.progress = 0; | ||
$scope.step.number = 3; | ||
|
||
var progress = _.clamp(data.progress, 0, 100); | ||
|
||
uploader_service.check_progress_loop( | ||
data.progress_key, | ||
progress, | ||
1 - (progress / 100), | ||
saveSuccess, | ||
saveFailure, // difficult to reach this as failures should be caught in confirmation step | ||
$scope.uploader | ||
); | ||
}); | ||
}; | ||
|
||
$scope.refresh_page = function () { | ||
$state.reload(); | ||
$uibModalInstance.dismiss('cancel'); | ||
}; | ||
|
||
}]); |
Oops, something went wrong.