+ + Back to site details + +
diff --git a/.gitignore b/.gitignore index 26e52bcb..1e07dd0d 100644 --- a/.gitignore +++ b/.gitignore @@ -125,3 +125,4 @@ docker-compose.yml /src/WebSDL/WebSDL/settings/settings.json settings.json settings.*.json +src/odm2/modelcache.pkl diff --git a/environment.yml b/environment.yml index 1b66bc44..3b1eb10e 100644 --- a/environment.yml +++ b/environment.yml @@ -39,4 +39,5 @@ dependencies: - django-cprofile-middleware #used for unicode_compatiblity #should confirm if this is still a dependency - - django-utils-six \ No newline at end of file + - django-utils-six + - django-formtools \ No newline at end of file diff --git a/src/WebSDL/settings/base.py b/src/WebSDL/settings/base.py index 5ebb36d6..3e02529e 100644 --- a/src/WebSDL/settings/base.py +++ b/src/WebSDL/settings/base.py @@ -51,6 +51,7 @@ 'dataloaderinterface.apps.DataloaderinterfaceConfig', 'hydroshare', 'leafpack', + 'streamwatch', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -60,7 +61,9 @@ 'widget_tweaks', 'requests', 'reset_migrations', - 'timeseries_visualization' + 'timeseries_visualization', + 'formtools', + ] MIDDLEWARE = [ diff --git a/src/dataloaderinterface/ajax.py b/src/dataloaderinterface/ajax.py index 93dc12a9..a5316342 100644 --- a/src/dataloaderinterface/ajax.py +++ b/src/dataloaderinterface/ajax.py @@ -1,18 +1,10 @@ -import json -from tkinter import E from typing import List, Dict, Any -#PRT - temporarily avoiding the Django models because there appears to be a mismatch for the foreign key -#from dataloaderinterface.models import SensorMeasurement, SiteSensor -#SiteSensor -> odm2.results -#SensorMeasurement -> odm2.resulttimeseries +from odm2 import odm2datamodels +odm2_engine = odm2datamodels.odm2_engine +models = odm2datamodels.models -from odm2 import Session -from odm2 import engine as _db_engine -from odm2.models.core import SamplingFeatures, Variables, FeatureActions, Units, Results -from odm2.models.results import TimeSeriesResultValues - -import sqlalchemy as sqla +import sqlalchemy from sqlalchemy.sql import func import pandas as pd import datetime @@ -26,35 +18,33 @@ def get_result_timeseries(request_data:Dict[str,Any]) -> str: end_date = request_data['end_date'] if 'end_date' in request_data.keys() else None if end_date: end_date = datetime.datetime.fromisoformat(end_date.rstrip('Z')) + query = sqlalchemy.select(models.TimeSeriesResultValues.valueid, + models.TimeSeriesResultValues.datavalue, + models.TimeSeriesResultValues.valuedatetime, + models.TimeSeriesResultValues.valuedatetimeutcoffset).\ + filter(models.TimeSeriesResultValues.resultid == resultid) + if interval is not None: + filter_args = [models.TimeSeriesResultValues.resultid == resultid] + subquery = sqlalchemy.select(func.max(models.TimeSeriesResultValues.valuedatetime) + - datetime.timedelta(days=interval)).\ + filter(models.TimeSeriesResultValues.resultid == resultid).scalar_subquery() + filter_args.append(models.TimeSeriesResultValues.valuedatetime >= subquery) + query = query.filter(*filter_args).\ + order_by(models.TimeSeriesResultValues.valuedatetime.asc()) + elif start_date is not None and end_date is not None: + query = query.filter(models.TimeSeriesResultValues.valuedatetime >= start_date) \ + .filter(models.TimeSeriesResultValues.valuedatetime <= end_date) \ + .order_by(models.TimeSeriesResultValues.valuedatetime.asc()) - with Session() as session: - filter_args = [TimeSeriesResultValues.resultid == resultid] - query = session.query(TimeSeriesResultValues.valueid, - TimeSeriesResultValues.datavalue, - TimeSeriesResultValues.valuedatetime, - TimeSeriesResultValues.valuedatetimeutcoffset).\ - filter(TimeSeriesResultValues.resultid == resultid) - if interval is not None: - subquery = session.query(func.max(TimeSeriesResultValues.valuedatetime) - - datetime.timedelta(days=interval)).\ - filter(TimeSeriesResultValues.resultid == resultid).scalar_subquery() - filter_args.append(TimeSeriesResultValues.valuedatetime >= subquery) - query = query.filter(*filter_args).\ - order_by(TimeSeriesResultValues.valuedatetime.asc()) - elif start_date is not None and end_date is not None: - query = query.filter(TimeSeriesResultValues.valuedatetime >= start_date) \ - .filter(TimeSeriesResultValues.valuedatetime <= end_date) \ - .order_by(TimeSeriesResultValues.valuedatetime.asc()) - - df = pd.read_sql(query.statement, session.bind) - df['valuedatetime'] = df['valuedatetime'] + pd.to_timedelta(df['valuedatetimeutcoffset'], unit='hours') - - return df.to_json(orient=orient, default_handler=str) + df = odm2_engine.read_query(query, output_format='dataframe') + df['valuedatetime'] = df['valuedatetime'] + pd.to_timedelta(df['valuedatetimeutcoffset'], unit='hours') + return df.to_json(orient=orient, default_handler=str) def get_sampling_feature_metadata(request_data:Dict[str,Any]) -> str: sampling_feature_code = str(request_data['sampling_feature_code']) - with _db_engine.connect() as connection: + #TODO - we should convert this to models instead of raw SQL + with odm2_engine.session_maker() as session: query = f"SELECT rs.resultid, rs.resultuuid, samplingfeaturecode, "\ "samplingfeaturename, sampledmediumcv, un.unitsabbreviation, "\ "un.unitsname, variablenamecv, variablecode, zlocation, " \ @@ -67,14 +57,12 @@ def get_sampling_feature_metadata(request_data:Dict[str,Any]) -> str: f"LEFT JOIN odm2.timeseriesresults AS tsr ON tsr.resultid = rs.resultid " \ f"LEFT JOIN odm2.units AS untrs ON untrs.unitsid = tsr.zlocationunitsid "\ f"WHERE sf.samplingfeaturecode = '{sampling_feature_code}'; " - df = pd.read_sql(query, connection) + df = pd.read_sql(query, session.bind) return df.to_json(orient='records', default_handler=str) def get_sampling_features(request_data:Dict[str,Any]) -> str: - with Session() as session: - query = session.query(SamplingFeatures.samplingfeatureuuid, - SamplingFeatures.samplingfeaturecode, - SamplingFeatures.samplingfeaturename).\ - order_by(SamplingFeatures.samplingfeaturecode) - df = pd.read_sql(query.statement, session.bind) - return df.to_json(orient='records', default_handler=str) \ No newline at end of file + query = sqlalchemy.select(models.SamplingFeatures.samplingfeatureuuid, + models.SamplingFeatures.samplingfeaturecode, + models.SamplingFeatures.samplingfeaturename).\ + order_by(models.SamplingFeatures.samplingfeaturecode) + return odm2_engine.read_query(query) \ No newline at end of file diff --git a/src/dataloaderinterface/static/dataloaderinterface/css/style.css b/src/dataloaderinterface/static/dataloaderinterface/css/style.css index 1ea2823f..b4608a4e 100644 --- a/src/dataloaderinterface/static/dataloaderinterface/css/style.css +++ b/src/dataloaderinterface/static/dataloaderinterface/css/style.css @@ -137,6 +137,26 @@ a:hover { color: rgb(23, 190, 207); } +.icon-dark-blue { + color: rgb(0, 53, 71); +} + +.icon-medium-orange { + color: rgb(237, 139, 22);; +} + +.icon-teal { + color: rgb(0, 94, 84); +} + +.icon-brown-green { + color: rgb(133, 128, 1); +} + +.icon-bright-red { + color: rgb(225, 82, 60); +} + .map-container { height: 410px; } @@ -980,7 +1000,7 @@ i.material-icons.site-card-icon { margin-bottom: 30px; } -#new-result-button, #new-leaf-pack-btn { +#new-result-button, #new-leaf-pack-btn, #new-streamwatch-btn { margin-left: 10px; margin-bottom: -10px; vertical-align: sub; @@ -1226,6 +1246,21 @@ div#followedSites .mdl-card__menu { width: 100px; } +.form-streamwatch .field-table input[type="number"] { + width: 100%; +} + +.form-streamwatch .num-input .num-field label { + width: 100%; +} + +.field-table div.form-field { + padding-bottom: 2px; + padding-left: 5px; + padding-right: 5px; + +} + .form-leafpack .fa { margin-right: 4px; } @@ -1241,6 +1276,12 @@ div#followedSites .mdl-card__menu { padding-bottom: 40px; } +.form-leafpack .std-row { + margin-left: 20px; + padding-left: 20px; + padding-bottom: 40px; +} + .form-leafpack .step-icon { font-size: 48px; color: #048a8a; @@ -1294,6 +1335,13 @@ td.label, p.label { font-weight: 700; } +td.block-label { + color: #66CCCC; + /* font-weight: 700; */ + font-style: italic; + font-size: 15px +} + .form-leafpack .step-number { display: inline-block; border-radius: 50%; @@ -1475,4 +1523,19 @@ i.file-icon { box-shadow: none !important; pointer-events: none !important; cursor: default !important; -} \ No newline at end of file +} + +/* Detail Page Styling */ +.block-label { + color: #66CCCC +} + +/* StreamWatch Site Detail Summary Table */ +.streamwatch-site-summary .mdl-data-table th{ + text-align: center; +} + +.streamwatch-site-summary .mdl-data-table td{ + text-align: center; +} + diff --git a/src/dataloaderinterface/static/dataloaderinterface/images/WSI_logo_blue_no_circle.png b/src/dataloaderinterface/static/dataloaderinterface/images/WSI_logo_blue_no_circle.png new file mode 100644 index 00000000..dff8a3fe Binary files /dev/null and b/src/dataloaderinterface/static/dataloaderinterface/images/WSI_logo_blue_no_circle.png differ diff --git a/src/dataloaderinterface/static/dataloaderinterface/images/WSI_logo_white_no_circle_unofficial.png b/src/dataloaderinterface/static/dataloaderinterface/images/WSI_logo_white_no_circle_unofficial.png new file mode 100644 index 00000000..2869bacf Binary files /dev/null and b/src/dataloaderinterface/static/dataloaderinterface/images/WSI_logo_white_no_circle_unofficial.png differ diff --git a/src/dataloaderinterface/static/dataloaderinterface/js/manage-streamwatch.js b/src/dataloaderinterface/static/dataloaderinterface/js/manage-streamwatch.js new file mode 100644 index 00000000..c0f80e10 --- /dev/null +++ b/src/dataloaderinterface/static/dataloaderinterface/js/manage-streamwatch.js @@ -0,0 +1,43 @@ +function defaultExperimentsMessage() { + $("tr.no-experiments-row").toggleClass("hidden", !!$("tr.leafpack-form:not(.deleted-row)").length); +} + +$(document).ready(function() { + $(".btn-delete-experiment").click(function () { + var experiment = $(this).parents('tr'); + $('#confirm-delete-experiment').data('to-delete', experiment).modal('show'); + }); + + $("#btn-confirm-delete-experiment").click(function () { + const dialog = $('#confirm-delete-experiment'); + const row = dialog.data('to-delete'); + $("#btn-confirm-delete-experiment").prop("disabled", true).text("DELETING ..."); + + const sampling_feature_code = $('#sampling_feature_code').attr('sampling_feature_code'); + + $.ajax({ + url: `/sites/${sampling_feature_code}/streamwatch/delete/`, + type: 'post', + data: { + csrfmiddlewaretoken: $('fieldset.form-fieldset input[name="csrfmiddlewaretoken"]').val(), + id: row.data('id') + } + }).done(function (data, message, xhr) { + if (xhr.status === 202) { + // Valid + row.remove(); + snackbarMsg('StreamWatch assessment has been deleted!'); + + } else if (xhr.status === 206) { + // Invalid + snackbarMsg('StreamWatch assessment could not be deleted!'); + } + }).fail(function (xhr, error) { + console.log(error); + }).always(function (response, status, xhr) { + $("#btn-confirm-delete-experiment").prop("disabled", false).text("DELETE"); + defaultExperimentsMessage(); + dialog.modal('hide'); + }); + }); +}); \ No newline at end of file diff --git a/src/dataloaderinterface/static/dataloaderinterface/js/streamwatch-create.js b/src/dataloaderinterface/static/dataloaderinterface/js/streamwatch-create.js new file mode 100644 index 00000000..29b634ad --- /dev/null +++ b/src/dataloaderinterface/static/dataloaderinterface/js/streamwatch-create.js @@ -0,0 +1,97 @@ +/** + * Created by HTAO on 6/7/2022. + */ + $(document).ready(function () { + $('.datepicker').datepicker({ + format: 'yyyy-mm-dd', + startDate: '0d' + }); + + // Validation for placement date + $('#id_placement_date, #id_retrieval_date').change(function () { + var placement = $('#id_placement_date').val(); + var retrieval = $('#id_retrieval_date').val(); + if (placement && retrieval) { + var placementDate = new Date(placement); + var retrievalDate = new Date(retrieval); + if (placementDate > retrievalDate) { + $('#id_placement_date')[0].setCustomValidity('Placement date has to be before the retrieval date.'); + } + else { + $('#id_placement_date')[0].setCustomValidity(''); + $('#id_retrieval_date')[0].setCustomValidity(''); + } + } + else if (!$(this).val()) { + this.setCustomValidity('Please fill out this field.'); + } + }); + + favorite =[]; + //totalForms = $('#id_2_TOTAL_FORMS'); + let totalForms = document.querySelector("#id_para-TOTAL_FORMS") + let sensorForm = document.querySelectorAll(".parameter-form") + let container = document.querySelector("#para-container") + let addButton = document.querySelector("#para-end") + + let formNum = sensorForm.length-1 // Get the number of the last form on the page with zero-based indexing + //alert("My selected types are: " + favorite.join(", ")); + + $("input[name='0-activity_type']").click(function() { + favorite =[]; + $.each($("input[name='0-activity_type']:checked"), function(){ + favorite.push($(this).val()); + }); + //alert("My selected types are: " + favorite.join(", ")); + }); + + $("form").submit(function() { + // favorite =[]; + // $.each($("input[name='0-activity_type']:checked"), function(){ + // favorite.push($(this).val()); + // }); + //alert("My selected types are: " + favorite.join(", ")); + }); + + $("#id_sensor-test_method").change(function() { + //alert("My selected types are: " + $(this).find(":selected").text()); + + if($(this).find(":selected").text()!="Meter") { //3rd radiobutton + $("#id_sensor-calibration_date").attr("disabled", "disabled"); + $("#id_sensor-meter").attr("disabled", "disabled"); + } + else { + $("#id_sensor-calibration_date").removeAttr("disabled"); + $("#id_sensor-meter").removeAttr("disabled"); + } + + }); + + $(".btn-add-parameter").click(function(){ + //alert("Add method clicked!"); + AddSensorParameterForm(); + }); + + + // tutorial for dynamically adding Forms in Django with Formsets and JavaScript + // https://www.brennantymrak.com/articles/django-dynamic-formsets-javascript + + function AddSensorParameterForm() { + //e.preventDefault() + + const newForm = sensorForm[0].cloneNode(true) //Clone the bird form + $(newForm).attr('class','row parameter-form'); + let formRegex = RegExp(`parameter-(\\d){1}-`,'g') //Regex to find all instances of the form number + + formNum++ //Increment the form number + newForm.innerHTML = newForm.innerHTML.replace(formRegex, `para-${formNum}-`) //Update the new form to have the correct form number + + //container.insertBefore(newForm, addButton) //Insert the new form at the end of the list of forms + //$(newForm).insertBefore( "#btn-add-parameter" ); + $(newForm).val(''); + $('.parameter-card').append($(newForm)); + + totalForms.setAttribute('value', `${formNum+1}`) //Increment the number of total forms in the management form + + } +}); \ No newline at end of file diff --git a/src/dataloaderinterface/static/dataloaderinterface/js/streamwatch-detail.js b/src/dataloaderinterface/static/dataloaderinterface/js/streamwatch-detail.js new file mode 100644 index 00000000..7bcf9746 --- /dev/null +++ b/src/dataloaderinterface/static/dataloaderinterface/js/streamwatch-detail.js @@ -0,0 +1,6 @@ +$(document).ready(function () { + + $( "#accordion" ).accordion(); + + +}); \ No newline at end of file diff --git a/src/dataloaderinterface/templates/dataloaderinterface/footer.html b/src/dataloaderinterface/templates/dataloaderinterface/footer.html index 7ca196fc..39646139 100644 --- a/src/dataloaderinterface/templates/dataloaderinterface/footer.html +++ b/src/dataloaderinterface/templates/dataloaderinterface/footer.html @@ -40,9 +40,9 @@
The Watershed Institute is a team focused on keeping water clean, safe and healthy through conservation, advocacy, science and education. +
+Number of StreamWatch Schools Assessments | +Number of CAT Assessments | +Number of BAT Assessments | +Number of BACT Assessments | +Most Recent Assessment | +
---|---|---|---|---|
{{ streamwatch.school }} | +{{ streamwatch.chemical }} | +{{ streamwatch.biological }} | +{{ streamwatch.bacterial }} | +{{ streamwatch.most_recent }} | +