diff --git a/.gitignore b/.gitignore index f9366b84..1ccf35a8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/src/dataloaderinterface/static/data /src/WebSDL/.idea # Created by .ignore support plugin (hsz.mobi) ### Python template diff --git a/requirements.txt b/requirements.txt index 9168b6b2..39346d35 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,19 +1,17 @@ beautifulsoup4==4.5.1 +codegen==1.0 coverage==4.2 Django==1.10.3 -django-autocomplete-light==3.2.1 -django-bootstrap-form==3.2.1 -django-crispy-forms==1.6.1 +django-debug-toolbar==1.6 django-discover-runner==1.0 -django-filter==1.0.0 -django-formset-js==0.5.0 -django-jquery-js==2.1.4 django-webtest==1.8.0 django-widget-tweaks==1.4.1 djangorestframework==3.5.3 Markdown==2.6.7 -mysqlclient==1.3.9 +patterns==0.3 +psycopg2==2.7.1 six==1.10.0 +sqlparse==0.2.2 waitress==1.0.1 WebOb==1.6.2 WebTest==2.0.23 diff --git a/src/WebSDL/settings/base.py b/src/WebSDL/settings/base.py index facda1d1..bf035e5a 100644 --- a/src/WebSDL/settings/base.py +++ b/src/WebSDL/settings/base.py @@ -32,22 +32,22 @@ print("The secret key is required in the settings.json file.") exit(1) -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ['*'] # Application definition INSTALLED_APPS = [ + 'rest_framework', + 'dataloader', + 'dataloaderservices', + 'dataloaderinterface', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'rest_framework', - 'dataloader', - 'dataloaderservices', - 'dataloaderinterface', 'widget_tweaks' ] @@ -131,14 +131,10 @@ LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'UTC' - USE_I18N = True USE_L10N = True -USE_TZ = True - LOGIN_URL = 'login' DATABASE_ROUTERS = ['WebSDL.db_routers.WebSDLRouter'] @@ -154,4 +150,17 @@ RECAPTCHA_USER_KEY = data["recaptcha_user_key"] if "recaptcha_user_key" in data else "" -RECAPTCHA_VERIFY_URL = "https://www.google.com/recaptcha/api/siteverify" \ No newline at end of file +RECAPTCHA_VERIFY_URL = "https://www.google.com/recaptcha/api/siteverify" + + +EMAIL_SENDER = data['email_sender'] if 'email_sender' in data else '', + +DEFAULT_FROM_EMAIL = EMAIL_SENDER[0] if isinstance(EMAIL_SENDER, tuple) else EMAIL_SENDER + +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' + +EMAIL_SERVER = data['email_host'] if 'email_host' in data else '', + +EMAIL_HOST = EMAIL_SERVER[0] if isinstance(EMAIL_SERVER, tuple) else EMAIL_SERVER + +DATETIME_FORMAT = "N j, Y, H:m" diff --git a/src/WebSDL/settings/development.py b/src/WebSDL/settings/development.py index d86e3553..b5725919 100644 --- a/src/WebSDL/settings/development.py +++ b/src/WebSDL/settings/development.py @@ -4,9 +4,9 @@ INTERNAL_IPS = ( '127.0.0.1', - '129.123.51.198' ) +STATIC_ROOT = os.path.join(os.path.join(BASE_DIR, os.pardir), 'dataloaderinterface', 'static') STATIC_URL = '/static/' SITE_URL = '' diff --git a/src/WebSDL/settings/settings_template.json b/src/WebSDL/settings/settings_template.json new file mode 100644 index 00000000..ec721c1b --- /dev/null +++ b/src/WebSDL/settings/settings_template.json @@ -0,0 +1,28 @@ +{ + "secret_key": "", + "host": "-optional", + "recaptcha_secret_key": "-optional", + "recaptcha_user_key": "-optional", + "email_sender": "password.reset@data.envirodiy.org", + "email_host": "mail.usu.edu", + "databases": [ + { + "name": "default", + "schema": "", + "engine": "", + "user": "", + "password": "", + "host": "", + "port": "" + }, + { + "name": "odm2", + "schema": "", + "engine": "", + "user": "", + "password": "", + "host": "", + "port": "" + } + ] +} diff --git a/src/WebSDL/urls.py b/src/WebSDL/urls.py index 85de5df7..280895d5 100644 --- a/src/WebSDL/urls.py +++ b/src/WebSDL/urls.py @@ -14,15 +14,13 @@ 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) """ from django.conf import settings -from django.contrib.auth import views as auth_views from django.conf.urls import url, include from django.contrib import admin +from django.contrib.auth import views as auth_views from django.core.urlresolvers import reverse_lazy -from django.views.generic.edit import CreateView -from dataloader.models import Organization -from dataloaderinterface.forms import UserRegistrationForm -from dataloaderinterface.views import UserRegistrationView +from dataloaderinterface.views import UserRegistrationView, UserUpdateView + BASE_URL = settings.SITE_URL[1:] @@ -34,11 +32,24 @@ 'next_page': reverse_lazy('home') } +password_reset_configuration = { + 'post_reset_redirect': 'password_reset_done' +} + +password_done_configuration = { + 'post_reset_redirect': 'password_reset_complete' +} + urlpatterns = [ + url(r'^' + BASE_URL + 'password-reset/$', auth_views.password_reset, password_reset_configuration, name='password_reset'), + url(r'^' + BASE_URL + 'password-reset/done/$', auth_views.password_reset_done, name='password_reset_done'), + url(r'^' + BASE_URL + 'password-reset/(?P[0-9A-Za-z]+)-(?P.+)/$', auth_views.password_reset_confirm, password_done_configuration, name='password_reset_confirm'), + url(r'^' + BASE_URL + 'password-reset/completed/$', auth_views.password_reset_complete, name='password_reset_complete'), url(r'^' + BASE_URL + 'admin/', admin.site.urls), url(r'^' + BASE_URL + 'login/$', auth_views.login, login_configuration, name='login'), url(r'^' + BASE_URL + 'logout/$', auth_views.logout, logout_configuration, name='logout'), url(r'^' + BASE_URL + 'register/$', UserRegistrationView.as_view(), name='user_registration'), + url(r'^' + BASE_URL + 'account/$', UserUpdateView.as_view(), name='user_account'), url(r'^' + BASE_URL + 'api-auth/', include('rest_framework.urls', namespace='rest_framework')), url(BASE_URL, include('dataloaderinterface.urls')), url(BASE_URL, include('dataloaderservices.urls')), diff --git a/src/dataloader/models.py b/src/dataloader/models.py index 1635ef43..4140694f 100644 --- a/src/dataloader/models.py +++ b/src/dataloader/models.py @@ -1,15 +1,17 @@ from __future__ import unicode_literals +import inspect +import sys import uuid -from django.db import models -from django.utils.encoding import python_2_unicode_compatible - from dataloader.querysets import AffiliationQuerySet, RelatedActionManager, ResultManager, \ - DataLoggerFileManager, EquipmentModelManager, DataLoggerFileColumnManager, InstrumentOutputVariableManager, \ + DataLoggerFileManager, InstrumentOutputVariableManager, \ EquipmentManager, CalibrationReferenceEquipmentManager, EquipmentUsedManager, MaintenanceActionManager, \ RelatedEquipmentManager, CalibrationActionManager, ODM2QuerySet, ActionQuerySet, ActionByQuerySet, \ - FeatureActionQuerySet, TimeSeriesValuesQuerySet + FeatureActionQuerySet, TimeSeriesValuesQuerySet, EquipmentModelQuerySet +from django.conf import settings +from django.db import models +from django.utils.encoding import python_2_unicode_compatible # TODO: function to handle the file upload folder for file fields. @@ -120,7 +122,7 @@ class ResultValue(models.Model): value_datetime_utc_offset = models.IntegerField(db_column='valuedatetimeutcoffset') def __str__(self): - return '%s %s' % self.value_datetime, self.data_value + return '%s %s' % (self.value_datetime, self.data_value) def __repr__(self): return "<%s('%s', '%s', Result['%s', '%s'], '%s')>" % ( @@ -137,7 +139,7 @@ class ResultValueAnnotation(models.Model): annotation = models.ForeignKey('Annotation', db_column='annotationid') def __str__(self): - return '%s %s' % self.value_datetime, self.data_value + return '%s %s' % (self.value_datetime, self.data_value) def __repr__(self): return "<%s('%s', Annotation['%s', '%s'], ResultValue['%s', '%s')>" % ( @@ -191,6 +193,9 @@ class XIntendedComponent(models.Model): intended_x_spacing = models.FloatField(db_column='intendedxspacing') intended_x_spacing_unit = models.ForeignKey('Unit', related_name='+', db_column='intendedxspacingunitsid', blank=True, null=True) + class Meta: + abstract = True + class YIntendedComponent(models.Model): intended_y_spacing = models.FloatField(db_column='intendedyspacing') @@ -407,7 +412,7 @@ class Meta: class Organization(ODM2Model): organization_id = models.AutoField(db_column='organizationid', primary_key=True) organization_type = models.ForeignKey('OrganizationType', db_column='organizationtypecv') - organization_code = models.CharField(db_column='organizationcode', max_length=50) + organization_code = models.CharField(db_column='organizationcode', max_length=50, unique=True) organization_name = models.CharField(db_column='organizationname', max_length=255) organization_description = models.CharField(db_column='organizationdescription', blank=True, max_length=500) organization_link = models.CharField(db_column='organizationlink', blank=True, max_length=255) @@ -447,7 +452,7 @@ def role_status(self): return 'Primary contact' if self.is_primary_organization_contact else 'Secondary contact' def __str__(self): - return '%s (%s) - %s' % (self.person, self.primary_email, self.organization) + return '%s - %s' % (self.person, self.organization) def __repr__(self): return "" % ( @@ -547,7 +552,7 @@ class SamplingFeature(models.Model): sampling_feature_id = models.AutoField(db_column='samplingfeatureid', primary_key=True) sampling_feature_uuid = models.UUIDField(default=uuid.uuid4, editable=False, db_column='samplingfeatureuuid') sampling_feature_type = models.ForeignKey('SamplingFeatureType', db_column='samplingfeaturetypecv') - sampling_feature_code = models.CharField(db_column='samplingfeaturecode', max_length=50) + sampling_feature_code = models.CharField(db_column='samplingfeaturecode', max_length=50, unique=True) sampling_feature_name = models.CharField(db_column='samplingfeaturename', blank=True, max_length=255) sampling_feature_description = models.CharField(db_column='samplingfeaturedescription', blank=True, max_length=500) sampling_feature_geo_type = models.ForeignKey('SamplingFeatureGeoType', db_column='samplingfeaturegeotypecv', blank=True, null=True) @@ -562,6 +567,12 @@ class SamplingFeature(models.Model): external_identifiers = models.ManyToManyField('ExternalIdentifierSystem', related_name='sampling_features', through='SamplingFeatureExternalIdentifier') + @property + def latest_updated_result(self): + return self.feature_actions.with_results()\ + .filter(results__value_count__gt=0)\ + .latest('results__result_datetime').results.first() + def __str__(self): return '%s %s' % (self.sampling_feature_code, self.sampling_feature_name) @@ -690,7 +701,7 @@ class Unit(models.Model): unit_link = models.CharField(db_column='unitslink', blank=True, max_length=255) def __str__(self): - return '%s: %s (%s)' % (self.unit_type_id, self.unit_abbreviation, self.unit_name) + return '%s: %s (%s)' % (self.unit_type_id, self.unit_name, self.unit_abbreviation) def __repr__(self): return "" % ( @@ -717,7 +728,7 @@ class Variable(models.Model): through='VariableExternalIdentifier') def __str__(self): - return '%s: %s (%s)' % (self.variable_name_id, self.variable_code, self.variable_type_id) + return '%s: %s' % (self.variable_name_id, self.variable_code) def __repr__(self): return "" % ( @@ -777,7 +788,7 @@ class Meta: @python_2_unicode_compatible class DataLoggerProgramFile(models.Model): program_id = models.AutoField(db_column='programid', primary_key=True) - affiliation_id = models.ForeignKey('Affiliation', db_column='affiliationid') + affiliation = models.ForeignKey('Affiliation', db_column='affiliationid', related_name='data_logger_programs') program_name = models.CharField(db_column='programname', max_length=255) program_description = models.CharField(db_column='programdescription', blank=True, max_length=500) program_version = models.CharField(db_column='programversion', blank=True, max_length=50) @@ -798,7 +809,7 @@ class Meta: @python_2_unicode_compatible class DataLoggerFile(models.Model): data_logger_file_id = models.AutoField(db_column='dataloggerfileid', primary_key=True) - program = models.ForeignKey('DataLoggerProgramFile', db_column='programid') + program = models.ForeignKey('DataLoggerProgramFile', db_column='programid', related_name='data_logger_files') data_logger_file_name = models.CharField(db_column='dataloggerfilename', max_length=255) data_logger_file_description = models.CharField(db_column='dataloggerfiledescription', blank=True, max_length=500) data_logger_file_link = models.FileField(db_column='dataloggerfilelink', blank=True) @@ -833,8 +844,6 @@ class DataLoggerFileColumn(models.Model): recording_interval_unit = models.ForeignKey('Unit', related_name='recording_interval_data_logger_file_columns', db_column='recordingintervalunitsid', blank=True, null=True) aggregation_statistic = models.ForeignKey('AggregationStatistic', related_name='data_logger_file_columns', db_column='aggregationstatisticcv', blank=True) - objects = DataLoggerFileColumnManager() - def __str__(self): return '%s %s' % (self.column_label, self.column_description) @@ -861,10 +870,10 @@ class EquipmentModel(models.Model): output_variables = models.ManyToManyField('Variable', related_name='instrument_models', through='InstrumentOutputVariable') output_units = models.ManyToManyField('Unit', related_name='instrument_models', through='InstrumentOutputVariable') - objects = EquipmentModelManager() + objects = EquipmentModelQuerySet.as_manager() def __str__(self): - return '%s: %s' % (self.model_name, self.model_description) + return '%s %s' % (self.model_manufacturer, self.model_name) def __repr__(self): return "" % ( @@ -1849,7 +1858,7 @@ class Meta: db_table = 'pointcoverageresults' -class ProfileResult(ExtendedResult, AggregatedComponent, XOffsetComponent, YOffsetComponent, YIntendedComponent, ZIntendedComponent, TimeIntendedComponent): +class ProfileResult(ExtendedResult, AggregatedComponent, XOffsetComponent, YOffsetComponent, ZIntendedComponent, TimeIntendedComponent): class Meta: db_table = 'profileresults' @@ -2068,3 +2077,19 @@ class Meta: db_table = 'transectresultvalueannotations' # endregion + + +# TODO: make something more sophisticated than this later on +sql_schema_fix = 'odm2].[' # for SQL databases +psql_schema_fix = 'odm2"."' # for postgres databases +clsmembers = inspect.getmembers(sys.modules[__name__], inspect.isclass) +classes = [model for name, model in clsmembers if issubclass(model, models.Model)] +database_manager = settings.DATABASES['odm2']['ENGINE'] + +for model in classes: + if database_manager == u'sql_server.pyodbc': + model._meta.db_table = sql_schema_fix.upper() + model._meta.db_table + elif database_manager == u'django.db.backends.postgresql_psycopg2': + model._meta.db_table = psql_schema_fix + model._meta.db_table + + # can add more fixes there depending on the database engine diff --git a/src/dataloader/querysets.py b/src/dataloader/querysets.py index 377af979..93d260fc 100644 --- a/src/dataloader/querysets.py +++ b/src/dataloader/querysets.py @@ -47,7 +47,7 @@ def for_display(self): return self.select_related('action').prefetch_related('sampling_feature') def with_results(self): - return self.prefetch_related('results__timeseriesresult__values', 'results__variable') + return self.prefetch_related('results__timeseriesresult__values', 'results__variable', 'results__unit') class RelatedActionManager(models.Manager): @@ -80,10 +80,9 @@ def get_queryset(self): # region ODM2 Equipment Extension -class EquipmentModelManager(models.Manager): - def get_queryset(self): - queryset = super(EquipmentModelManager, self).get_queryset() - return queryset.prefetch_related('model_manufacturer') +class EquipmentModelQuerySet(ODM2QuerySet): + def for_display(self): + return self.prefetch_related('model_manufacturer') class InstrumentOutputVariableManager(models.Manager): diff --git a/src/dataloader/tests/test_models.py b/src/dataloader/tests/test_models.py index 7b89e06e..3a8b89b2 100644 --- a/src/dataloader/tests/test_models.py +++ b/src/dataloader/tests/test_models.py @@ -1,6 +1,6 @@ +from dataloader.models import * from django.test import TestCase -from dataloader.models import * from dataloader.tests.data import data_manager models_data = data_manager.test_data['models']['data'] diff --git a/src/dataloaderinterface/csv_serializer.py b/src/dataloaderinterface/csv_serializer.py new file mode 100644 index 00000000..b8325afe --- /dev/null +++ b/src/dataloaderinterface/csv_serializer.py @@ -0,0 +1,67 @@ +import codecs +import csv +import os + +from datetime import timedelta +from django.contrib.staticfiles.storage import staticfiles_storage +from unicodecsv.py2 import UnicodeWriter + + +class SiteResultSerializer: + headers = ('DateTime', 'TimeOffset', 'DateTimeUTC', 'Value', 'CensorCode', 'QualifierCode', ) + date_format = '%Y-%m-%d %H:%M:%S' + + def __init__(self, result): + self.result = result + + def get_file_path(self): + filename = "{0}_{1}_{2}.csv".format(self.result.feature_action.sampling_feature.sampling_feature_code, + self.result.variable.variable_code, self.result.result_id) + return os.path.join('data', filename) + + def open_csv_file(self): + csv_file = staticfiles_storage.open(self.get_file_path(), 'ab+') + return csv_file + + def create_csv_file(self): + csv_file = staticfiles_storage.open(self.get_file_path(), 'wb') + return csv_file + + def get_metadata_template(self): + with codecs.open(os.path.join(os.path.dirname(__file__), 'metadata_template.txt'), 'r', encoding='utf-8') as metadata_file: + return metadata_file.read() + + def generate_metadata(self): + action = self.result.feature_action.action + equipment_model = self.result.data_logger_file_columns.first().instrument_output_variable.model + affiliation = action.action_by.filter(is_action_lead=True).first().affiliation + return self.get_metadata_template().format( + sampling_feature=self.result.feature_action.sampling_feature, + variable=self.result.variable, + unit=self.result.unit, + model=equipment_model, + result=self.result, + action=action, + affiliation=affiliation + ).encode('utf-8') + + def build_csv(self): + with self.create_csv_file() as output_file: + output_file.write(self.generate_metadata()) + csv_writer = UnicodeWriter(output_file) + csv_writer.writerow(self.headers) + + def add_data_value(self, data_value): + self.add_data_values([data_value]) + + def add_data_values(self, data_values): + data = [(data_value.value_datetime.strftime(self.date_format), + '{0}:00'.format(data_value.value_datetime_utc_offset), + (data_value.value_datetime - timedelta(hours=data_value.value_datetime_utc_offset)).strftime(self.date_format), + data_value.data_value, + data_value.censor_code_id, + data_value.quality_code_id) + for data_value in data_values] + with self.open_csv_file() as output_file: + csv_writer = UnicodeWriter(output_file) + csv_writer.writerows(data) diff --git a/src/dataloaderinterface/forms.py b/src/dataloaderinterface/forms.py index 0956dbde..c1b51a55 100644 --- a/src/dataloaderinterface/forms.py +++ b/src/dataloaderinterface/forms.py @@ -1,13 +1,14 @@ from datetime import datetime +from dataloader.models import SamplingFeature, People, Organization, Affiliation, Result, Site, EquipmentModel, Medium, \ + OrganizationType, ActionBy, SiteType from django import forms -from django.contrib.auth.forms import UserCreationForm +from django.contrib.auth.forms import UserCreationForm, UserChangeForm +from django.contrib.auth.models import User from django.db.models.query_utils import Q from django.forms.formsets import formset_factory from dataloaderinterface.models import ODM2User -from dataloader.models import SamplingFeature, Action, People, Organization, Affiliation, Result, ActionBy, Method, \ - Site, EquipmentModel, Medium, OrganizationType # AUTHORIZATION @@ -20,7 +21,7 @@ class UserRegistrationForm(UserCreationForm): last_name = forms.CharField(required=True, max_length=50) email = forms.EmailField(required=True, max_length=254) organization = forms.ModelChoiceField( - queryset=Organization.objects.all().exclude(organization_type__in=['Vendor', 'Manufacturer']), required=False) + queryset=Organization.objects.all().exclude(organization_type__in=['Vendor', 'Manufacturer']), required=False, help_text='Begin to enter the common name of your organization to choose from the list. If "No results found", then clear your entry, click on the drop-down-list to select "Add New Organization".') agreement = forms.BooleanField(required=True) def save(self, commit=True): @@ -31,26 +32,76 @@ def save(self, commit=True): user.agreement = self.cleaned_data['agreement'] organization = self.cleaned_data['organization'] - person = People.objects.filter(person_first_name=user.first_name, person_last_name=user.last_name).first() or \ - People.objects.create(person_first_name=user.first_name, person_last_name=user.last_name) - affiliation = Affiliation.objects.filter(person=person, organization=organization).first() or \ - Affiliation.objects.create(person=person, organization=organization, affiliation_start_date=datetime.now()) - if commit: user.save() + person = People.objects.create(person_first_name=user.first_name, person_last_name=user.last_name) + affiliation = Affiliation.objects.create(person=person, organization=organization, affiliation_start_date=datetime.now(), primary_email=user.email) ODM2User.objects.create(user=user, affiliation_id=affiliation.affiliation_id) return user + class Meta: + model = User + fields = ['first_name', 'last_name', 'email', 'username', 'organization'] + + +class UserUpdateForm(UserChangeForm): + use_required_attribute = False + + first_name = forms.CharField(required=True, max_length=50) + last_name = forms.CharField(required=True, max_length=50) + email = forms.EmailField(required=True, max_length=254) + organization = forms.ModelChoiceField( + queryset=Organization.objects.all().exclude(organization_type__in=['Vendor', 'Manufacturer']), + required=False, + help_text='Begin to enter the common name of your organization to choose from the list. If "No results found", then clear your entry, click on the drop-down-list to select "Add New Organization".') + + def save(self, commit=True): + user = super(UserUpdateForm, self).save(commit=False) + organization = self.cleaned_data['organization'] + + if commit: + affiliation = user.odm2user.affiliation + person = affiliation.person + + person.person_first_name = user.first_name + person.person_last_name = user.last_name + affiliation.primary_email = user.email + affiliation.organization = organization + + user.save() + person.save() + affiliation.save() + + return user + + class Meta: + model = User + fields = ['first_name', 'last_name', 'email', 'password', 'username', 'organization'] + # ODM2 +class ActionByForm(forms.ModelForm): + use_required_attribute = False + affiliation = forms.ModelChoiceField(queryset=Affiliation.objects.all(), required=False, help_text='', label='Deployed By') + + class Meta: + model = ActionBy + fields = ['affiliation'] + + class OrganizationForm(forms.ModelForm): use_required_attribute = False - organization_type = forms.ModelChoiceField(queryset=OrganizationType.objects.all().exclude(name__in=['Vendor', 'Manufacturer']), required=False) + organization_type = forms.ModelChoiceField(queryset=OrganizationType.objects.all().exclude(name__in=['Vendor', 'Manufacturer']), required=False, help_text='Choose the type of organization') class Meta: model = Organization + help_texts = { + 'organization_code': 'Enter a brief, but unique code to identify your organization (e.g., "USU" or "USGS")', + 'organization_name': 'Enter the name of your organization', + 'organization_description': 'Enter a description for your organization' + } fields = [ 'organization_code', 'organization_name', @@ -61,9 +112,15 @@ class Meta: class SamplingFeatureForm(forms.ModelForm): use_required_attribute = False + sampling_feature_name = forms.CharField(required=True, label='Site Name', help_text='Enter a brief but descriptive name for your site (e.g., "Delaware River near Phillipsburg")') class Meta: model = SamplingFeature + help_texts = { + 'sampling_feature_code': 'Enter a brief and unique text string to identify your site (e.g., "Del_Phil")', + 'elevation_m': 'Enter the elevation of your site in meters', + 'elevation_datum': 'Choose the elevation datum for your site\'s elevation. If you don\'t know it, choose "MSL"' + } fields = [ 'sampling_feature_code', 'sampling_feature_name', @@ -72,16 +129,28 @@ class Meta: ] labels = { 'sampling_feature_code': 'Site Code', - 'sampling_feature_name': 'Site Name', 'elevation_m': 'Elevation', } class SiteForm(forms.ModelForm): + allowed_site_types = [ + 'Aggregate groundwater use', 'Ditch', 'Atmosphere', 'Estuary', 'House', 'Land', 'Pavement', 'Stream', 'Spring', + 'Lake, Reservoir, Impoundment', 'Laboratory or sample-preparation area', 'Soil hole', 'Storm sewer', 'Tidal stream', 'Wetland' + ] + + site_type = forms.ModelChoiceField( + queryset=SiteType.objects.filter(name__in=allowed_site_types), help_text='Select the type of site you are deploying (e.g., "Stream")' + ) use_required_attribute = False class Meta: model = Site + help_texts = { + 'site_type': '', + 'latitude': 'Enter the latitude of your site in decimal degrees (e.g., 40.6893)', + 'longitude': 'Enter the longitude of your site in decimal degrees (e.g., -75.2033)', + } fields = [ 'site_type', 'latitude', @@ -94,16 +163,25 @@ def __init__(self, *args, **kwargs): super(ResultForm, self).__init__(*args, **kwargs) self.empty_permitted = False - equipment_model = forms.ModelChoiceField(queryset=EquipmentModel.objects.all()) + equipment_model = forms.ModelChoiceField(queryset=EquipmentModel.objects.for_display(), help_text='Choose the model of your sensor') sampled_medium = forms.ModelChoiceField(queryset=Medium.objects.filter( Q(pk='Air') | Q(pk='Soil') | - Q(pk='Liquid aqueous') - )) + Q(pk='Liquid aqueous') | + Q(pk='Equipment') | + Q(pk='Not applicable') + ), help_text='Choose the sampled medium') class Meta: model = Result + help_texts = { + 'equipment_model': 'Choose the model of your sensor', + 'variable': 'Choose the measured variable', + 'unit': 'Choose the measured units', + 'sampled_medium': 'Choose the sampled medium' + } fields = [ + 'result_id', 'equipment_model', 'variable', 'unit', @@ -116,4 +194,4 @@ class Meta: } -ResultFormSet = formset_factory(ResultForm, extra=0, can_order=False, min_num=0) +ResultFormSet = formset_factory(ResultForm, extra=0, can_order=False, min_num=1, can_delete=True) diff --git a/src/dataloader/migrations/__init__.py b/src/dataloaderinterface/management/__init__.py similarity index 100% rename from src/dataloader/migrations/__init__.py rename to src/dataloaderinterface/management/__init__.py diff --git a/src/dataloaderinterface/migrations/__init__.py b/src/dataloaderinterface/management/commands/__init__.py similarity index 100% rename from src/dataloaderinterface/migrations/__init__.py rename to src/dataloaderinterface/management/commands/__init__.py diff --git a/src/dataloaderinterface/management/commands/add_equipment_medium.py b/src/dataloaderinterface/management/commands/add_equipment_medium.py new file mode 100644 index 00000000..730d76a1 --- /dev/null +++ b/src/dataloaderinterface/management/commands/add_equipment_medium.py @@ -0,0 +1,30 @@ +from django.core.management.base import BaseCommand +from django.db.models.aggregates import Count +from django.db.models.query_utils import Q + +from dataloader.models import Result, Medium +from dataloaderinterface.models import SiteSensor + + +class Command(BaseCommand): + help = '' + + def get_medium_data(self): + equipment_data = { + 'term': 'equipment', + 'name': 'Equipment', + 'definition': 'An instrument, sensor or other piece of human-made equipment upon which a measurement is made,' + ' such as datalogger temperature or battery voltage.', + } + return equipment_data + + def handle(self, *args, **options): + equipment_medium_variables = ['EnviroDIY_Mayfly_Temp', 'EnviroDIY_Mayfly_Volt', 'EnviroDIY_Mayfly_FreeSRAM'] + equipment_medium = Medium.objects.filter(pk='Equipment').first() + if not equipment_medium: + equipment_medium = Medium.objects.create(**self.get_medium_data()) + + results = Result.objects.prefetch_related('variable').filter(variable__variable_code__in=equipment_medium_variables) + sensors = SiteSensor.objects.filter(variable_code__in=equipment_medium_variables) + results.update(sampled_medium=equipment_medium) + sensors.update(sampled_medium=equipment_medium.name) diff --git a/src/dataloaderinterface/management/commands/fix_data.py b/src/dataloaderinterface/management/commands/fix_data.py new file mode 100644 index 00000000..0b473ed0 --- /dev/null +++ b/src/dataloaderinterface/management/commands/fix_data.py @@ -0,0 +1,148 @@ + +from dataloader.models import DataLoggerProgramFile, DataLoggerFile, ActionBy, TimeSeriesResult, DataLoggerFileColumn, \ + InstrumentOutputVariable, Result +from django.core.management.base import BaseCommand + +from dataloaderinterface.models import DeviceRegistration + + +class Command(BaseCommand): + help = 'Fix corrupted or incomplete odm2 data in the scope of this application.' + + @staticmethod + def guess_equipment_model(result): + return InstrumentOutputVariable.objects.filter(variable=result.variable, instrument_raw_output_unit=result.unit).first() + + @staticmethod + def check_sampling_feature(registration): + sampling_feature = registration.sampling_feature + if sampling_feature.feature_actions.count() == 0: + # site doesn't have any sensors. delete site and registration. + + data_logger_program = DataLoggerProgramFile.filter( + affiliation=registration.user.affiliation, + program_name='%s' % sampling_feature.sampling_feature_code + ).all() + + data_logger_file = DataLoggerFile.filter( + program=data_logger_program, + data_logger_file_name='%s' % sampling_feature.sampling_feature_code + ).all() + + # TODO: columns. + + print("* site doesn't have any sensors. deleting.") + sampling_feature.site and sampling_feature.site.delete() + print("- site instance deleted.") + sampling_feature.delete() + print("- sampling feature instance deleted.") + registration.delete() + print("- registration instance deleted.") + + @staticmethod + def delete_entire_registration(): + pass + + def handle(self, *args, **options): + registrations = DeviceRegistration.objects.all() + print("%s site registrations found." % registrations.count()) + print("----------------------------------------------------") + + for registration in registrations: + if not registration.user and registration.user.affiliation and registration.sampling_feature: + # doesn't have an affiliation or sampling feature. no way of fixing this. + print("* unrepairable registration found. deleting." % registration) + registration.delete() + continue + + affiliation = registration.user.affiliation + sampling_feature = registration.sampling_feature + print("---- retrieving registration for site %s, deployment by %s." % (sampling_feature, affiliation)) + + if sampling_feature.feature_actions.count() == 0: + # site doesn't have any sensors. delete site and registration. + print("* site doesn't have any sensors. deleting.") + sampling_feature.site and sampling_feature.site.delete() + print("- site instance deleted.") + sampling_feature.delete() + print("- sampling feature instance deleted.") + registration.delete() + print("- registration instance deleted.") + continue + + data_logger_program, created = DataLoggerProgramFile.objects.get_or_create( + affiliation=affiliation, + program_name='%s' % sampling_feature.sampling_feature_code + ) + print("- data logger program found for this site." if not created else "* no data logger program found for this site. creating.") + + data_logger_file, created = DataLoggerFile.objects.get_or_create( + program=data_logger_program, + data_logger_file_name='%s' % sampling_feature.sampling_feature_code + ) + print("- data logger file found for this site." if not created else "* no data logger file found for this site. creating.") + + print("-- loading feature actions") + for feature_action in sampling_feature.feature_actions.all(): + result = feature_action.results.first() + action = feature_action.action + + if not result: + print("* feature action has no associated result. deleting.") + feature_action.delete() + print("- feature action instance deleted.") + action.action_by and action.action_by.all().delete() + print("- action by instance deleted.") + action.delete() + print("- action instance deleted.") + continue + + print("- retrieving result %s." % result) + action = feature_action.action + action_by, created = ActionBy.objects.get_or_create(action=action, affiliation=affiliation, is_action_lead=True) + print("- action by instance found." if not created else "* action by instance not found. creating.") + + time_series_result, created = TimeSeriesResult.objects.get_or_create( + result=result, + aggregation_statistic_id='Average', + ) + print("- time series result instance found." if not created else "* no time series result instance found for this site. creating.") + + column = result.data_logger_file_columns.first() + column_label = '%s(%s)' % (result.variable.variable_code, result.unit.unit_abbreviation) + if not column: + print("* no data logger file column associated with this result. creating.") + DataLoggerFileColumn.objects.create( + result=result, + data_logger_file=data_logger_file, + instrument_output_variable=self.guess_equipment_model(result), + column_label=column_label + ) + else: + if column.data_logger_file != data_logger_file: + print("* data logger column doesn't belong to retrieved data logger file. matching up.") + column.data_logger_file = data_logger_file + column.save() + if column.column_label != column_label: + column.column_label = column_label + column.save() + + # data_logger_file_column = result.data_logger_file_columns.first() + # data_logger_file_column.instrument_output_variable = instrument_output_variable + # data_logger_file_column.column_label = '%s(%s)' % ( + # result.variable.variable_code, result.unit.unit_abbreviation) + + if sampling_feature.feature_actions.count() == 0: + # TODO: delete sampling feature. + pass + + # Remove rogue Time Series Results. + site_registrations = [str(uuid['deployment_sampling_feature_uuid']) for uuid in DeviceRegistration.objects.all().values('deployment_sampling_feature_uuid')] + rogue_results = Result.objects.exclude(feature_action__sampling_feature__sampling_feature_uuid__in=(site_registrations)) + for rogue_result in rogue_results: + rogue_result.timeseriesresult and rogue_result.timeseriesresult.values.all().delete() + rogue_result.timeseriesresult and rogue_result.timeseriesresult.delete() + rogue_result.delete() + + + print("check complete!") diff --git a/src/dataloaderinterface/management/commands/generate_csv_data.py b/src/dataloaderinterface/management/commands/generate_csv_data.py new file mode 100644 index 00000000..23e5949a --- /dev/null +++ b/src/dataloaderinterface/management/commands/generate_csv_data.py @@ -0,0 +1,32 @@ +from django.core.management.base import BaseCommand + +from dataloader.models import Result, TimeSeriesResult +from dataloaderinterface.csv_serializer import SiteResultSerializer + + +class Command(BaseCommand): + help = '' + + def handle(self, *args, **options): + results = TimeSeriesResult.objects.prefetch_related( + 'result__variable', + 'result__unit', + 'result__processing_level', + 'result__data_logger_file_columns__instrument_output_variable__model', + 'result__feature_action__sampling_feature__site', + 'result__feature_action__action__method', + 'result__feature_action__action__action_by__affiliation__person', + 'result__feature_action__action__action_by__affiliation__organization' + ).all() + # falta como una musica de elevador. wtf. + for time_series_result in results: + print('result %s:' % time_series_result.result_id) + serializer = SiteResultSerializer(result=time_series_result.result) + serializer.build_csv() + print('-file with metadata created') + if time_series_result.result.value_count > 0: + values = time_series_result.values.all() + serializer.add_data_values(values) + print('-data values written to file') + + print('--csv generated.') diff --git a/src/dataloaderinterface/management/commands/migrate_site_registrations.py b/src/dataloaderinterface/management/commands/migrate_site_registrations.py new file mode 100644 index 00000000..28e7e3c7 --- /dev/null +++ b/src/dataloaderinterface/management/commands/migrate_site_registrations.py @@ -0,0 +1,84 @@ +from django.core.management.base import BaseCommand + +from dataloader.models import SamplingFeature +from dataloaderinterface.models import DeviceRegistration, SiteRegistration, SiteSensor + + +class Command(BaseCommand): + help = 'Fix corrupted or incomplete odm2 data in the scope of this application.' + + @staticmethod + def create_site_registration(device_registration): + existing_site_registration = SiteRegistration.objects.filter(registration_token=device_registration.authentication_token).first() + if existing_site_registration: + return existing_site_registration + + affiliation = device_registration.user.affiliation + sampling_feature = device_registration.sampling_feature + + registration_data = { + 'registration_token': device_registration.authentication_token, + 'registration_date': device_registration.registration_date(), + 'django_user': device_registration.user.user, + 'affiliation_id': device_registration.user.affiliation_id, + 'person': str(affiliation.person), + 'organization': str(affiliation.organization) or '', + 'sampling_feature_id': sampling_feature.sampling_feature_id, + 'sampling_feature_code': sampling_feature.sampling_feature_code, + 'sampling_feature_name': sampling_feature.sampling_feature_name, + 'elevation_m': sampling_feature.elevation_m, + 'latitude': sampling_feature.site.latitude, + 'longitude': sampling_feature.site.longitude, + 'site_type': sampling_feature.site.site_type_id + } + + site_registration = SiteRegistration(**registration_data) + site_registration.save() + return site_registration + + @staticmethod + def create_site_sensor(result, site_registration): + existing_site_sensor = SiteSensor.objects.filter(result_id=result.result_id).first() + if existing_site_sensor: + return existing_site_sensor + + model = result.data_logger_file_columns.first().instrument_output_variable.model + values_manager = result.timeseriesresult.values + last_value = values_manager.latest('value_datetime') if values_manager.count() > 0 else None + + sensor_data = { + 'result_id': result.result_id, + 'result_uuid': result.result_uuid, + 'registration': site_registration, + 'model_name': model.model_name, + 'model_manufacturer': model.model_manufacturer.organization_name, + 'variable_name': result.variable.variable_name_id, + 'variable_code': result.variable.variable_code, + 'unit_name': result.unit.unit_name, + 'unit_abbreviation': result.unit.unit_abbreviation, + 'sampled_medium': result.sampled_medium_id, + 'activation_date': result.valid_datetime, + 'activation_date_utc_offset': result.valid_datetime_utc_offset, + 'last_measurement_id': last_value and last_value.value_id + } + + site_sensor = SiteSensor(**sensor_data) + site_sensor.save() + return site_sensor + + def handle(self, *args, **options): + sampling_features = SamplingFeature.objects.all() + print("%s sites found." % sampling_features.count()) + print("----------------------------------------------------") + + for sampling_feature in sampling_features: + device_registration = DeviceRegistration.objects.filter(deployment_sampling_feature_uuid=sampling_feature.sampling_feature_uuid).first() + if not device_registration: + print('**** Sampling Feature %s (%s) exists without a site registration!!!' % (sampling_feature.sampling_feature_id, sampling_feature.sampling_feature_code)) + continue + + site_registration = self.create_site_registration(device_registration) + for feature_action in sampling_feature.feature_actions.all(): + result = feature_action.results.first() + self.create_site_sensor(result, site_registration) + print('- site %s migrated!' % sampling_feature.sampling_feature_code) diff --git a/src/dataloaderinterface/management/commands/patch_results_datetime.py b/src/dataloaderinterface/management/commands/patch_results_datetime.py new file mode 100644 index 00000000..b1b9c5a9 --- /dev/null +++ b/src/dataloaderinterface/management/commands/patch_results_datetime.py @@ -0,0 +1,26 @@ +from django.core.management.base import BaseCommand +from django.db.models.aggregates import Count + +from dataloader.models import Result + + +class Command(BaseCommand): + help = '' + + @staticmethod + def fix_results_value_count(): + results = Result.objects.annotate(number_of_values=Count('timeseriesresult__values')) + for result in results: + result.value_count = result.number_of_values + result.save() + + + def handle(self, *args, **options): + self.fix_results_value_count() + results = Result.objects.prefetch_related('timeseriesresult__values').filter(value_count__gt=0) + for result in results: + first_value = result.timeseriesresult.values.order_by('value_datetime').first() + result.valid_datetime = first_value.value_datetime + result.valid_datetime_utc_offset = first_value.value_datetime_utc_offset + result.save() + print('result %s: set first valid datetime to %s' % (result.pk, result.valid_datetime)) diff --git a/src/dataloaderinterface/management/commands/regenerate_datalogger_metadata.py b/src/dataloaderinterface/management/commands/regenerate_datalogger_metadata.py new file mode 100644 index 00000000..1e29a687 --- /dev/null +++ b/src/dataloaderinterface/management/commands/regenerate_datalogger_metadata.py @@ -0,0 +1,53 @@ +from django.core.management.base import BaseCommand + +from dataloader.models import DataLoggerFileColumn, DataLoggerProgramFile, DataLoggerFile, InstrumentOutputVariable, \ + EquipmentModel +from dataloaderinterface.models import SiteRegistration, SiteSensor + + +class Command(BaseCommand): + help = '' + + @staticmethod + def delete_datalogger_data(): + print('%s program files found vs %s registrations' % (DataLoggerProgramFile.objects.count(), SiteRegistration.objects.count())) + print('%s datalogger files found' % DataLoggerFile.objects.count()) + print('%s datalogger file columns found vs %s sensors registered' % (DataLoggerFileColumn.objects.count(), SiteSensor.objects.count())) + DataLoggerProgramFile.objects.all().delete() + DataLoggerFileColumn.objects.all().delete() + DataLoggerFile.objects.all().delete() + + def handle(self, *args, **options): + self.delete_datalogger_data() + + registrations = SiteRegistration.objects.prefetch_related('sensors').all() + for registration in registrations: + data_logger_program = DataLoggerProgramFile.objects.create( + affiliation_id=registration.affiliation_id, + program_name='%s' % registration.sampling_feature_code + ) + + data_logger_file = DataLoggerFile.objects.create( + program=data_logger_program, + data_logger_file_name='%s' % registration.sampling_feature_code + ) + + print('--- Program and file created for site %s' % registration.sampling_feature_code) + + sensors = registration.sensors.all() + for sensor in sensors: + instrument_output_variable = InstrumentOutputVariable.objects.filter( + model__model_name=sensor.model_name, + variable__variable_code=sensor.variable_code, + instrument_raw_output_unit__unit_name=sensor.unit_name, + ).first() + if not instrument_output_variable: + print('***** instrument output variable not found for result %s' % sensor.result_id) + + DataLoggerFileColumn.objects.create( + result=sensor.result, + data_logger_file=data_logger_file, + instrument_output_variable=instrument_output_variable, + column_label='%s(%s)' % (sensor.variable_code, sensor.unit_abbreviation) + ) + print('---- Datalogger file column created for %s' % sensor.sensor_identity) diff --git a/src/dataloaderinterface/metadata_template.txt b/src/dataloaderinterface/metadata_template.txt new file mode 100644 index 00000000..bd6457c3 --- /dev/null +++ b/src/dataloaderinterface/metadata_template.txt @@ -0,0 +1,52 @@ +# ---------------------------------------------------------------------------------- +# WARNING: These data are provisional and subject to revision. The data are provided +# as is with no warranty and on the condition that neither the data collector nor +# any of the participants in or developers of http://data.envirodiy.org may be held +# liable for any damages resulting from thier use. +# +# The following metadata describe the data in this file: +# ---------------------------------------------------------------------------------- +# +# Site Information +# --------------------------- +# SiteCode: {sampling_feature.sampling_feature_code} +# SiteName: {sampling_feature.sampling_feature_name} +# SiteDescription: {sampling_feature.sampling_feature_description} +# Latitude: {sampling_feature.site.latitude} +# Longitude: {sampling_feature.site.longitude} +# Elevation: {sampling_feature.elevation_m} +# VerticalDatum: {sampling_feature.elevation_datum_id} +# SiteType: {sampling_feature.site.site_type_id} +# +# Variable and Unit Information +# ----------------------------- +# VariableCode: {variable.variable_code} +# VariableName: {variable.variable_name_id} +# VariableType: {variable.variable_type_id} +# VariableDefinition: {variable.variable_definition} +# NoDataValue: {variable.no_data_value} +# VariableUnitsName: {unit.unit_name} +# VariableUnitsType: {unit.unit_type_id} +# VariableUnitsAbbreviation: {unit.unit_abbreviation} +# +# Result Information +# ----------------------------- +# ResultType: {result.result_type} +# ProcessingLevelCode: {result.processing_level.processing_level_code} +# ProcessingLevelDefinition: {result.processing_level.definition} +# ProcessingLevelExplanation: {result.processing_level.explanation} +# ResultStatus: {result.status} +# SampledMedium: {result.sampled_medium_id} +# ValueCount: {result.value_count} +# +# Instrument Deployment Information +# --------------------------------- +# SensorModel: {model} +# MethodTypeCV: {action.method.method_type_id} +# MethodCode: {action.method.method_code} +# MethodName: {action.method.method_name} +# MethodDescription: {action.method.method_description} +# ResponsiblePerson: {affiliation.person} +# Email: {affiliation.primary_email} +# Affiliation: {affiliation.organization} +# diff --git a/src/dataloaderinterface/models.py b/src/dataloaderinterface/models.py index 3304f09f..197757a1 100644 --- a/src/dataloaderinterface/models.py +++ b/src/dataloaderinterface/models.py @@ -1,11 +1,130 @@ from __future__ import unicode_literals +# Create your models here. +import uuid + +from django.db.models.aggregates import Min + +from dataloader.models import SamplingFeature, Affiliation, Result, TimeSeriesResultValue, EquipmentModel, Variable, \ + Unit, Medium from django.contrib.auth.models import User from django.db import models -# Create your models here. -from dataloader.models import SamplingFeature, Affiliation +class SiteRegistration(models.Model): + registration_id = models.AutoField(primary_key=True, db_column='RegistrationID') + registration_token = models.CharField(max_length=64, editable=False, db_column='RegistrationToken', unique=True) + + registration_date = models.DateTimeField(db_column='RegistrationDate') + deployment_date = models.DateTimeField(db_column='DeploymentDate', blank=True, null=True) + + django_user = models.ForeignKey(User, on_delete=models.CASCADE, db_column='User', related_name='registrations') + affiliation_id = models.IntegerField(db_column='AffiliationID') + person = models.CharField(max_length=765, db_column='Person') + organization = models.CharField(max_length=255, db_column='Organization', blank=True, null=True) + + sampling_feature_id = models.IntegerField(db_column='SamplingFeatureID') + sampling_feature_code = models.CharField(max_length=50, unique=True, db_column='SamplingFeatureCode') + sampling_feature_name = models.CharField(max_length=255, blank=True, db_column='SamplingFeatureName') + elevation_m = models.FloatField(blank=True, null=True, db_column='Elevation') + + latitude = models.FloatField(db_column='Latitude') + longitude = models.FloatField(db_column='Longitude') + site_type = models.CharField(max_length=765, db_column='SiteType') + + followed_by = models.ManyToManyField(User, related_name='followed_sites') + + @property + def sampling_feature(self): + return SamplingFeature.objects.get(pk=self.sampling_feature_id) + + @property + def odm2_affiliation(self): + return Affiliation.objects.get(pk=self.affiliation_id) + + @property + def deployment_date(self): + min_datetime = self.sensors.aggregate(first_light=Min('activation_date')) + return min_datetime['first_light'] + + @property + def last_measurements(self): + if not self.deployment_date: + return [] + + measurement_ids = [long(measurement.last_measurement_id) for measurement in self.sensors.all() if measurement.last_measurement_id] + measurements = TimeSeriesResultValue.objects.filter(pk__in=measurement_ids) + return measurements + + @property + def latest_measurement(self): + if not self.deployment_date: + return None + return self.last_measurements.latest('value_datetime') + + def __str__(self): + return '%s by %s from %s on %s' % (self.sampling_feature_code, self.person, self.organization, self.registration_date) + + def __repr__(self): + return "" % ( + self.registration_id, self.registration_date, self.sampling_feature_code, self.person + ) + + +class SiteSensor(models.Model): + registration = models.ForeignKey('SiteRegistration', db_column='RegistrationID', related_name='sensors') + result_id = models.IntegerField(db_column='ResultID', unique=True) + result_uuid = models.UUIDField(default=uuid.uuid4, editable=False, db_column='ResultUUID', unique=True) + model_name = models.CharField(db_column='ModelName', max_length=255) + model_manufacturer = models.CharField(db_column='ModelManufacturer', max_length=255) + variable_name = models.CharField(max_length=255, db_column='VariableName') + variable_code = models.CharField(max_length=50, db_column='VariableCode') + unit_name = models.CharField(max_length=255, db_column='UnitsName') + unit_abbreviation = models.CharField(max_length=255, db_column='UnitAbbreviation') + sampled_medium = models.CharField(db_column='SampledMedium', max_length=255) + last_measurement_id = models.IntegerField(db_column='LastMeasurementID', unique=True, blank=True, null=True) + activation_date = models.DateTimeField(db_column='ActivationDate', blank=True, null=True) + activation_date_utc_offset = models.IntegerField(db_column='ActivationDateUtcOffset', blank=True, null=True) + + @property + def equipment_model(self): + return EquipmentModel.objects.filter(model_name=self.model_name).first() + + @property + def variable(self): + return Variable.objects.filter(variable_code=self.variable_code).first() + + @property + def unit(self): + return Unit.objects.filter(unit_name=self.unit_name).first() + + @property + def medium(self): + return Medium.objects.filter(name=self.sampled_medium).first() + + @property + def make_model(self): + return "{0}_{1}".format(self.model_manufacturer, self.model_name) + + @property + def sensor_identity(self): + return "{0}_{1}_{2}".format(self.registration.sampling_feature_code, self.variable_code, self.result_id) + + @property + def last_measurement(self): + return TimeSeriesResultValue.objects.filter(pk=self.last_measurement_id).first() + + @property + def result(self): + return Result.objects.get(pk=self.result_id) + + def __str__(self): + return '%s %s' % (self.variable_name, self.unit_abbreviation) + + def __repr__(self): + return "" % ( + self.id, self.registration, self.variable_code, self.unit_abbreviation, + ) class DeviceRegistration(models.Model): @@ -13,17 +132,22 @@ class DeviceRegistration(models.Model): deployment_sampling_feature_uuid = models.UUIDField(db_column='SamplingFeatureUUID') authentication_token = models.CharField(max_length=64, editable=False, db_column='AuthenticationToken') user = models.ForeignKey('ODM2User', db_column='User') + # deployment_date = models.DateTimeField(db_column='DeploymentDate') + + def registration_date(self): + action = self.sampling_feature.actions.first() + return action and action.begin_datetime + + @property + def deployment_date(self): + sampling_feature = self.sampling_feature + min_datetime = sampling_feature.feature_actions.aggregate(first_light=Min('results__valid_datetime')) + return min_datetime['first_light'] @property def sampling_feature(self): return SamplingFeature.objects.get(sampling_feature_uuid__exact=self.deployment_sampling_feature_uuid) - def registration_date(self): - return self.sampling_feature.actions.first().begin_datetime.strftime('%Y/%m/%d') - - def device_name(self): - return self.sampling_feature.sampling_feature_code - def __str__(self): action = self.sampling_feature.actions.first() return '{}\t{}: {}'.format(self.sampling_feature.sampling_feature_code, action.action_type_id, action.begin_datetime.strftime('%Y/%m/%d')) @@ -36,3 +160,6 @@ class ODM2User(models.Model): @property def affiliation(self): return Affiliation.objects.get(pk=self.affiliation_id) + + def can_administer_site(self, registration): + return self.user.is_staff or registration.user == self diff --git a/src/dataloaderinterface/static/dataloaderinterface/css/browse-sites.css b/src/dataloaderinterface/static/dataloaderinterface/css/browse-sites.css index 2e04c87c..200c0a8f 100644 --- a/src/dataloaderinterface/static/dataloaderinterface/css/browse-sites.css +++ b/src/dataloaderinterface/static/dataloaderinterface/css/browse-sites.css @@ -20,22 +20,6 @@ span.map-tip { padding: 7px 15px; } -div#marker-content { - padding: 5px; -} - -.site-field, .site-link { - padding: 3px; -} - -span.site-label { - font-weight: bold; -} - -span.site-label::after { - content: ': '; -} - .browse-sites-header { display: inline-block; position: absolute; @@ -49,4 +33,5 @@ span.site-label::after { .browse-sites-header .page-title { margin: 0; + font-size: 14px; } diff --git a/src/dataloaderinterface/static/dataloaderinterface/css/my-sites.css b/src/dataloaderinterface/static/dataloaderinterface/css/my-sites.css index 74ab738c..e69de29b 100644 --- a/src/dataloaderinterface/static/dataloaderinterface/css/my-sites.css +++ b/src/dataloaderinterface/static/dataloaderinterface/css/my-sites.css @@ -1,15 +0,0 @@ -div#marker-content { - padding: 5px; -} - -.site-field, .site-link { - padding: 3px; -} - -span.site-label { - font-weight: bold; -} - -span.site-label::after { - content: ': '; -} \ 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 6c1fc9f8..9f24f119 100644 --- a/src/dataloaderinterface/static/dataloaderinterface/css/style.css +++ b/src/dataloaderinterface/static/dataloaderinterface/css/style.css @@ -35,7 +35,8 @@ button.navbar-toggle { @media (max-width: 890px) { .navbar-header { - float: none; !important; + float: none; + !important; } .navbar-left, .navbar-right { @@ -53,7 +54,8 @@ button.navbar-toggle { .navbar-fixed-top { top: 0; - border-width: 0 0 1px; !important; + border-width: 0 0 1px; + !important; } .navbar-collapse.collapse { @@ -66,12 +68,14 @@ button.navbar-toggle { } .navbar-nav > li { - float: none; !important; + float: none; + !important; } .navbar-nav > li > a { padding-top: 10px; - padding-bottom: 10px; !important; + padding-bottom: 10px; + !important; } .collapse.in { @@ -111,7 +115,6 @@ button.navbar-toggle { div.map-info { margin: 5px; - top: 5px !important; left: 0 !important; box-shadow: rgba(0, 0, 0, 0.298039) 0 1px 4px -1px; background-color: rgba(255, 255, 255, .95); @@ -120,7 +123,6 @@ div.map-info { padding: 7px 15px; } - .control-row { clear: both; } @@ -190,8 +192,9 @@ div.separator { .modal-header { padding: 15px; border-bottom: 1px solid #e5e5e5; - background-color: #353535; - color: #FFF; + background-color: #f6f7f9; + color: #000; + border-radius: 4px; } .select2-results__option[aria-disabled="true"] { @@ -213,43 +216,51 @@ div.form-field { } /*#results-template {*/ - /*display: none;*/ - /*visibility: hidden;*/ +/*display: none;*/ +/*visibility: hidden;*/ /*}*/ /*span.remove-result {*/ - /*font-size: 1.3em;*/ - /*cursor: pointer;*/ - /*position: absolute;*/ - /*top: 14px;*/ - /*right: 30px;*/ +/*font-size: 1.3em;*/ +/*cursor: pointer;*/ +/*position: absolute;*/ +/*top: 14px;*/ +/*right: 30px;*/ /*}*/ .inline { display: inline-block; } +td[data-behaviour="delete"] input { + display: none; +} + +tr.deleted-row { + display: none; +} + #new-result-button { vertical-align: baseline; margin-left: 10px; padding: 4px 10px; } -span.copy-icon { - font-size: 1.3em; - cursor: pointer; - vertical-align: text-bottom; +div.registration-data { + margin-top: 2px; + margin-bottom: 20px; + overflow: auto; } -span.copy-key-icon { +div.registration-data i, div.registration-data .material-icons { + width: 10px; margin-right: 10px; - font-size: 1.4em; - cursor: pointer; } -div.registration-data { - margin-bottom: 20px; - overflow: auto; +div.registration-data .material-icons { + font-size: 15px; + margin-left: -2px; + margin-right: 12px; } div.registration-field { @@ -261,34 +272,84 @@ div.registration-field span.control-label { font-weight: 700; } -div.registration-field span.control-field { - -} - .plot_box { float: left; width: 100%; - margin-bottom: 15px; + margin-bottom: 30px; border-radius: 2px; - border: 1px solid #DDD; - min-height: 174px; + min-height: 180px; +} + +svg.stale { + background-color: #ffefef; +} + +svg.not-stale { + background-color: #f8fff7; +} + +.last-observation { + font-weight: bold; } .plot_box .mdl-card__actions { border-top: 0; + background: #f6f6f6; +} + +.white-shadow { + text-shadow: 0 1px 0 #fff; +} + +.help-icon { + font-size: 18px; + line-height: 0; + vertical-align: sub; + cursor: default; +} + +.buttons-toolbar .button-link { + margin-bottom: 20px; + width: 100%; +} + +.buttons-toolbar .button-link:hover { + text-decoration: none; } .table-trigger { cursor: pointer; } +.table-trigger.disabled, .download-trigger.disabled, .tsa-trigger.disabled { + pointer-events: none; + color: #c5c5c5; +} + +.measurements-table { + max-height: 350px; + overflow: auto; +} + +.measurements-table .mdl-data-table tbody tr:hover { + background-color: initial; +} + .variable-description { width: 120px; - padding: 20px; } .variable-name { - padding: 8px 20px; + font-size: 0.8em; +} + +.variable-info { + font-size: 0.8em; +} + +.variable-info td { + padding-right: 10px; + padding-bottom: 3px; } .variable-data { @@ -303,21 +364,21 @@ div.registration-field span.control-field { margin-bottom: 3px; } -.variable-code { - font-size: 0.8em; - margin-bottom: 3px; -} - .latest-value { font-weight: bold; font-size: 1.5em; - margin-top: 5px; + margin: 5px 0; +} + +.unit { + font-size: 0.8em; } .graph-container { border: 1px solid #DDD; height: 80px; margin: 10px; + margin-bottom: 5px; } .plot_box div p { @@ -349,8 +410,7 @@ div.registration-field span.control-field { } .line { - fill: rgb(232, 237, 240); - stroke: steelblue; + fill: none; stroke-width: 2px; } @@ -388,6 +448,11 @@ span.form-message.error { margin-top: 5px; } +.sampling-feature-fields .help-icon { + float: right; + margin-top: 10px; +} + div.form-group.sampling-feature-fields { display: -webkit-box; display: -moz-box; @@ -418,22 +483,13 @@ div.form-field[data-field="elevation_m"], div.form-field[data-field="elevation_d order: 2; } -div.form-field[data-field="affiliation"] .select2-selection { - border: none; - border-radius: 0; - box-shadow: none; - font-size: 2em; -} - -div.form-field[data-field="affiliation"] .select2-selection__arrow b { - border-width: 8px 6px; - margin-left: -14px !important; - margin-top: 9px; +.select2-selection__rendered { + font-family: Roboto, Helvetica, Arial, sans-serif; } -div.form-field[data-field="affiliation"] .select2-container--bootstrap.select2-container--open .select2-selection__arrow b { - border-width: 8px 6px; - margin-top: 1px; +span.select2-selection__clear { + font-size: 1.4em; + line-height: 1.1em; } span.name-dropdown { @@ -454,7 +510,6 @@ ul.errorlist { color: rgb(232, 0, 0); } - .organization-fields, .organization-fields .form-field { padding-left: 15px; } @@ -502,18 +557,6 @@ ul.errorlist { min-height: 80px; } -/*DATATABLES STYLING*/ -table.data-values, table.dataTable { - display: table; !important -} - -table.dataTable { - margin-bottom: 0; -} - -table.dataTable.table-bordered > thead > tr > th { - border-bottom-width: 1px; -} /*FOOTER STYLING*/ footer { background-color: #353535; @@ -558,21 +601,111 @@ footer .separator { /*GENERAL STYLING*/ -.btn-copy { - background-color: #FFF !important; - cursor: default; +#btn-follow { + top: -3px; + left: 15px; + position: absolute; +} + +#icon-follow, #icon-unfollow { + position: absolute; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + -wekbit-transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + display: block; +} + +.follow-status { + margin-bottom: 20px; +} + +.follow-status.following #btn-follow { + background-color: orange; } -.plot_box .input-group-copy { - width: calc(100% - 50px); +.follow-status:not(.following) #icon-unfollow { + opacity: 0; + visibility: hidden; + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + top: 8px; + left: 8px; } -.input-group-copy .material-icons { + +.follow-status.following #icon-follow { + opacity: 0; + visibility: hidden; + top: 8px; + left: 8px; + -webkit-transform: rotate(225deg); + transform: rotate(225deg); +} + +.chip-follow .fa-check-circle-o { + font-size: 18px; + vertical-align: middle; + display: inline-block; + margin-bottom: 2px; +} + +.chip-follow { + padding: 0; + margin-left: 10px; + transition: width 0.4s; + -wekbit-transition: width 0.4s; + overflow: hidden; +} + +.follow-status.following #follow-chip { + width: 225px; +} + +.text-green { + color: #00bf00; +} + +.follow-status:not(.following) #follow-chip { + width: 0; +} + +.chip-follow .mdl-chip__text { + padding-left: 35px; +} + +.my-sites-action { + margin-top: 32px; + text-align: left; +} + +.my-sites-action:hover { + text-decoration: none; +} + +.center-icon { + display: inline-block; + vertical-align: middle; + margin-bottom: 2px; +} + +#btn-delete-site { + background-color: #d9534f; + border-color: #d43f3a; +} + +.result-uuid { + border-bottom: 0 !important; +} + +.site-tokens .material-icons { font-size: 14px; } -.plot_box .table-trigger { +.site-tokens { + margin-bottom: 20px; +} + +.plot_box .fa { margin-left: 13px; - margin-top: 2px; + margin-top: 10px; } dialog#data-table-dialog { @@ -583,25 +716,15 @@ dialog#data-table-dialog .mdl-dialog__content { color: #555; } -.input-group-copy .input-group-addon { - background-color: #FFF !important; -} - .site-tokens .input-group-copy .input-group-addon:first-child { width: 186px; text-align: left; } -.btn-copy:hover { - background-color: #eeeeee !important; -} - -.btn-copy:active { - background-color: #DDD !important; -} .full-width { width: 100%; } + .mdl-data-table td:last-of-type { white-space: normal; } @@ -643,13 +766,17 @@ dialog#data-table-dialog .mdl-dialog__content { } .ribbon-blue { - background-color: #337ab7 + background-color: #337ab7; } .ribbon-green { background-color: #62843e; } +.ribbon-dark { + background-color: #494949; +} + .ribbon-content { margin-top: -35vh; -webkit-flex-shrink: 0; @@ -669,7 +796,7 @@ dialog#data-table-dialog .mdl-dialog__content { } .shadow { - box-shadow: 0 4px 5px 0 rgba(0,0,0,.14), 0 1px 10px 0 rgba(0,0,0,.12), 0 2px 4px -1px rgba(0,0,0,.2); + box-shadow: 0 4px 5px 0 rgba(0, 0, 0, .14), 0 1px 10px 0 rgba(0, 0, 0, .12), 0 2px 4px -1px rgba(0, 0, 0, .2); } .text-shadow { @@ -677,7 +804,7 @@ dialog#data-table-dialog .mdl-dialog__content { } .btn.sharp { - border-radius:0; + border-radius: 0; } .card { @@ -686,6 +813,10 @@ dialog#data-table-dialog .mdl-dialog__content { margin-bottom: 50px; } +#how-it-works .card { + height: 300px; +} + .card h4 { font-size: 18px; font-family: "Roboto", "Helvetica", "Arial", sans-serif; @@ -704,9 +835,16 @@ dialog#data-table-dialog .mdl-dialog__content { .site-card .mdl-card__supporting-text { color: #252525; + width: 100%; +} + +.site-card .mdl-card__title-text a { + color: #FFF; + font-weight: 300; } .site-card .mdl-card__title { + padding-right: 60px; background-color: #92d050; color: #FFF; } @@ -718,26 +856,48 @@ dialog#data-table-dialog .mdl-dialog__content { .site-card { margin-top: 20px; width: auto; + height: 100%; +} + +.flex { + display: flex; +} + +.mdl-dialog__title { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + font-size: 1.5rem; +} + +.site { + flex-shrink: 0; } i.material-icons.site-card-icon { margin-right: 8px; color: #92d050; + max-width: 24px; } .page-title, .section-title { margin: 40px auto; text-transform: uppercase; color: rgba(0, 0, 0, 0.541176); - font-size: 16px; + font-size: 18px; font-weight: 500; font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif; } +.section-subtitle { + margin-top: -30px; + margin-bottom: 30px; +} #new-result-button { - margin-left: 20px; + margin-left: 10px; margin-bottom: -10px; + vertical-align: sub; } /*LOGIN STYLING*/ @@ -772,10 +932,124 @@ a.orange:hover { background-color: rgb(101, 101, 101); } +.buttons-toolbar { + margin-bottom: 20px; +} + +.noselect { + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; + /* Non-prefixed version, currently supported by Chrome and Opera */ +} + +#code-output { + height: auto; + font-size: 12px; +} + +#site-table-header { + font-weight: 500; + font-size: 16px; + border-top: 6px solid #90a877; +} + label.checkbox-label { color: #FFF !important; } #agreement-icon { margin-left: 18px; +} + +/*PROFILE STYLING*/ +#profile-banner { + height: 35vh; + background: #5d688a; + margin: -80px -4vw 40px -4vw; + padding-right: 4vw; + padding-left: 4vw; + display: flex; + color: #FFF; + +} + +#profile-banner h3 { + align-self: flex-end; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3); +} + +.profile-name > i { + font-size: 84px; + display: block; +} + +#btn-edit-profile { + margin-top: -68px; +} + +#btn-cancel-profile-edit, #btn-update-user { + margin-top: -60px; +} + +#btn-cancel-profile-edit { + right: 145px; + background-color: #ffffff; +} + +#profile-details-table tr td:first-child { + padding-right: 24px; +} + +.d-block { + display: block; +} + +.alert-error { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; +} + +/*TRANSITIONS*/ +.mdl-button--trans { + -webkit-transition: all 300ms; + -moz-transition: all 300ms; + -ms-transition: all 300ms; + -o-transition: all 300ms; + transition: all 300ms; + transform: scale(0); +} + +.fab-trans { + transform: scale(1); +} + +/*MARKERS TABLE*/ + +.site-field { + padding-bottom: 3px; +} + +.marker-info tr:not(.site-link) { + border-bottom: 1px solid #DDD; +} + +.marker-info th, .marker-info td { + padding: 6px; +} + +.marker-info td { + max-width: 150px; +} + +.marker-info th { + display: inline-block; +} + +.site-link { + margin-top: 20px; + display: block; } \ No newline at end of file diff --git a/src/dataloaderinterface/static/dataloaderinterface/js/bootstrap-toolkit.min.js b/src/dataloaderinterface/static/dataloaderinterface/js/bootstrap-toolkit.min.js new file mode 100644 index 00000000..018fae27 --- /dev/null +++ b/src/dataloaderinterface/static/dataloaderinterface/js/bootstrap-toolkit.min.js @@ -0,0 +1,8 @@ +/*! + * Responsive Bootstrap Toolkit + * Author: Maciej Gurban + * License: MIT + * Version: 2.5.1 (2015-11-02) + * Origin: https://github.com/maciej-gurban/responsive-bootstrap-toolkit + */ +;var ResponsiveBootstrapToolkit=function(i){var e={detectionDivs:{bootstrap:{xs:i('
'),sm:i('
'),md:i('
'),lg:i('
')},foundation:{small:i('
'),medium:i('
'),large:i('
'),xlarge:i('
')}},applyDetectionDivs:function(){i(document).ready(function(){i.each(o.breakpoints,function(i){o.breakpoints[i].appendTo(".responsive-bootstrap-toolkit")})})},isAnExpression:function(i){return"<"==i.charAt(0)||">"==i.charAt(0)},splitExpression:function(i){var e=i.charAt(0),o="="==i.charAt(1)?!0:!1,s=1+(o?1:0),n=i.slice(s);return{operator:e,orEqual:o,breakpointName:n}},isAnyActive:function(e){var s=!1;return i.each(e,function(i,e){return o.breakpoints[e].is(":visible")?(s=!0,!1):void 0}),s},isMatchingExpression:function(i){var s=e.splitExpression(i),n=Object.keys(o.breakpoints),r=n.indexOf(s.breakpointName);if(-1!==r){var t=0,a=0;"<"==s.operator&&(t=0,a=s.orEqual?++r:r),">"==s.operator&&(t=s.orEqual?r:++r,a=void 0);var c=n.slice(t,a);return e.isAnyActive(c)}}},o={interval:300,framework:null,breakpoints:null,is:function(i){return e.isAnExpression(i)?e.isMatchingExpression(i):o.breakpoints[i]&&o.breakpoints[i].is(":visible")},use:function(i,s){o.framework=i.toLowerCase(),o.breakpoints="bootstrap"===o.framework||"foundation"===o.framework?e.detectionDivs[o.framework]:s,e.applyDetectionDivs()},current:function(){var e="unrecognized";return i.each(o.breakpoints,function(i){o.is(i)&&(e=i)}),e},changed:function(i,e){var s;return function(){clearTimeout(s),s=setTimeout(function(){i()},e||o.interval)}}};return i(document).ready(function(){i('
').appendTo("body")}),null===o.framework&&o.use("bootstrap"),o}(jQuery); diff --git a/src/dataloaderinterface/static/dataloaderinterface/js/browse-sites.js b/src/dataloaderinterface/static/dataloaderinterface/js/browse-sites.js index d8e5610e..df61464e 100644 --- a/src/dataloaderinterface/static/dataloaderinterface/js/browse-sites.js +++ b/src/dataloaderinterface/static/dataloaderinterface/js/browse-sites.js @@ -1,17 +1,32 @@ function initMap() { - const DEFAULT_ZOOM = 6; - const DEFAULT_LATITUDE = 37.0902; + const DEFAULT_ZOOM = 5; + const DEFAULT_SPECIFIC_ZOOM = 12; + const DEFAULT_LATITUDE = 40.0902; const DEFAULT_LONGITUDE = -95.7129; const DEFAULT_POSITION = { lat: DEFAULT_LATITUDE, lng: DEFAULT_LONGITUDE }; + var ZOOM_LEVEL = sessionStorage && parseInt(sessionStorage.getItem('CURRENT_ZOOM')) || DEFAULT_ZOOM; + var temp = sessionStorage.getItem('CURRENT_CENTER'); + var MAP_CENTER = DEFAULT_POSITION; + + if(sessionStorage.getItem('CURRENT_CENTER')) { + MAP_CENTER = getLatLngFromString(temp); + } else if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition(function getLocation(locatedPosition) { + map.setCenter({ lat: locatedPosition.coords.latitude, lng: locatedPosition.coords.longitude }); + map.setZoom(DEFAULT_SPECIFIC_ZOOM); + }, undefined, { timeout: 5000 }); + } var markerData = JSON.parse(document.getElementById('sites-data').innerHTML); + var map = new google.maps.Map(document.getElementById('map'), { - center: DEFAULT_POSITION, - zoom: DEFAULT_ZOOM, + center: MAP_CENTER, + zoom: ZOOM_LEVEL, disableDefaultUI: true, + gestureHandling: 'greedy', zoomControl: true, zoomControlOptions: { - position: google.maps.ControlPosition.LEFT_BOTTOM + position: google.maps.ControlPosition.LEFT_BOTTOM }, mapTypeId: google.maps.MapTypeId.ROADMAP, scaleControl: true @@ -21,6 +36,19 @@ function initMap() { content: '' }); + map.addListener('zoom_changed', function(){ + var CURRENT_ZOOM = map.getZoom(); + + sessionStorage.setItem('CURRENT_ZOOM', CURRENT_ZOOM); + }); + + map.addListener('center_changed', function(){ + var CURRENT_CENTER = map.getCenter(); + + sessionStorage.setItem('CURRENT_CENTER', CURRENT_CENTER); + }); + + markerData.forEach(function(site) { var marker = new google.maps.Marker({ position: { lat: site.latitude, lng: site.longitude }, @@ -35,7 +63,7 @@ function initMap() { $(element).find('.site-data').text(site[field]); }); contentElement.find('.site-link a').attr('href', site.detail_link); - + var infoContent = $('
').append(contentElement.html()).html(); infoWindow.setContent(infoContent); infoWindow.open(marker.get('map'), marker); @@ -55,3 +83,10 @@ $( window ).resize(function() { $(".map-container").css("height", $("#wrapper").height()); $("body").css("overflow", "hidden") }); + +function getLatLngFromString(location) { + var latlang = location.replace(/[()]/g,''); + var latlng = latlang.split(','); + var locate = new google.maps.LatLng(parseFloat(latlng[0]) , parseFloat(latlng[1])); + return locate; +} \ No newline at end of file diff --git a/src/dataloaderinterface/static/dataloaderinterface/js/common-forms.js b/src/dataloaderinterface/static/dataloaderinterface/js/common-forms.js index 06585424..c4b21b4a 100644 --- a/src/dataloaderinterface/static/dataloaderinterface/js/common-forms.js +++ b/src/dataloaderinterface/static/dataloaderinterface/js/common-forms.js @@ -4,11 +4,15 @@ function initializeSelect(select) { setTimeout(function () { - select.select2({ - theme: "bootstrap", - containerCssClass : "input-sm", - dropdownAutoWidth: true, - width: 'auto' + select.each(function(index, selectElement) { + $(selectElement).select2({ + theme: "bootstrap", + containerCssClass : "input-sm", + dropdownAutoWidth: true, + width: 'auto', + allowClear: true, + placeholder: $(selectElement).attr('placeholder') + }); }); }); } diff --git a/src/dataloaderinterface/static/dataloaderinterface/js/copyToClipboard.js b/src/dataloaderinterface/static/dataloaderinterface/js/copyToClipboard.js index 463d4b39..c84d9b63 100644 --- a/src/dataloaderinterface/static/dataloaderinterface/js/copyToClipboard.js +++ b/src/dataloaderinterface/static/dataloaderinterface/js/copyToClipboard.js @@ -24,6 +24,7 @@ $(document).ready(function () { }); function copyToClipboard(elem, event) { + var tempScrollTop = $(window).scrollTop(); // Save current scroll position // create hidden text element, if it doesn't already exist var targetId = "_hiddenCopyText_"; var isInput = elem.tagName === "INPUT" || elem.tagName === "TEXTAREA"; @@ -76,5 +77,7 @@ function copyToClipboard(elem, event) { target.remove(); } + $(window).scrollTop(tempScrollTop); // Restore scroll position + return succeed; } \ No newline at end of file diff --git a/src/dataloaderinterface/static/dataloaderinterface/js/device-detail.js b/src/dataloaderinterface/static/dataloaderinterface/js/device-detail.js index 6289cdb2..2a51cad3 100644 --- a/src/dataloaderinterface/static/dataloaderinterface/js/device-detail.js +++ b/src/dataloaderinterface/static/dataloaderinterface/js/device-detail.js @@ -1,38 +1,18 @@ -$(document).ready(function () { - var dialog = document.querySelector('#data-table-dialog'); - if (!dialog.showModal) { - dialogPolyfill.registerDialog(dialog); - } - - $(".table-trigger").click(function(){ - var box = $(this).parents('.plot_box'); - var id = box.data('result-id'); - var tables = $('table.data-values'); - tables.hide(); - - tables.filter('[data-result-id="' + id + '"]').show(); - $(dialog).find('.mdl-dialog__title').text(box.data('variable-name') + ' (' + box.data('variable-code') + ')'); - - dialog.showModal(); - }); - - dialog.querySelector('.dialog-close').addEventListener('click', function () { - dialog.close(); - }); -}); - +const EXTENT_HOURS = 72; +const GAP_HOURS = 6; +const STALE_DATA_CUTOFF = new Date(new Date() - 1000 * 60 * 60 * EXTENT_HOURS); function initMap() { var defaultZoomLevel = 18; var latitude = parseFloat($('#site-latitude').val()); var longitude = parseFloat($('#site-longitude').val()); - var sitePosition = { lat: latitude, lng: longitude }; + var sitePosition = {lat: latitude, lng: longitude}; var map = new google.maps.Map(document.getElementById('map'), { center: sitePosition, - scrollwheel: false, + gestureHandling: 'greedy', zoom: defaultZoomLevel, - mapTypeId: google.maps.MapTypeId.ROADMAP + mapTypeId: google.maps.MapTypeId.SATELLITE }); var marker = new google.maps.Marker({ @@ -41,85 +21,278 @@ function initMap() { }); } -function plotValues(result_id, values) { - var plotBox = $('div.plot_box[data-result-id="' + result_id + '"] div.graph-container'); +// Makes all site cards have the same height. +function fixViewPort() { + var cards = $('.plot_box'); - var margin = {top: 0, right: 0, bottom: 0, left: 0}; + var maxHeight = 0; + for (var i = 0; i < cards.length; i++) { + maxHeight = Math.max($(cards[i]).height(), maxHeight); + } + + // set to new max height + for (var i = 0; i < cards.length; i++) { + $(cards[i]).height(maxHeight); + } +} + +function bindDeleteDialogEvents() { + var deleteDialog = document.querySelector('#site-delete-dialog'); + var deleteButton = document.querySelector('#btn-delete-site'); + + if (!deleteButton) { + return; + } + + if (!deleteDialog.showModal) { + dialogPolyfill.registerDialog(deleteDialog); + } + + deleteButton.addEventListener('click', function () { + deleteDialog.showModal(); + }); + + deleteDialog.querySelector('.dialog-close').addEventListener('click', function () { + deleteDialog.close(); + }); + + deleteDialog.querySelector('.confirm-delete').addEventListener('click', function () { + deleteDialog.close(); + }); +} + +// Returns the most recent 72 hours since the last reading +function getRecentData(timeSeriesData) { + var lastRead = Math.max.apply(Math, timeSeriesData.map(function(value){ + return new Date(value.DateTime); + })); + + var dataTimeOffset = new Date(lastRead - 1000 * 60 * 60 * EXTENT_HOURS); + return timeSeriesData.filter(function (value) { + return (new Date(value.DateTime)) >= dataTimeOffset; + }); +} + +function fillValueTable(table, data) { + var rows = data.map(function (dataValue) { + return "" + dataValue.DateTime + "" + dataValue.TimeOffset + "" + dataValue.Value + ""; + }); + table.append($(rows.join(''))); +} + +function drawSparklineOnResize(seriesInfo, seriesData) { + var resizeTimer; + window.addEventListener('resize', function (event) { + clearTimeout(resizeTimer); + resizeTimer = setTimeout(function () { + drawSparklinePlot(seriesInfo, seriesData); + }, 500); + }); +} + +function drawSparklinePlot(seriesInfo, seriesData) { + var card = $('div.plot_box[data-result-id="' + seriesInfo['resultId'] + '"]'); + var plotBox = card.find(".graph-container"); + var $lastObservation = card.find(".last-observation"); + + plotBox.empty(); + + var margin = {top: 5, right: 1, bottom: 5, left: 1}; var width = plotBox.width() - margin.left - margin.right; var height = plotBox.height() - margin.top - margin.bottom; + if (seriesData.length === 0) { + card.find(".table-trigger").toggleClass("disabled", true); + card.find(".download-trigger").toggleClass("disabled", true); + card.find(".tsa-trigger").toggleClass("disabled", true); + + // Append message when there is no data + d3.select(plotBox.get(0)).append("svg") + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .append("text") + .text("No data exist for this variable.") + .attr("font-size", "12px") + .attr("fill", "#AAA") + .attr("text-anchor", "left") + .attr("transform", "translate(" + (margin.left + 10) + "," + (margin.top + 20) + ")"); + return; + } + + $('.plot_box[data-result-id=' + seriesInfo['resultId'] + ' ]').find('.latest-value').text(seriesData[seriesData.length - 1].Value); + + var lastRead = Math.max.apply(Math, seriesData.map(function(value){ + return new Date(value.DateTime); + })); + + $lastObservation.text(formatDate(lastRead)); + + var dataTimeOffset = new Date(lastRead - 1000 * 60 * 60 * EXTENT_HOURS); + var xAxis = d3.scaleTime().range([0, width]); var yAxis = d3.scaleLinear().range([height, 0]); - xAxis.domain(d3.extent(values, function (d) { - return d.index; - })); - yAxis.domain(d3.extent(values, function (d) { - return parseFloat(d.value); + xAxis.domain([dataTimeOffset, lastRead]); + yAxis.domain(d3.extent(seriesData, function(d) { + return parseInt(d.Value); })); var line = d3.line() .x(function(d) { - return xAxis(d.index); + var date = new Date(d.DateTime); + return xAxis(date); }) .y(function(d) { - return yAxis(d.value); + return yAxis(d.Value); }); + var svg = d3.select(plotBox.get(0)).append("svg") .attr("width", width + margin.left + margin.right) + .attr("class", function() { + if (lastRead <= STALE_DATA_CUTOFF) { + return "stale"; + } + + return "not-stale"; + }) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); - svg.append("path").data([values]).attr("class", "line").attr("d", line); -} + // Rendering the paths + var gapOffset; // The minimum date required before being considered a gap. + var previousDate; + var start = 0; // Latest start detected after a gap. Initially set to the start of the list. + var paths = []; + + for (var i = 0; i < seriesData.length; i++) { + var currentDate = new Date(seriesData[i].DateTime); + + if (previousDate) { + gapOffset = new Date(currentDate - 1000 * 60 * 60 * GAP_HOURS); + } + + if (previousDate && previousDate < gapOffset) { + paths.push(seriesData.slice(start, i)); + start = i; + } + previousDate = currentDate; + } -function drawSparklinePlots(tableData) { - $('div.graph-container').empty(); - for (var index = 0; index < tableData.length; index++) { - plotValues(tableData[index]['id'], tableData[index]['data']); + if (start > 0) { + paths.push(seriesData.slice(start, seriesData.length)); + } + else { + paths.push(seriesData); // No gaps were detected. Just plot the entire original data. + } + + // Plot all paths separately to display gaps between them. + for (var i = 0; i < paths.length; i++) { + if (paths[i].length == 1) { + svg.append("circle") + .attr("r", 2) + .style("fill", "steelblue") + .attr("transform", "translate(" + xAxis(new Date(paths[i][0].DateTime)) + ", " + yAxis(paths[i][0].Value) + ")") + } + else { + svg.append("path") + .data([paths[i]]) + .attr("class", "line").attr("d", line) + .attr("stroke", "steelblue"); + } } } -function initializeTable(table) { - return table.dataTable({ - info: false, - ordering: true, - paging: false, - searching: false, - scrollY: '700', - scrollCollapse: true +function getTimeSeriesData(sensorInfo) { + Papa.parse(sensorInfo['csvPath'], { + download: true, + header: true, + worker: true, + comments: "#", + skipEmptyLines: true, + complete: function (result) { + if (result.data) { + var recentValues = getRecentData(result.data); + fillValueTable($('table.data-values[data-result-id=' + sensorInfo['resultId'] + ']'), recentValues); + drawSparklineOnResize(sensorInfo, recentValues); + drawSparklinePlot(sensorInfo, recentValues); + } + } }); } -$(document).ready(function() { - $('nav .menu-sites-list').addClass('active'); - - var resizeTimer; - var tablesData = []; - var plotBoxes = $('div.plot_box'); - - var tables = initializeTable($('table.data-values')); - - for (var index = 0; index < tables.length; index++) { - var table = $(tables.get(index)); - tablesData[index] = { - table: table, - id: table.data('result-id'), - data: tables.api().table(index).data().map(function(data, index) { - return { index: index, value: data[1] } - }) - } +$(document).ready(function () { + var dialog = document.querySelector('#data-table-dialog'); + if (!dialog.showModal) { + dialogPolyfill.registerDialog(dialog); } - tables.api().destroy(); - $(window).on('resize', function(event) { - clearTimeout(resizeTimer); - resizeTimer = setTimeout(function() { - drawSparklinePlots(tablesData); - }, 500); + $("#btn-follow").on("click", function () { + var statusContainer = $(".follow-status"); + var followForm = $("#follow-site-form"); + var following = statusContainer.hasClass('following'); + var tooltip = $(".mdl-tooltip[data-mdl-for='btn-follow']"); + + $.ajax({ + url: $('#follow-site-api').val(), + type: 'post', + data: { + csrfmiddlewaretoken: followForm.find('input[name="csrfmiddlewaretoken"]').val(), + sampling_feature_code: followForm.find('input[name="sampling_feature_code"]').val(), + action: (following)?'unfollow':'follow' + }}).done(function(data) { + statusContainer.toggleClass("following"); + tooltip.text((following)?'Follow':'Unfollow'); + }); + }); + + $(".table-trigger").click(function () { + var box = $(this).parents('.plot_box'); + var id = box.data('result-id'); + var tables = $('table.data-values'); + tables.hide(); + + tables.filter('[data-result-id="' + id + '"]').show(); + var title = box.data('variable-name') + ' (' + box.data('variable-code') + ')'; + $(dialog).find('.mdl-dialog__title').text(title); + $(dialog).find('.mdl-dialog__title').attr("title", title); + + dialog.showModal(); }); - drawSparklinePlots(tablesData); - plotBoxes.first().trigger('click'); + dialog.querySelector('.dialog-close').addEventListener('click', function () { + dialog.close(); + }); + + $('nav .menu-sites-list').addClass('active'); + + + var sensors = document.querySelectorAll('.device-data .plot_box'); + for (var index = 0; index < sensors.length; index++) { + var sensorInfo = sensors[index].dataset; + getTimeSeriesData(sensorInfo); + } + + bindDeleteDialogEvents(); + + // Executes when page loads + fixViewPort(); + + // Executes each time window size changes + $(window).resize( + ResponsiveBootstrapToolkit.changed(function () { + $('.plot_box').height("initial"); // Reset height + fixViewPort(); + })); }); + +function formatDate(date) { + date = new Date(date); + + var options = { + year: "numeric", month: "short", + day: "numeric", hour: "2-digit", minute: "2-digit" + }; + + return date.toLocaleTimeString("en-us", options); +} diff --git a/src/dataloaderinterface/static/dataloaderinterface/js/my-sites.js b/src/dataloaderinterface/static/dataloaderinterface/js/my-sites.js index b0b414db..23d2efdf 100644 --- a/src/dataloaderinterface/static/dataloaderinterface/js/my-sites.js +++ b/src/dataloaderinterface/static/dataloaderinterface/js/my-sites.js @@ -7,21 +7,45 @@ function initMap() { const DEFAULT_LATITUDE = 37.0902; const DEFAULT_LONGITUDE = -95.7129; const DEFAULT_POSITION = { lat: DEFAULT_LATITUDE, lng: DEFAULT_LONGITUDE }; + var ZOOM_LEVEL = sessionStorage && parseInt(sessionStorage.getItem('MY_CURRENT_ZOOM')) || DEFAULT_ZOOM; + var temp = sessionStorage.getItem('MY_CURRENT_CENTER'); + + if(sessionStorage.getItem('MY_CURRENT_CENTER')){ + var CUR_CENTER = getLatLngFromString(temp); + var MAP_CENTER = CUR_CENTER; + } + else{ + var MAP_CENTER = DEFAULT_POSITION; + } var markerData = JSON.parse(document.getElementById('sites-data').innerHTML); + var map = new google.maps.Map(document.getElementById('map'), { - center: DEFAULT_POSITION, - zoom: DEFAULT_ZOOM, - scrollwheel: false, + center: MAP_CENTER, + zoom: ZOOM_LEVEL, disableDefaultUI: true, zoomControl: true, + gestureHandling: 'greedy', zoomControlOptions: { - position: google.maps.ControlPosition.LEFT_BOTTOM + position: google.maps.ControlPosition.LEFT_BOTTOM }, mapTypeId: google.maps.MapTypeId.ROADMAP, scaleControl: true }); + map.addListener('zoom_changed', function(){ + var CURRENT_ZOOM = map.getZoom(); + + sessionStorage.setItem('MY_CURRENT_ZOOM', CURRENT_ZOOM); + }); + + map.addListener('center_changed', function(){ + var CURRENT_CENTER = map.getCenter(); + + sessionStorage.setItem('MY_CURRENT_CENTER', CURRENT_CENTER); + }); + + var bounds = new google.maps.LatLngBounds(); var infoWindow = new google.maps.InfoWindow({ content: '' }); @@ -32,6 +56,8 @@ function initMap() { map: map }); + bounds.extend(marker.getPosition()); + marker.addListener('click', function() { var infoContent = createInfoWindowContent(site); infoWindow.setContent(infoContent); @@ -46,8 +72,16 @@ function initMap() { var infoContent = createInfoWindowContent(data); event.data['info-window'].setContent(infoContent); event.data['info-window'].open(marker.get('map'), marker); + + $('html, body').animate({ + scrollTop: 0 + }, 150); }); }); + + if (!sessionStorage.getItem('MY_CURRENT_CENTER')){ + map.fitBounds(bounds); + } } function createInfoWindowContent(siteInfo) { @@ -62,7 +96,39 @@ function createInfoWindowContent(siteInfo) { return $('
').append(contentElement.html()).html(); } -$(document).ready(function() { +// Makes all site cards have the same height. +function fixViewPort() { + var cards = $('.site-card'); + + var maxHeight = 0; + for (var i = 0; i < cards.length; i++) { + maxHeight = Math.max($(cards[i]).height(), maxHeight); + } + + // set to new max height + for (var i = 0; i < cards.length; i++) { + $(cards[i]).height(maxHeight); + } +} + +$(document).ready(function () { $('nav .menu-sites-list').addClass('active'); -}); \ No newline at end of file + // Executes when page loads + fixViewPort(); + + // Executes each time window size changes + $(window).resize( + ResponsiveBootstrapToolkit.changed(function () { + $('.site-card').height("initial"); // Reset height + fixViewPort(); + }) + ); +}); + +function getLatLngFromString(location) { + var latlang = location.replace(/[()]/g,''); + var latlng = latlang.split(','); + var locate = new google.maps.LatLng(parseFloat(latlng[0]) , parseFloat(latlng[1])); + return locate; +} \ No newline at end of file diff --git a/src/dataloaderinterface/static/dataloaderinterface/js/papaparse.min.js b/src/dataloaderinterface/static/dataloaderinterface/js/papaparse.min.js new file mode 100644 index 00000000..e88f353d --- /dev/null +++ b/src/dataloaderinterface/static/dataloaderinterface/js/papaparse.min.js @@ -0,0 +1,7 @@ +/*! + Papa Parse + v4.3.5 + https://github.com/mholt/PapaParse + License: MIT +*/ +!function(a,b){"function"==typeof define&&define.amd?define([],b):"object"==typeof module&&"undefined"!=typeof exports?module.exports=b():a.Papa=b()}(this,function(){"use strict";function a(a,b){b=b||{};var c=b.dynamicTyping||!1;if(r(c)&&(b.dynamicTypingFunction=c,c={}),b.dynamicTyping=c,b.worker&&z.WORKERS_SUPPORTED){var h=k();return h.userStep=b.step,h.userChunk=b.chunk,h.userComplete=b.complete,h.userError=b.error,b.step=r(b.step),b.chunk=r(b.chunk),b.complete=r(b.complete),b.error=r(b.error),delete b.worker,void h.postMessage({input:a,config:b,workerId:h.id})}var i=null;return"string"==typeof a?i=b.download?new d(b):new f(b):a.readable===!0&&r(a.read)&&r(a.on)?i=new g(b):(t.File&&a instanceof File||a instanceof Object)&&(i=new e(b)),i.stream(a)}function b(a,b){function c(){"object"==typeof b&&("string"==typeof b.delimiter&&1===b.delimiter.length&&z.BAD_DELIMITERS.indexOf(b.delimiter)===-1&&(j=b.delimiter),("boolean"==typeof b.quotes||b.quotes instanceof Array)&&(h=b.quotes),"string"==typeof b.newline&&(k=b.newline),"string"==typeof b.quoteChar&&(l=b.quoteChar),"boolean"==typeof b.header&&(i=b.header))}function d(a){if("object"!=typeof a)return[];var b=[];for(var c in a)b.push(c);return b}function e(a,b){var c="";"string"==typeof a&&(a=JSON.parse(a)),"string"==typeof b&&(b=JSON.parse(b));var d=a instanceof Array&&a.length>0,e=!(b[0]instanceof Array);if(d&&i){for(var g=0;g0&&(c+=j),c+=f(a[g],g);b.length>0&&(c+=k)}for(var h=0;h0&&(c+=j);var n=d&&e?a[m]:m;c+=f(b[h][n],m)}h-1||" "===a.charAt(0)||" "===a.charAt(a.length-1);return c?l+a+l:a}function g(a,b){for(var c=0;c-1)return!0;return!1}var h=!1,i=!0,j=",",k="\r\n",l='"';c();var m=new RegExp(l,"g");if("string"==typeof a&&(a=JSON.parse(a)),a instanceof Array){if(!a.length||a[0]instanceof Array)return e(null,a);if("object"==typeof a[0])return e(d(a[0]),a)}else if("object"==typeof a)return"string"==typeof a.data&&(a.data=JSON.parse(a.data)),a.data instanceof Array&&(a.fields||(a.fields=a.meta&&a.meta.fields),a.fields||(a.fields=a.data[0]instanceof Array?a.fields:d(a.data[0])),a.data[0]instanceof Array||"object"==typeof a.data[0]||(a.data=[a.data])),e(a.fields||[],a.data||[]);throw"exception: Unable to serialize unrecognized input"}function c(a){function b(a){var b=p(a);b.chunkSize=parseInt(b.chunkSize),a.step||a.chunk||(b.chunkSize=null),this._handle=new h(b),this._handle.streamer=this,this._config=b}this._handle=null,this._paused=!1,this._finished=!1,this._input=null,this._baseIndex=0,this._partialLine="",this._rowCount=0,this._start=0,this._nextChunk=null,this.isFirstChunk=!0,this._completeResults={data:[],errors:[],meta:{}},b.call(this,a),this.parseChunk=function(a){if(this.isFirstChunk&&r(this._config.beforeFirstChunk)){var b=this._config.beforeFirstChunk(a);void 0!==b&&(a=b)}this.isFirstChunk=!1;var c=this._partialLine+a;this._partialLine="";var d=this._handle.parse(c,this._baseIndex,!this._finished);if(!this._handle.paused()&&!this._handle.aborted()){var e=d.meta.cursor;this._finished||(this._partialLine=c.substring(e-this._baseIndex),this._baseIndex=e),d&&d.data&&(this._rowCount+=d.data.length);var f=this._finished||this._config.preview&&this._rowCount>=this._config.preview;if(v)t.postMessage({results:d,workerId:z.WORKER_ID,finished:f});else if(r(this._config.chunk)){if(this._config.chunk(d,this._handle),this._paused)return;d=void 0,this._completeResults=void 0}return this._config.step||this._config.chunk||(this._completeResults.data=this._completeResults.data.concat(d.data),this._completeResults.errors=this._completeResults.errors.concat(d.errors),this._completeResults.meta=d.meta),!f||!r(this._config.complete)||d&&d.meta.aborted||this._config.complete(this._completeResults,this._input),f||d&&d.meta.paused||this._nextChunk(),d}},this._sendError=function(a){r(this._config.error)?this._config.error(a):v&&this._config.error&&t.postMessage({workerId:z.WORKER_ID,error:a,finished:!1})}}function d(a){function b(a){var b=a.getResponseHeader("Content-Range");return null===b?-1:parseInt(b.substr(b.lastIndexOf("/")+1))}a=a||{},a.chunkSize||(a.chunkSize=z.RemoteChunkSize),c.call(this,a);var d;u?this._nextChunk=function(){this._readChunk(),this._chunkLoaded()}:this._nextChunk=function(){this._readChunk()},this.stream=function(a){this._input=a,this._nextChunk()},this._readChunk=function(){if(this._finished)return void this._chunkLoaded();if(d=new XMLHttpRequest,this._config.withCredentials&&(d.withCredentials=this._config.withCredentials),u||(d.onload=q(this._chunkLoaded,this),d.onerror=q(this._chunkError,this)),d.open("GET",this._input,!u),this._config.downloadRequestHeaders){var a=this._config.downloadRequestHeaders;for(var b in a)d.setRequestHeader(b,a[b])}if(this._config.chunkSize){var c=this._start+this._config.chunkSize-1;d.setRequestHeader("Range","bytes="+this._start+"-"+c),d.setRequestHeader("If-None-Match","webkit-no-cache")}try{d.send()}catch(a){this._chunkError(a.message)}u&&0===d.status?this._chunkError():this._start+=this._config.chunkSize},this._chunkLoaded=function(){if(4==d.readyState){if(d.status<200||d.status>=400)return void this._chunkError();this._finished=!this._config.chunkSize||this._start>b(d),this.parseChunk(d.responseText)}},this._chunkError=function(a){var b=d.statusText||a;this._sendError(b)}}function e(a){a=a||{},a.chunkSize||(a.chunkSize=z.LocalChunkSize),c.call(this,a);var b,d,e="undefined"!=typeof FileReader;this.stream=function(a){this._input=a,d=a.slice||a.webkitSlice||a.mozSlice,e?(b=new FileReader,b.onload=q(this._chunkLoaded,this),b.onerror=q(this._chunkError,this)):b=new FileReaderSync,this._nextChunk()},this._nextChunk=function(){this._finished||this._config.preview&&!(this._rowCount=this._input.size,this.parseChunk(a.target.result)},this._chunkError=function(){this._sendError(b.error)}}function f(a){a=a||{},c.call(this,a);var b,d;this.stream=function(a){return b=a,d=a,this._nextChunk()},this._nextChunk=function(){if(!this._finished){var a=this._config.chunkSize,b=a?d.substr(0,a):d;return d=a?d.substr(a):"",this._finished=!d,this.parseChunk(b)}}}function g(a){a=a||{},c.call(this,a);var b=[],d=!0;this.stream=function(a){this._input=a,this._input.on("data",this._streamData),this._input.on("end",this._streamEnd),this._input.on("error",this._streamError)},this._nextChunk=function(){b.length?this.parseChunk(b.shift()):d=!0},this._streamData=q(function(a){try{b.push("string"==typeof a?a:a.toString(this._config.encoding)),d&&(d=!1,this.parseChunk(b.shift()))}catch(a){this._streamError(a)}},this),this._streamError=q(function(a){this._streamCleanUp(),this._sendError(a.message)},this),this._streamEnd=q(function(){this._streamCleanUp(),this._finished=!0,this._streamData("")},this),this._streamCleanUp=q(function(){this._input.removeListener("data",this._streamData),this._input.removeListener("end",this._streamEnd),this._input.removeListener("error",this._streamError)},this)}function h(a){function b(){if(x&&o&&(l("Delimiter","UndetectableDelimiter","Unable to auto-detect delimiting character; defaulted to '"+z.DefaultDelimiter+"'"),o=!1),a.skipEmptyLines)for(var b=0;b=w.length?"__parsed_extra":w[d]),g=f(e,g),"__parsed_extra"===e?(c[e]=c[e]||[],c[e].push(g)):c[e]=g}x.data[b]=c,a.header&&(d>w.length?l("FieldMismatch","TooManyFields","Too many fields: expected "+w.length+" fields but parsed "+d,b):d1&&(l+=Math.abs(q-g),g=q):g=q}o.data.length>0&&(m/=o.data.length-n),("undefined"==typeof f||l1.99&&(f=l,e=k)}return a.delimiter=e,{successful:!!e,bestDelimiter:e}}function j(a){a=a.substr(0,1048576);var b=a.split("\r"),c=a.split("\n"),d=c.length>1&&c[0].length=b.length/2?"\r\n":"\r"}function k(a){var b=q.test(a);return b?parseFloat(a):a}function l(a,b,c,d){x.errors.push({type:a,code:b,message:c,row:d})}var m,n,o,q=/^\s*-?(\d*\.?\d+|\d+\.?\d*)(e[-+]?\d+)?\s*$/i,s=this,t=0,u=!1,v=!1,w=[],x={data:[],errors:[],meta:{}};if(r(a.step)){var y=a.step;a.step=function(d){if(x=d,c())b();else{if(b(),0===x.data.length)return;t+=d.data.length,a.preview&&t>a.preview?n.abort():y(x,s)}}}this.parse=function(c,d,e){if(a.newline||(a.newline=j(c)),o=!1,a.delimiter)r(a.delimiter)&&(a.delimiter=a.delimiter(c),x.meta.delimiter=a.delimiter);else{var f=h(c,a.newline,a.skipEmptyLines);f.successful?a.delimiter=f.bestDelimiter:(o=!0,a.delimiter=z.DefaultDelimiter),x.meta.delimiter=a.delimiter}var g=p(a);return a.preview&&a.header&&g.preview++,m=c,n=new i(g),x=n.parse(m,d,e),b(),u?{meta:{paused:!0}}:x||{meta:{paused:!1}}},this.paused=function(){return u},this.pause=function(){u=!0,n.abort(),m=m.substr(n.getCharIndex())},this.resume=function(){u=!1,s.streamer.parseChunk(m)},this.aborted=function(){return v},this.abort=function(){v=!0,n.abort(),x.meta.aborted=!0,r(a.complete)&&a.complete(x),m=""}}function i(a){a=a||{};var b=a.delimiter,c=a.newline,d=a.comments,e=a.step,f=a.preview,g=a.fastMode,h=a.quoteChar||'"';if(("string"!=typeof b||z.BAD_DELIMITERS.indexOf(b)>-1)&&(b=","),d===b)throw"Comment character same as delimiter";d===!0?d="#":("string"!=typeof d||z.BAD_DELIMITERS.indexOf(d)>-1)&&(d=!1),"\n"!=c&&"\r"!=c&&"\r\n"!=c&&(c="\n");var i=0,j=!1;this.parse=function(a,k,l){function m(a){x.push(a),A=i}function n(b){return l?p():("undefined"==typeof b&&(b=a.substr(i)),z.push(b),i=s,m(z),w&&q(),p())}function o(b){i=b,m(z),z=[],E=a.indexOf(c,i)}function p(a){return{data:x,errors:y,meta:{delimiter:b,linebreak:c,aborted:j,truncated:!!a,cursor:A+(k||0)}}}function q(){e(p()),x=[],y=[]}if("string"!=typeof a)throw"Input must be a string";var s=a.length,t=b.length,u=c.length,v=d.length,w=r(e);i=0;var x=[],y=[],z=[],A=0;if(!a)return p();if(g||g!==!1&&a.indexOf(h)===-1){for(var B=a.split(c),C=0;C=f)return x=x.slice(0,f),p(!0)}}return p()}for(var D=a.indexOf(b,i),E=a.indexOf(c,i),F=new RegExp(h+h,"g");;)if(a[i]!==h)if(d&&0===z.length&&a.substr(i,v)===d){if(E===-1)return p();i=E+u,E=a.indexOf(c,i),D=a.indexOf(b,i)}else if(D!==-1&&(D=f)return p(!0)}else{var G=i;for(i++;;){var G=a.indexOf(h,G+1);if(G===-1)return l||y.push({type:"Quotes",code:"MissingQuotes",message:"Quoted field unterminated",row:x.length,index:i}),n();if(G===s-1){var H=a.substring(i,G).replace(F,h);return n(H)}if(a[G+1]!==h){if(a[G+1]===b){z.push(a.substring(i,G).replace(F,h)),i=G+1+t,D=a.indexOf(b,i),E=a.indexOf(c,i);break}if(a.substr(G+1,u)===c){if(z.push(a.substring(i,G).replace(F,h)),o(G+1+u),D=a.indexOf(b,i),w&&(q(),j))return p();if(f&&x.length>=f)return p(!0);break}}else G++}}return n()},this.abort=function(){j=!0},this.getCharIndex=function(){return i}}function j(){var a=document.getElementsByTagName("script");return a.length?a[a.length-1].src:""}function k(){if(!z.WORKERS_SUPPORTED)return!1;if(!w&&null===z.SCRIPT_PATH)throw new Error("Script path cannot be determined automatically when Papa Parse is loaded asynchronously. You need to set Papa.SCRIPT_PATH manually.");var a=z.SCRIPT_PATH||s;a+=(a.indexOf("?")!==-1?"&":"?")+"papaworker";var b=new t.Worker(a);return b.onmessage=l,b.id=y++,x[b.id]=b,b}function l(a){var b=a.data,c=x[b.workerId],d=!1;if(b.error)c.userError(b.error,b.file);else if(b.results&&b.results.data){var e=function(){d=!0,m(b.workerId,{data:[],errors:[],meta:{aborted:!0}})},f={abort:e,pause:n,resume:n};if(r(c.userStep)){for(var g=0;gAdd New Organization').insertAfter(organizationSelect.children().first()); + $('').insertAfter(organizationSelect.children().first()); organizationSelect.on('change', function() { if ($(this).val() == 'new') { cleanOrganizationForm(); @@ -46,7 +47,7 @@ $(document).ready(function() { if (xhr.status == 201) { // organization created var newOption = $(''); - $('.organization-fields select[name="organization"]').append(newOption).val(data.organization_id); + $('.user-fields select[name="organization"]').append(newOption).val(data.organization_id); $('#organization-dialog').modal('toggle'); } else if (xhr.status == 206) { // organization form error diff --git a/src/dataloaderinterface/templates/dataloaderinterface/base.html b/src/dataloaderinterface/templates/dataloaderinterface/base.html index adb0d8ca..cd51902f 100644 --- a/src/dataloaderinterface/templates/dataloaderinterface/base.html +++ b/src/dataloaderinterface/templates/dataloaderinterface/base.html @@ -21,7 +21,7 @@ - + @@ -29,6 +29,10 @@ {% endblock %} + + {% block forced_scripts %} + + {% endblock %} @@ -54,14 +58,15 @@