diff --git a/.circleci/config.yml b/.circleci/config.yml
index daf1e81ed..bb3fc634b 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -29,7 +29,7 @@ workflows:
only:
- develop
- automated_tests
- - release-1.6
+ - /release-.*/
version: 2
jobs:
diff --git a/compose/local/tests/Dockerfile b/compose/local/tests/Dockerfile
index b1d2518d0..1cac611a9 100644
--- a/compose/local/tests/Dockerfile
+++ b/compose/local/tests/Dockerfile
@@ -17,11 +17,13 @@ RUN apt-get install -y nodejs
RUN npm install jsdom
RUN npm install jquery
RUN npm install jquery-csv
+RUN npm install unzipper
+RUN npm install node-fetch
RUN npm install selenium-webdriver
RUN wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb && apt install -y ./google-chrome-stable_current_amd64.deb
-RUN wget https://chromedriver.storage.googleapis.com/85.0.4183.83/chromedriver_linux64.zip && unzip -o chromedriver_linux64.zip -d/usr/local/bin
+RUN wget https://chromedriver.storage.googleapis.com/86.0.4240.22/chromedriver_linux64.zip && unzip -o chromedriver_linux64.zip -d/usr/local/bin
#RUN apt install -y firefox && apt install -y firefox-geckodriver
diff --git a/config/settings/base.py b/config/settings/base.py
index be155c176..97c2b99d0 100644
--- a/config/settings/base.py
+++ b/config/settings/base.py
@@ -394,3 +394,6 @@
# https://docs.celeryproject.org/en/latest/userguide/configuration.html#std-setting-task_track_started
# Report task state updates
CELERY_TASK_TRACK_STARTED = True
+# https://docs.djangoproject.com/en/3.1/ref/settings/#file-upload-max-memory-size
+# 100mb (in bytes)
+FILE_UPLOAD_MAX_MEMORY_SIZE = 100000000
diff --git a/rebuild_django_and_deploy.sh b/rebuild_django_and_deploy.sh
index 14f318e77..f523218f8 100644
--- a/rebuild_django_and_deploy.sh
+++ b/rebuild_django_and_deploy.sh
@@ -2,7 +2,7 @@
# Copyright (C) 2019-2020 Woods Hole Oceanographic Institution
#
-# This file is part of the Roundabout Database project ("RDB" or
+# This file is part of the Roundabout Database project ("RDB" or
# "ooicgsn-roundabout").
#
# ooicgsn-roundabout is free software: you can redistribute it and/or modify
@@ -28,6 +28,9 @@ then
docker-compose -f production.yml up -d --no-deps --build django
docker-compose -f production.yml run --rm django python manage.py migrate
docker-compose -f production.yml run --rm django python manage.py collectstatic --noinput
+ docker-compose -f production.yml up -d --no-deps --build celeryworker
+ docker-compose -f production.yml up -d --no-deps --build celerybeat
+ docker-compose -f production.yml up -d --no-deps --build flower
fi
if [ -e production-demo-site.yml ]
diff --git a/rebuild_testing_and_deploy.sh b/rebuild_testing_and_deploy.sh
index 0b7c21d42..f007cfba0 100644
--- a/rebuild_testing_and_deploy.sh
+++ b/rebuild_testing_and_deploy.sh
@@ -28,4 +28,7 @@ then
docker-compose -f production-testing-site.yml up -d --no-deps --build django_testing
docker-compose -f production-testing-site.yml run --rm django_testing python manage.py migrate
docker-compose -f production-testing-site.yml run --rm django_testing python manage.py collectstatic --noinput
+ docker-compose -f production-testing-site.yml up -d --no-deps --build celeryworker
+ docker-compose -f production-testing-site.yml up -d --no-deps --build celerybeat
+ docker-compose -f production-testing-site.yml up -d --no-deps --build flower
fi
diff --git a/requirements/base.txt b/requirements/base.txt
index 3d89124ae..381761230 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -11,7 +11,7 @@ flower==0.9.5 # https://github.com/mher/flower
# Django
# ------------------------------------------------------------------------------
-django==3.0.10 # pyup: < 3.1 # https://www.djangoproject.com/
+django==3.1.3 # # https://www.djangoproject.com/
django-environ==0.4.5 # https://github.com/joke2k/django-environ
django-model-utils==4.0.0 # https://github.com/jazzband/django-model-utils
django-allauth==0.42.0 # https://github.com/pennersr/django-allauth
@@ -22,16 +22,16 @@ django-redis==4.12.1 # https://github.com/niwinz/django-redis
djangorestframework==3.12.1 # https://github.com/encode/django-rest-framework
django-cors-headers==3.5.0
coreapi==2.3.3 # https://github.com/core-api/python-client
-django-filter==2.2.0
+django-filter==2.4.0
djangorestframework-link-header-pagination==0.1.1
-drf-flex-fields==0.8.6
+drf-flex-fields==0.8.8
# Your custom requirements go here
django-mptt==0.11.0
django-summernote==0.8.11.6
django-bootstrap-datepicker-plus==3.0.5
sigfig==1.1.8 #https://pypi.org/project/sigfig/
-django-tables2==2.2.1 # https://github.com/jieter/django-tables2
+django-tables2==2.3.3 # https://github.com/jieter/django-tables2
tablib==1.1 # https://github.com/jazzband/tablib for exporting/downloading tables2/tabular data
django-tables2-column-shifter==0.5.2 # for showing/hiding columns for tables2
django-import-export==2.0.2
diff --git a/roundabout/admintools/views.py b/roundabout/admintools/views.py
index 83c488471..c83a70fee 100644
--- a/roundabout/admintools/views.py
+++ b/roundabout/admintools/views.py
@@ -329,7 +329,7 @@ def get_redirect_url(self, *args, **kwargs):
tempimport_obj = None
if tempimport_obj:
- # get all the Inventory items to upload from the Temp tables
+ # get all the Inventory items to upload from the Temp tables2
for item_obj in tempimport_obj.tempimportitems.all():
inventory_obj = Inventory()
diff --git a/roundabout/assemblies/forms.py b/roundabout/assemblies/forms.py
index 44eccb373..71978a749 100644
--- a/roundabout/assemblies/forms.py
+++ b/roundabout/assemblies/forms.py
@@ -1,7 +1,7 @@
"""
# Copyright (C) 2019-2020 Woods Hole Oceanographic Institution
#
-# This file is part of the Roundabout Database project ("RDB" or
+# This file is part of the Roundabout Database project ("RDB" or
# "ooicgsn-roundabout").
#
# ooicgsn-roundabout is free software: you can redistribute it and/or modify
@@ -21,6 +21,7 @@
from pprint import pprint
from django import forms
+from django.shortcuts import get_object_or_404
from django.forms.models import inlineformset_factory
from django.core.exceptions import ValidationError
@@ -164,3 +165,25 @@ class Meta:
labels = {
'name': '%s Type Name' % (labels['label_assemblies_app_singular']),
}
+
+
+"""
+Custom Deletion form for AssemblyType
+User needs to be able to choose a new AssemblyType for existing Assemblies or they
+disappear from tree nav
+"""
+class AssemblyTypeDeleteForm(forms.Form):
+ new_assembly_type = forms.ModelChoiceField(label='Select new Assembly Type', queryset=AssemblyType.objects.all())
+
+ def __init__(self, *args, **kwargs):
+ assembly_type_pk= kwargs.pop('pk')
+ assembly_type_to_delete = get_object_or_404(AssemblyType, id=assembly_type_pk)
+
+ super(AssemblyTypeDeleteForm, self).__init__(*args, **kwargs)
+ # Check if this PartType has IPart Templates, remove new field if false
+ if not assembly_type_to_delete.assemblies.exists():
+ self.fields['new_assembly_type'].required = False
+ self.fields['new_assembly_type'].widget = forms.HiddenInput()
+
+ # remove this object from the possible replacements
+ self.fields['new_assembly_type'].queryset = AssemblyType.objects.exclude(id=assembly_type_to_delete.id)
diff --git a/roundabout/assemblies/migrations/0011_auto_20201117_2030.py b/roundabout/assemblies/migrations/0011_auto_20201117_2030.py
new file mode 100644
index 000000000..a3c3e8500
--- /dev/null
+++ b/roundabout/assemblies/migrations/0011_auto_20201117_2030.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.1.3 on 2020-11-17 20:30
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('assemblies', '0010_auto_20201014_1253'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='assembly',
+ name='name',
+ field=models.CharField(db_index=True, max_length=255, unique=True),
+ ),
+ ]
diff --git a/roundabout/assemblies/models.py b/roundabout/assemblies/models.py
index 60b154b3d..d681b0549 100644
--- a/roundabout/assemblies/models.py
+++ b/roundabout/assemblies/models.py
@@ -40,7 +40,7 @@ def __str__(self):
# Assembly base model
class Assembly(models.Model):
- name = models.CharField(max_length=255, unique=False, db_index=True)
+ name = models.CharField(max_length=255, unique=True, db_index=True)
assembly_type = models.ForeignKey(AssemblyType, related_name='assemblies',
on_delete=models.SET_NULL, null=True, blank=True)
assembly_number = models.CharField(max_length=100, unique=False, db_index=True, null=False, blank=True)
diff --git a/roundabout/assemblies/views.py b/roundabout/assemblies/views.py
index bd4b9fb44..a35fcb3a8 100644
--- a/roundabout/assemblies/views.py
+++ b/roundabout/assemblies/views.py
@@ -1,7 +1,7 @@
"""
# Copyright (C) 2019-2020 Woods Hole Oceanographic Institution
#
-# This file is part of the Roundabout Database project ("RDB" or
+# This file is part of the Roundabout Database project ("RDB" or
# "ooicgsn-roundabout").
#
# ooicgsn-roundabout is free software: you can redistribute it and/or modify
@@ -27,7 +27,7 @@
from django.db import transaction
from .models import Assembly, AssemblyPart, AssemblyType, AssemblyDocument, AssemblyRevision
-from .forms import AssemblyForm, AssemblyPartForm, AssemblyTypeForm, AssemblyRevisionForm, AssemblyRevisionFormset, AssemblyDocumentationFormset
+from .forms import AssemblyForm, AssemblyPartForm, AssemblyTypeForm, AssemblyRevisionForm, AssemblyRevisionFormset, AssemblyDocumentationFormset, AssemblyTypeDeleteForm
from roundabout.parts.models import PartType, Part
from roundabout.inventory.models import Action
from common.util.mixins import AjaxFormMixin
@@ -746,13 +746,44 @@ def get_success_url(self):
return reverse('assemblies:assembly_type_home', )
-class AssemblyTypeDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
- model = AssemblyType
+class AssemblyTypeDeleteView(LoginRequiredMixin, PermissionRequiredMixin, FormView):
+ form_class = AssemblyTypeDeleteForm
template_name = 'assemblies/assembly_type_confirm_delete.html'
- success_url = reverse_lazy('assemblies:assembly_type_home')
permission_required = 'assemblies.delete_assembly'
redirect_field_name = 'home'
+ def get_context_data(self, **kwargs):
+ context = super(AssemblyTypeDeleteView, self).get_context_data(**kwargs)
+ assembly_type = AssemblyType.objects.get(id=self.kwargs['pk'])
+
+ context.update({
+ 'assembly_type': assembly_type
+ })
+ return context
+
+ def get_form_kwargs(self):
+ kwargs = super(AssemblyTypeDeleteView, self).get_form_kwargs()
+ if 'pk' in self.kwargs:
+ kwargs['pk'] = self.kwargs['pk']
+ return kwargs
+
+ def form_valid(self, form):
+ new_assembly_type = form.cleaned_data['new_assembly_type']
+ assembly_type_to_delete = AssemblyType.objects.get(id=self.kwargs['pk'])
+
+ # Need to check if there's Part Templates. If so, need move them to new Part Type.
+ if assembly_type_to_delete.assemblies.exists():
+ for assembly in assembly_type_to_delete.assemblies.all():
+ assembly.assembly_type = new_assembly_type
+ assembly.save()
+
+ # Delete the Assembly object
+ assembly_type_to_delete.delete()
+ return HttpResponseRedirect(self.get_success_url())
+
+ def get_success_url(self):
+ return reverse('assemblies:assembly_type_home')
+
# Direct Detail view for Assembly Types
class AssemblyTypeDetailView(LoginRequiredMixin, DetailView):
diff --git a/roundabout/builds/api/filters.py b/roundabout/builds/api/filters.py
index e492c9e5b..fb3f02789 100644
--- a/roundabout/builds/api/filters.py
+++ b/roundabout/builds/api/filters.py
@@ -96,3 +96,15 @@ def filter_time_in_field(self, queryset, name, value):
# alternatively, you could opt to hardcode the lookup. e.g.,
# return queryset.filter(published_on__isnull=False)
+
+
+class DeploymentOmsCustomFilter(filters.FilterSet):
+ deployment_number = filters.CharFilter(lookup_expr='icontains')
+ build_number = filters.CharFilter(field_name='build__build_number', lookup_expr='icontains')
+ class Meta:
+ model = Deployment
+ fields = [
+ 'deployment_number',
+ 'build_number',
+ 'build',
+ ]
diff --git a/roundabout/builds/api/serializers.py b/roundabout/builds/api/serializers.py
index 97f44c4b0..d70e3e14c 100644
--- a/roundabout/builds/api/serializers.py
+++ b/roundabout/builds/api/serializers.py
@@ -28,8 +28,14 @@
from roundabout.cruises.models import Cruise
from roundabout.inventory.models import Deployment
from roundabout.locations.models import Location
+from roundabout.configs_constants.models import ConfigEvent
+from roundabout.calibrations.models import CalibrationEvent
from ..models import Build
+# Import environment variables from .env files
+import environ
+env = environ.Env()
+base_url = env('RDB_SITE_URL')
API_VERSION = 'api_v1'
class BuildSerializer(serializers.HyperlinkedModelSerializer, FlexFieldsModelSerializer):
@@ -179,3 +185,147 @@ class Meta:
def get_time_in_field(self, obj):
return time_at_sea_display(obj.deployment_time_in_field)
+
+
+class DeploymentOmsCustomSerializer(FlexFieldsModelSerializer):
+ deployment_url = serializers.HyperlinkedIdentityField(
+ view_name = API_VERSION + ':deployments-detail',
+ lookup_field='pk',
+ )
+ build_number = serializers.SerializerMethodField('get_build_number')
+ build_url = serializers.HyperlinkedRelatedField(
+ source='build',
+ view_name = API_VERSION + ':builds-detail',
+ lookup_field = 'pk',
+ queryset = Build.objects
+ )
+ location_name = serializers.SerializerMethodField('get_location_name')
+ location_url = serializers.HyperlinkedRelatedField(
+ source='deployed_location',
+ view_name = API_VERSION + ':locations-detail',
+ lookup_field = 'pk',
+ queryset = Location.objects
+ )
+ assembly_parts = serializers.SerializerMethodField('get_assembly_parts')
+
+ class Meta:
+ model = Deployment
+ fields = [
+ 'build_url',
+ 'build_number',
+ 'deployment_url',
+ 'deployment_number',
+ 'location_name',
+ 'location_url',
+ 'current_status',
+ 'latitude',
+ 'longitude',
+ 'assembly_parts',
+ ]
+
+ def get_build_id(self, obj):
+ if obj.build:
+ return obj.build.id
+ return None
+
+ def get_build_number(self, obj):
+ if obj.build:
+ return obj.build.build_number
+ return None
+
+ def get_location_id(self, obj):
+ if obj.deployed_location:
+ return obj.deployed_location.id
+ return None
+
+ def get_location_name(self, obj):
+ if obj.deployed_location:
+ return obj.deployed_location.name
+ return None
+
+ def get_assembly_parts(self, obj):
+ # Use the InventoryDeployment related model to get historical list of Inventory items
+ # on each Deployment
+ inventory_dep_qs = obj.inventory_deployments.exclude(current_status=Deployment.DEPLOYMENTRETIRE).select_related('inventory')
+ assembly_parts = []
+
+ for inv in inventory_dep_qs:
+ # get all config_events for this Inventory/Deployment
+ configuration_values = []
+ config_events = inv.inventory.config_events.filter(deployment=inv.deployment).prefetch_related('config_values')
+ if config_events:
+ for event in config_events:
+ for value in event.config_values.all():
+ configuration_values.append({
+ 'name': value.config_name.name,
+ 'value': value.config_value,
+ })
+
+ # get all calibration_events for this Inventory/Deployment
+ # need to get the CalibrationEvent that matches the Deployment date
+ # calibration_date field sets the range for valid Calibration Events
+ calibration_values = []
+ if inv.inventory.calibration_events.exists():
+ for event in inv.inventory.calibration_events.all():
+ # find the CalibrationEvent valid date range that matches Deployment date
+ first_date, last_date = event.get_valid_calibration_range()
+ if inv.deployment_to_field_date and first_date < inv.deployment_to_field_date < last_date:
+ for value in event.coefficient_value_sets.all():
+ calibration_values.append({
+ 'name': value.coefficient_name.calibration_name,
+ 'value': value.value_set,
+ })
+ break
+
+ # get all constant_default_events for this Inventory/Deployment
+ constant_default_values = []
+ if inv.inventory.constant_default_events.exists():
+ for event in inv.inventory.constant_default_events.all():
+ for value in event.constant_defaults.all():
+ constant_default_values.append({
+ 'name': value.config_name.name,
+ 'value': value.default_value,
+ })
+
+ # get all custom_fields for this Inventory/Deployment
+ custom_fields = []
+ if inv.inventory.fieldvalues.exists():
+ inv_custom_fields = inv.inventory.fieldvalues.filter(is_current=True).select_related('field')
+ # create initial empty dict
+ for field in inv_custom_fields:
+ custom_fields.append({
+ 'name': field.field.field_name,
+ 'value': field.field_value,
+ })
+
+ # set up URL link fields
+ request = self.context.get("request")
+ inventory_url = reverse('api_v1:inventory-detail', kwargs={'pk': inv.inventory_id}, request=request)
+ assembly_part_url = reverse('api_v1:assembly-templates/assembly-parts-detail', kwargs={'pk': inv.assembly_part_id}, request=request)
+
+ if inv.assembly_part and inv.assembly_part.parent:
+ parent_assembly_part_url = reverse('api_v1:assembly-templates/assembly-parts-detail', kwargs={'pk': inv.assembly_part.parent_id}, request=request)
+ parent_assembly_part_id = inv.assembly_part.parent.id
+ else:
+ parent_assembly_part_url = None
+ parent_assembly_part_id = None
+ # create object to populate the "assembly_part" list
+ item_obj = {
+ 'assembly_part_url': assembly_part_url,
+ 'assembly_part_id': inv.assembly_part.id,
+ 'part_name': inv.inventory.part.name,
+ 'part_type': inv.inventory.part.part_type.name if inv.inventory.part.part_type else None,
+ 'parent_assembly_part_url': parent_assembly_part_url,
+ 'parent_assembly_part_id': parent_assembly_part_id,
+ 'inventory_url': inventory_url,
+ 'inventory_serial_number': inv.inventory.serial_number,
+ 'deployment_to_field_date': inv.deployment_to_field_date,
+ 'deployment_recovery_date': inv.deployment_recovery_date,
+ 'configuration_values': configuration_values,
+ 'calibration_values': calibration_values,
+ 'constant_default_values': constant_default_values,
+ 'custom_fields': custom_fields,
+ }
+ assembly_parts.append(item_obj)
+
+ return assembly_parts
diff --git a/roundabout/builds/api/views.py b/roundabout/builds/api/views.py
index 5b0aa2df8..34785cc8d 100644
--- a/roundabout/builds/api/views.py
+++ b/roundabout/builds/api/views.py
@@ -23,7 +23,7 @@
from roundabout.core.api.views import FlexModelViewSet
from .filters import *
-from .serializers import BuildSerializer, DeploymentSerializer
+from .serializers import BuildSerializer, DeploymentSerializer, DeploymentOmsCustomSerializer
class BuildViewSet(FlexModelViewSet):
@@ -38,3 +38,14 @@ class DeploymentViewSet(FlexModelViewSet):
serializer_class = DeploymentSerializer
permission_classes = (IsAuthenticated,)
filterset_class = DeploymentFilter
+
+
+class DeploymentOmsCustomViewSet(FlexModelViewSet):
+ serializer_class = DeploymentOmsCustomSerializer
+ permission_classes = (IsAuthenticated,)
+ filterset_class = DeploymentOmsCustomFilter
+
+ def get_queryset(self):
+ queryset = Deployment.objects.all().prefetch_related('inventory_deployments')
+ queryset = queryset.prefetch_related('inventory_deployments').select_related('build')
+ return queryset
diff --git a/roundabout/builds/forms.py b/roundabout/builds/forms.py
index 4a64b5924..8a7b80273 100644
--- a/roundabout/builds/forms.py
+++ b/roundabout/builds/forms.py
@@ -238,6 +238,8 @@ class Meta:
'deployment_number': '%s Number' % (labels['label_deployments_app_singular']),
'deployed_location': 'Final %s Location' % (labels['label_deployments_app_singular']),
'cruise_deployed': 'Cruise Deployed On',
+ 'latitude': 'Latitude (+/- degrees N)',
+ 'longitude': 'Longitude (+/- degrees E)',
}
widgets = {
@@ -376,6 +378,8 @@ class Meta:
labels = {
'location': '%s Location' % (labels['label_deployments_app_singular']),
'cruise_deployed': 'Cruise Deployed On',
+ 'latitude': 'Latitude (+/- degrees N)',
+ 'longitude': 'Longitude (+/- degrees E)',
}
# Add custom date field to allow user to pick date for the Action record
diff --git a/roundabout/builds/models.py b/roundabout/builds/models.py
index 5e9e3483d..379fd3d7c 100644
--- a/roundabout/builds/models.py
+++ b/roundabout/builds/models.py
@@ -68,7 +68,7 @@ class Meta:
ordering = ['assembly_revision', 'build_number']
def __str__(self):
- return '%s - %s' % (self.build_number, self.assembly_revision.assembly.name)
+ return '%s - %s' % (self.assembly_revision.assembly.name, self.build_number)
@property
def name(self):
diff --git a/roundabout/calibrations/models.py b/roundabout/calibrations/models.py
index dff82357c..4df758d82 100644
--- a/roundabout/calibrations/models.py
+++ b/roundabout/calibrations/models.py
@@ -27,7 +27,6 @@
from roundabout.parts.models import Part
from roundabout.users.models import User
-
# Tracks Calibration Coefficient event history across Inventory Parts
class CalibrationEvent(models.Model):
class Meta:
@@ -60,6 +59,23 @@ def get_sorted_reviewers(self):
def get_sorted_approvers(self):
return self.user_approver.all().order_by('username')
+ # method to return a date range that corresponds to the period of time when this CalibrationEvent is valid
+ # this range corresponds to this calibration_date -> next latest calibration_date.
+ # calibration_date is floor of range.
+ # Returns a list of datetimes
+ def get_valid_calibration_range(self):
+ next_event = CalibrationEvent.objects.filter(inventory=self.inventory).filter(calibration_date__gt=self.calibration_date).last()
+
+ if next_event:
+ last_date = next_event.calibration_date
+ else:
+ last_date = timezone.now()
+
+ calibration_range = [self.calibration_date, last_date]
+ print(calibration_range)
+ print(self)
+ return calibration_range
+
# Tracks Coefficient Name Event history across Parts
class CoefficientNameEvent(models.Model):
diff --git a/roundabout/calibrations/tasks.py b/roundabout/calibrations/tasks.py
new file mode 100644
index 000000000..af98ad16c
--- /dev/null
+++ b/roundabout/calibrations/tasks.py
@@ -0,0 +1,41 @@
+"""
+# Copyright (C) 2019-2020 Woods Hole Oceanographic Institution
+#
+# This file is part of the Roundabout Database project ("RDB" or
+# "ooicgsn-roundabout").
+#
+# ooicgsn-roundabout is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# ooicgsn-roundabout is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with ooicgsn-roundabout in the COPYING.md file at the project root.
+# If not, see .
+"""
+
+from celery import shared_task
+
+from roundabout.calibrations.models import CalibrationEvent
+from roundabout.configs_constants.models import ConfigEvent, ConfigDefaultEvent, ConstDefaultEvent
+
+
+@shared_task(bind = True)
+def check_events(self):
+ for event in CalibrationEvent.objects.all():
+ if not event.coefficient_value_sets.exists():
+ event.delete()
+ for event in ConfigEvent.objects.all():
+ if not event.config_values.exists():
+ event.delete()
+ for event in ConfigDefaultEvent.objects.all():
+ if not event.config_defaults.exists():
+ event.delete()
+ for event in ConstDefaultEvent.objects.all():
+ if not event.constant_defaults.exists():
+ event.delete()
\ No newline at end of file
diff --git a/roundabout/calibrations/views.py b/roundabout/calibrations/views.py
index f7910a9c2..1a023359b 100644
--- a/roundabout/calibrations/views.py
+++ b/roundabout/calibrations/views.py
@@ -19,7 +19,7 @@
# If not, see .
"""
-from django.shortcuts import render
+from django.shortcuts import render, redirect
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.views.generic import CreateView, UpdateView, DeleteView, DetailView
@@ -37,7 +37,8 @@
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from django.forms.models import inlineformset_factory, BaseInlineFormSet
-from .utils import handle_reviewers, check_events
+from .utils import handle_reviewers
+from .tasks import check_events
# Handles creation of Calibration Events, Names,and Coefficients
class EventValueSetAdd(LoginRequiredMixin, AjaxFormMixin, CreateView):
@@ -231,19 +232,32 @@ class EventValueSetDelete(LoginRequiredMixin, PermissionRequiredMixin, DeleteVie
permission_required = 'calibrations.add_calibrationevent'
redirect_field_name = 'home'
- def delete(self, request, *args, **kwargs):
- self.object = self.get_object()
- data = {
- 'message': "Successfully submitted form data.",
- 'parent_id': self.object.inventory.id,
- 'parent_type': 'part_type',
- 'object_type': self.object.get_object_type(),
- }
- self.object.delete()
+ def delete(self, request, *args, **kwargs):
+ self.object = self.get_object()
+ data = {
+ 'message': "Successfully submitted form data.",
+ 'parent_id': self.object.inventory.id,
+ 'parent_type': 'part_type',
+ 'object_type': self.object.get_object_type(),
+ }
+ self.object.delete()
return JsonResponse(data)
def get_success_url(self):
- return reverse_lazy('inventory:ajax_inventory_detail', args=(self.object.inventory.id, ))
+ return reverse('inventory:ajax_inventory_detail', args=(self.object.inventory.id,))
+
+
+def event_delete_view(request, pk):
+ evt = CalibrationEvent.objects.get(id=pk)
+ inv_id = evt.inventory.id
+ if request.method == "POST":
+ evt.delete()
+ return HttpResponseRedirect(reverse('inventory:ajax_inventory_detail', args=(inv_id,)))
+
+ return render(request, 'calibrations/event_delete.html', {
+ "event_template": evt,
+ 'request': request
+ })
@@ -484,7 +498,7 @@ def form_valid(self, form, part_calname_form, part_cal_copy_form):
part_calname_form.save()
part_cal_copy_form.save()
_create_action_history(self.object, Action.UPDATE, self.request.user)
- check_events()
+ job = check_events.delay()
response = HttpResponseRedirect(self.get_success_url())
if self.request.is_ajax():
data = {
@@ -551,7 +565,7 @@ def delete(self, request, *args, **kwargs):
'object_type': self.object.get_object_type(),
}
self.object.delete()
- check_events()
+ job = check_events.delay()
return JsonResponse(data)
def get_success_url(self):
diff --git a/roundabout/configs_constants/api/serializers.py b/roundabout/configs_constants/api/serializers.py
index 78c3746bb..bcc03f951 100644
--- a/roundabout/configs_constants/api/serializers.py
+++ b/roundabout/configs_constants/api/serializers.py
@@ -253,10 +253,10 @@ class ConstDefaultSerializer(FlexFieldsModelSerializer):
lookup_field = 'pk',
queryset = ConfigName.objects
)
- config_event = serializers.HyperlinkedRelatedField(
- view_name = API_VERSION + ':configs-constants/config-events-detail',
+ const_event = serializers.HyperlinkedRelatedField(
+ view_name = API_VERSION + ':configs-constants/const-default-events-detail',
lookup_field = 'pk',
- queryset = ConfigEvent.objects
+ queryset = ConstDefaultEvent.objects
)
class Meta:
@@ -266,13 +266,13 @@ class Meta:
'url',
'default_value',
'created_at',
- 'config_event',
+ 'const_event',
'config_name',
]
expandable_fields = {
'config_name': 'roundabout.configs_constants.api.serializers.ConfigNameSerializer',
- 'config_event': 'roundabout.configs_constants.api.serializers.ConfigEventSerializer',
+ 'const_event': 'roundabout.configs_constants.api.serializers.ConstDefaultEventSerializer',
}
diff --git a/roundabout/configs_constants/migrations/0019_auto_20201217_1543.py b/roundabout/configs_constants/migrations/0019_auto_20201217_1543.py
new file mode 100644
index 000000000..711268d13
--- /dev/null
+++ b/roundabout/configs_constants/migrations/0019_auto_20201217_1543.py
@@ -0,0 +1,17 @@
+# Generated by Django 3.1.3 on 2020-12-17 15:43
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('configs_constants', '0018_configname_deprecated'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='configname',
+ options={'ordering': ['created_at']},
+ ),
+ ]
diff --git a/roundabout/configs_constants/models.py b/roundabout/configs_constants/models.py
index 41b685952..d89af757d 100644
--- a/roundabout/configs_constants/models.py
+++ b/roundabout/configs_constants/models.py
@@ -103,7 +103,7 @@ def get_sorted_approvers(self):
# Tracks Configurations across Parts
class ConfigName(models.Model):
class Meta:
- ordering = ['name']
+ ordering = ['created_at']
unique_together = ['part','config_type','name']
def __str__(self):
return self.name
diff --git a/roundabout/configs_constants/views.py b/roundabout/configs_constants/views.py
index d0bf71114..ae1095188 100644
--- a/roundabout/configs_constants/views.py
+++ b/roundabout/configs_constants/views.py
@@ -38,7 +38,8 @@
from django.utils.translation import gettext_lazy as _
from django.forms.models import inlineformset_factory, BaseInlineFormSet
from roundabout.inventory.utils import _create_action_history
-from roundabout.calibrations.utils import handle_reviewers, check_events
+from roundabout.calibrations.utils import handle_reviewers
+from roundabout.calibrations.tasks import check_events
# Handles creation of Configuration / Constant Events, along with Name/Value formsets
class ConfigEventValueAdd(LoginRequiredMixin, AjaxFormMixin, CreateView):
@@ -463,7 +464,7 @@ def form_valid(self, form, part_confname_form, part_conf_copy_form):
part_confname_form.save()
part_conf_copy_form.save()
_create_action_history(self.object, Action.UPDATE, self.request.user)
- check_events()
+ job = check_events.delay()
response = HttpResponseRedirect(self.get_success_url())
if self.request.is_ajax():
data = {
@@ -529,7 +530,7 @@ def delete(self, request, *args, **kwargs):
'object_type': self.object.get_object_type(),
}
self.object.delete()
- check_events()
+ job = check_events.delay()
return JsonResponse(data)
def get_success_url(self):
diff --git a/roundabout/contrib/sites/migrations/0004_auto_20201117_1702.py b/roundabout/contrib/sites/migrations/0004_auto_20201117_1702.py
new file mode 100644
index 000000000..0d2c3f467
--- /dev/null
+++ b/roundabout/contrib/sites/migrations/0004_auto_20201117_1702.py
@@ -0,0 +1,17 @@
+# Generated by Django 3.1.3 on 2020-11-17 17:02
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('sites', '0003_set_site_domain_and_name'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='site',
+ options={'ordering': ['domain'], 'verbose_name': 'site', 'verbose_name_plural': 'sites'},
+ ),
+ ]
diff --git a/roundabout/core/api/renderers.py b/roundabout/core/api/renderers.py
index d633c4534..b56131630 100644
--- a/roundabout/core/api/renderers.py
+++ b/roundabout/core/api/renderers.py
@@ -5,5 +5,10 @@ class CustomBrowsableAPIRenderer(BrowsableAPIRenderer):
def get_rendered_html_form(self, data, view, method, request):
return None
+ def get_name(self, view):
+ if 'Deployment Oms Custom' in view.get_view_name():
+ return 'OMS++ Build/Deployment Custom Endpoint'
+ return view.get_view_name()
+
def get_description(self, view, status_code):
return ''
diff --git a/roundabout/core/api/urls.py b/roundabout/core/api/urls.py
index e8b052806..4847ce3c8 100644
--- a/roundabout/core/api/urls.py
+++ b/roundabout/core/api/urls.py
@@ -25,7 +25,7 @@
from roundabout.assemblies.api.views import AssemblyViewSet, AssemblyRevisionViewSet, AssemblyPartViewSet, \
AssemblyTypeViewSet
-from roundabout.builds.api.views import BuildViewSet, DeploymentViewSet
+from roundabout.builds.api.views import BuildViewSet, DeploymentViewSet, DeploymentOmsCustomViewSet
from roundabout.calibrations.api.views import *
from roundabout.configs_constants.api.views import *
from roundabout.cruises.api.views import CruiseViewSet, VesselViewSet
@@ -42,6 +42,7 @@
router.register(r'builds', BuildViewSet, 'builds' )
router.register(r'deployments', DeploymentViewSet, 'deployments' )
+router.register(r'oms-builds', DeploymentOmsCustomViewSet, 'oms-builds' )
router.register(r'cruises', CruiseViewSet, 'cruises' )
router.register(r'vessels', VesselViewSet, 'vessels' )
diff --git a/roundabout/core/updaters.py b/roundabout/core/updaters.py
index 6921fe050..c0905f537 100644
--- a/roundabout/core/updaters.py
+++ b/roundabout/core/updaters.py
@@ -26,6 +26,19 @@
# Functions to update legacy content to match new model updates
#------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
+# v.1.6.1 upgrades
+# v1.5 upgrades
+def run_v1_6_1_content_updates():
+ _update_inventory_deployments()
+
+def _update_inventory_deployments():
+ inventory_deployments = InventoryDeployment.objects.all()
+
+ for inv in inventory_deployments:
+ print(inv.inventory.assembly_part)
+ inv.assembly_part = inv.inventory.assembly_part
+ inv.save()
+
# v1.5 upgrades
def run_v1_5_content_updates():
_update_deployment_actions()
diff --git a/roundabout/cruises/forms.py b/roundabout/cruises/forms.py
index f56d7d748..54fb3758c 100644
--- a/roundabout/cruises/forms.py
+++ b/roundabout/cruises/forms.py
@@ -27,7 +27,6 @@
class VesselForm(forms.ModelForm):
-
class Meta:
model = Vessel
fields = '__all__'
@@ -36,6 +35,24 @@ class Meta:
'max_speed': 'Max speed (m/s)',
'max_draft': 'Max draft (m)',
}
+ number_err_message = 'Enter a value 0.1 - 999.9 in the format NNN.N, with or without leading zeros or a decimal place.'
+ error_messages = {
+ 'length': {
+ 'max_digits': number_err_message,
+ 'min_value': number_err_message,
+ 'decimal_places': number_err_message,
+ },
+ 'max_speed': {
+ 'max_digits': number_err_message,
+ 'min_value': number_err_message,
+ 'decimal_places': number_err_message,
+ },
+ 'max_draft': {
+ 'max_digits': number_err_message,
+ 'min_value': number_err_message,
+ 'decimal_places': number_err_message,
+ },
+ }
class CruiseForm(forms.ModelForm):
diff --git a/roundabout/cruises/migrations/0015_auto_20201117_1702.py b/roundabout/cruises/migrations/0015_auto_20201117_1702.py
new file mode 100644
index 000000000..1403d6be4
--- /dev/null
+++ b/roundabout/cruises/migrations/0015_auto_20201117_1702.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.1.3 on 2020-11-17 17:02
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('cruises', '0014_auto_20201002_1827'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='vessel',
+ name='R2R',
+ field=models.BooleanField(blank=True, choices=[(True, 'Yes'), (False, 'No')], null=True),
+ ),
+ migrations.AlterField(
+ model_name='vessel',
+ name='active',
+ field=models.BooleanField(blank=True, choices=[(True, 'Yes'), (False, 'No')], null=True),
+ ),
+ ]
diff --git a/roundabout/cruises/models.py b/roundabout/cruises/models.py
index fcfd8c2eb..6e060dc6a 100644
--- a/roundabout/cruises/models.py
+++ b/roundabout/cruises/models.py
@@ -36,8 +36,8 @@ class Vessel(models.Model):
validators=[MinValueValidator(Decimal('0.01'))],
)
designation = models.CharField(max_length=10, null=False, blank=True)
- active = models.BooleanField(choices=BOOLEAN_CHOICES, null=True, default=True)
- R2R = models.BooleanField(choices=BOOLEAN_CHOICES, null=True, default=True, blank=True)
+ active = models.BooleanField(choices=BOOLEAN_CHOICES, null=True, blank=True)
+ R2R = models.BooleanField(choices=BOOLEAN_CHOICES, null=True, blank=True)
notes = models.TextField(null=False, blank=True)
class Meta:
diff --git a/roundabout/exports/views.py b/roundabout/exports/views.py
index 66eef876a..ca5f9a82b 100644
--- a/roundabout/exports/views.py
+++ b/roundabout/exports/views.py
@@ -172,7 +172,9 @@ class ExportCalibrationEvents(ZipExport):
@classmethod
def build_zip(cls, zf, objs, subdir=None):
+ objs = objs.select_related('inventory__part__part_type').exclude(inventory__part__part_type__ccc_toggle=False)
objs = objs.prefetch_related('inventory', 'inventory__fieldvalues', 'inventory__fieldvalues__field')
+
for cal in objs:
csv_fname = '{}__{}.csv'.format(cal.inventory.serial_number, cal.calibration_date.strftime('%Y%m%d'))
if subdir: csv_fname = join(subdir, csv_fname)
@@ -190,8 +192,7 @@ def build_zip(cls, zf, objs, subdir=None):
@staticmethod
def get_csvrows_aux(cal):
- serial_label_qs = cal.inventory.fieldvalues.filter(field__field_name__iexact='Manufacturer Serial Number',
- is_current=True)
+ serial_label_qs = cal.inventory.fieldvalues.filter(field__field_name__iexact='Manufacturer Serial Number', is_current=True)
if serial_label_qs.exists():
serial_label = serial_label_qs[0].field_value
else:
@@ -226,7 +227,9 @@ class ExportConfigEvents(ZipExport):
@classmethod
def build_zip(cls, zf, objs, subdir=None):
+ objs = objs.select_related('inventory__part__part_type').exclude(inventory__part__part_type__ccc_toggle=False)
objs = objs.prefetch_related('inventory', 'inventory__fieldvalues', 'inventory__fieldvalues__field')
+
for confconst in objs:
csv_fname = '{}__{}.csv'.format(confconst.inventory.serial_number,
confconst.configuration_date.strftime('%Y%m%d'))
@@ -278,10 +281,9 @@ def get_queryset(cls):
calib_qs = CalibrationEvent.objects.all().annotate(date = F('calibration_date')).order_by('-date')
config_qs = ConfigEvent.objects.all().annotate(date = F('configuration_date')).order_by('-date')
- # keep only configs that are associated with a deployment and approved
- # -- commented out for verbose output in build_zip() --
- #config_qs = config_qs.filter(approved=True)
- #config_qs = config_qs.exclude(deployment__isnull=True)
+ # keep only CCCs that are active in the part-template
+ config_qs = config_qs.select_related('inventory__part__part_type').exclude(inventory__part__part_type__ccc_toggle=False)
+ calib_qs = calib_qs.select_related('inventory__part__part_type').exclude(inventory__part__part_type__ccc_toggle=False)
# keep only configs where at least one ConfigValue has include_with_calibration=True
include_with_calibs = ConfigValue.objects.filter(config_event=OuterRef('pk'), config_name__include_with_calibrations=True)
@@ -303,7 +305,7 @@ def get_queryset(cls):
return qs
@classmethod
- def build_zip(cls, zf, objs, subdir=None, verbose='CalibrationsWithConfigs_exportlog.txt'): # TODO~ PRODUCTION: change VERBOSE to None
+ def build_zip(cls, zf, objs, subdir=None, verbose=None):
# objs here is a dict-of-dicts, not a queryset.
# Each top-level key is an inst_id.
# Per inst_id there is (a) a "calibs" key containing a CalibrationEvent Queryset
@@ -313,7 +315,7 @@ def build_zip(cls, zf, objs, subdir=None, verbose='CalibrationsWithConfigs_expor
# To be valid, the bundled inventory CCC fields must (1) include all the part's inventory CCC fields
# (2) be approved==True
- # setting up printing to file option.
+ # if verbose is a string, a log file with that string name is included in the export
if isinstance(verbose,str):
if subdir: verbose = join(subdir,verbose)
out = io.StringIO()
@@ -563,7 +565,7 @@ def depl_row(depl_obj, attribs):
objs = objs.prefetch_related('build__assembly_revision__assembly')
assy_names = objs.values_list('build__assembly_revision__assembly__name',flat=True)
assy_names = set(assy_names)
- #assy_names.discard(None) # removes None if any
+ assy_names.discard(None) # removes None if any
for assy_name in assy_names:
csv_fname = '{}_Deploy.csv'.format(str(assy_name).replace(' ','_'))
if subdir: csv_fname = join(subdir,csv_fname)
diff --git a/roundabout/inventory/api/serializers.py b/roundabout/inventory/api/serializers.py
index 99dbbd67b..69296b32f 100644
--- a/roundabout/inventory/api/serializers.py
+++ b/roundabout/inventory/api/serializers.py
@@ -256,15 +256,22 @@ class InventorySerializer(FlexFieldsModelSerializer):
read_only = True,
lookup_field = 'pk',
)
+ constant_default_events = serializers.HyperlinkedRelatedField(
+ view_name = API_VERSION + ':configs-constants/const-default-events-detail',
+ many = True,
+ read_only = True,
+ lookup_field = 'pk',
+ )
time_in_field = serializers.SerializerMethodField('get_time_in_field')
class Meta:
model = Inventory
fields = [
- 'id', 'url', 'serial_number', 'old_serial_number', 'part', 'location', 'revision', \
- 'parent', 'children', 'build', 'assembly_part', 'assigned_destination_root', 'created_at', \
+ 'id', 'url', 'serial_number', 'old_serial_number', 'part', 'location', 'revision',
+ 'parent', 'children', 'build', 'assembly_part', 'assigned_destination_root', 'created_at',
'updated_at', 'test_result', 'test_type', 'flag', 'time_in_field',
- 'calibration_events', 'config_events', 'actions', 'fieldvalues', 'inventory_deployments',
+ 'calibration_events', 'config_events', 'constant_default_events',
+ 'actions', 'fieldvalues', 'inventory_deployments',
]
expandable_fields = {
@@ -278,6 +285,7 @@ class Meta:
'children': ('roundabout.inventory.api.serializers.InventorySerializer', {'many': True}),
'calibration_events': ('roundabout.calibrations.api.serializers.CalibrationEventSerializer', {'many': True}),
'config_events': ('roundabout.configs_constants.api.serializers.ConfigEventSerializer', {'many': True}),
+ 'constant_default_events': ('roundabout.configs_constants.api.serializers.ConstDefaultEventSerializer', {'many': True}),
'actions': ('roundabout.inventory.api.serializers.ActionSerializer', {'many': True}),
'fieldvalues': ('roundabout.userdefinedfields.api.serializers.FieldValueSerializer', {'many': True, "omit": ["field.fieldvalues"]}),
'inventory_deployments': ('roundabout.inventory.api.serializers.InventoryDeploymentSerializer', {'many': True}),
diff --git a/roundabout/inventory/migrations/0056_inventorydeployment_assembly_part.py b/roundabout/inventory/migrations/0056_inventorydeployment_assembly_part.py
new file mode 100644
index 000000000..1dde7bd66
--- /dev/null
+++ b/roundabout/inventory/migrations/0056_inventorydeployment_assembly_part.py
@@ -0,0 +1,20 @@
+# Generated by Django 3.1.3 on 2020-11-23 17:24
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('assemblies', '0011_auto_20201117_2030'),
+ ('inventory', '0055_auto_20201014_1253'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='inventorydeployment',
+ name='assembly_part',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inventory_deployments', to='assemblies.assemblypart'),
+ ),
+ ]
diff --git a/roundabout/inventory/models.py b/roundabout/inventory/models.py
index f291eb011..452217d29 100644
--- a/roundabout/inventory/models.py
+++ b/roundabout/inventory/models.py
@@ -302,6 +302,8 @@ class InventoryDeployment(DeploymentBase):
on_delete=models.CASCADE, null=False)
inventory = models.ForeignKey(Inventory, related_name='inventory_deployments',
on_delete=models.CASCADE, null=False)
+ assembly_part = models.ForeignKey('assemblies.AssemblyPart', related_name='inventory_deployments',
+ on_delete=models.SET_NULL, null=True)
objects = InventoryDeploymentQuerySet.as_manager()
@@ -316,7 +318,12 @@ def deployment_percentage_vs_build(self):
deployment_percentage = 0
if self.deployment_to_field_date:
# calculate percentage of total build deployment item was deployed
- deployment_percentage = int(self.deployment_time_in_field / self.deployment.deployment_time_in_field * 100)
+ if not self.deployment.deployment_time_in_field:
+ deployment_percentage = 0
+ elif not self.deployment_time_in_field:
+ deployment_percentage = 0
+ else:
+ deployment_percentage = int(self.deployment_time_in_field / self.deployment.deployment_time_in_field * 100)
if deployment_percentage >= 99:
deployment_percentage = 100
return deployment_percentage
diff --git a/roundabout/inventory/urls.py b/roundabout/inventory/urls.py
index edd41eba0..40329626b 100644
--- a/roundabout/inventory/urls.py
+++ b/roundabout/inventory/urls.py
@@ -19,7 +19,6 @@
# If not, see .
"""
-from django.urls import path
from django.urls import path
from . import views
diff --git a/roundabout/inventory/utils.py b/roundabout/inventory/utils.py
index f228fa102..2de20a8a1 100644
--- a/roundabout/inventory/utils.py
+++ b/roundabout/inventory/utils.py
@@ -214,6 +214,7 @@ def _create_action_history(obj, action_type, user, referring_obj=None, referring
inventory_deployment = InventoryDeployment.objects.create(
deployment=deployment,
inventory=obj,
+ assembly_part=obj.assembly_part,
deployment_start_date = action_date
)
action_record.inventory_deployment = inventory_deployment
diff --git a/roundabout/ooi_ci_tools/forms.py b/roundabout/ooi_ci_tools/forms.py
index dfb26a762..6761e43c3 100644
--- a/roundabout/ooi_ci_tools/forms.py
+++ b/roundabout/ooi_ci_tools/forms.py
@@ -20,20 +20,31 @@
"""
import csv
+import io
+import json
+import re
+import requests
+from dateutil import parser
import datetime
import io
from types import SimpleNamespace
+from decimal import Decimal
+
from django import forms
+from django.db import transaction
from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
-from roundabout.calibrations.forms import validate_coeff_vals
-from roundabout.calibrations.models import CoefficientName
-from roundabout.inventory.models import Inventory
+from roundabout.inventory.models import Inventory, Action, Deployment
+from roundabout.cruises.models import Cruise, Vessel
+from roundabout.inventory.utils import _create_action_history
+from roundabout.calibrations.models import CoefficientName, CoefficientValueSet, CalibrationEvent, CoefficientNameEvent
+from roundabout.calibrations.forms import validate_coeff_vals, parse_valid_coeff_vals
+from roundabout.configs_constants.models import ConfigName
from roundabout.users.models import User
-
+from roundabout.userdefinedfields.models import Field, FieldValue
class ImportDeploymentsForm(forms.Form):
deployments_csv = forms.FileField(
@@ -41,16 +52,223 @@ class ImportDeploymentsForm(forms.Form):
attrs={
'multiple': True
}
- )
+ ),
+ required=False
)
+ def clean_deployments_csv(self):
+ deployments_csv = self.files.getlist('deployments_csv')
+ counter = 0
+ for csv_file in deployments_csv:
+ counter += 1
+ filename = csv_file.name[:-4]
+ cache.set('validation_progress',{
+ 'progress': counter,
+ 'total': len(deployments_csv),
+ 'file': filename
+ })
+ try:
+ csv_file.seek(0)
+ reader = csv.DictReader(io.StringIO(csv_file.read().decode('utf-8')))
+ headers = reader.fieldnames
+ except:
+ raise ValidationError(
+ _('File: %(filename)s: Unable to decode file headers'),
+ params={'filename': filename},
+ )
+ deployments = []
+ for row in reader:
+ try:
+ mooring_id = row['mooring.uid']
+ except:
+ raise ValidationError(
+ _('File: %(filename)s: Unable to parse Mooring UID'),
+ params={'filename': filename},
+ )
+ if mooring_id not in deployments:
+ # get Assembly number from RefDes as that seems to be most consistent across CSVs
+ try:
+ ref_des = row['Reference Designator']
+ except:
+ raise ValidationError(
+ _('File: %(filename)s: Unable to parse Reference Designator'),
+ params={'filename': filename},
+ )
+ try:
+ assembly = ref_des.split('-')[0]
+ except:
+ raise ValidationError(
+ _('File: %(filename)s: Unable to parse Assembly from Reference Designator'),
+ params={'filename': filename},
+ )
+ # build data dict
+ mooring_uid_dict = {'mooring.uid': row['mooring.uid'], 'assembly': assembly, 'rows': []}
+ deployments.append(mooring_uid_dict)
+ deployment = next((deployment for deployment in deployments if deployment['mooring.uid']== row['mooring.uid']), False)
+ for key, value in row.items():
+ deployment['rows'].append({key: value})
+ return deployments_csv
+
class ImportVesselsForm(forms.Form):
- vessels_csv = forms.FileField()
+ vessels_csv = forms.FileField(
+ widget=forms.ClearableFileInput(
+ attrs={
+ 'multiple': True
+ }
+ ),
+ required=False
+ )
+
+ def clean_vessels_csv(self):
+ vessels_csv = self.files.getlist('vessels_csv')
+ counter = 0
+ for csv_file in vessels_csv:
+ counter += 1
+ filename = csv_file.name[:-4]
+ cache.set('validation_progress',{
+ 'progress': counter,
+ 'total': len(vessels_csv),
+ 'file': filename
+ })
+ try:
+ csv_file.seek(0)
+ reader = csv.DictReader(io.StringIO(csv_file.read().decode('utf-8')))
+ headers = reader.fieldnames
+ except:
+ raise ValidationError(
+ _('File: %(filename)s: Unable to decode file headers'),
+ params={'filename': filename},
+ )
+ for row in reader:
+ try:
+ vessel_name = row['Vessel Name']
+ vessel_obj = Vessel.objects.get(
+ vessel_name = vessel_name,
+ )
+ except Vessel.DoesNotExist:
+ vessel_obj = ''
+ except Vessel.MultipleObjectsReturned:
+ raise ValidationError(
+ _('File: %(filename)s, %(v_name)s: More than one Vessel associated with CSV Vessel Name'),
+ params={'filename': filename, 'v_name': vessel_name},
+ )
+ except:
+ raise ValidationError(
+ _('File: %(filename)s: Unable to parse Vessel Name'),
+ params={'filename': filename},
+ )
+ MMSI_number = None
+ IMO_number = None
+ length = None
+ max_speed = None
+ max_draft = None
+ try:
+ active = row['Active']
+ except:
+ raise ValidationError(
+ _('File: %(filename)s: Unable to parse Active'),
+ params={'filename': filename},
+ )
+ try:
+ R2R = row['R2R']
+ except:
+ raise ValidationError(
+ _('File: %(filename)s: Unable to parse R2R'),
+ params={'filename': filename},
+ )
+ try:
+ MMSI_number = row['MMSI#']
+ except:
+ raise ValidationError(
+ _('File: %(filename)s: Unable to parse MMSI'),
+ params={'filename': filename},
+ )
+ try:
+ IMO_number = row['IMO#']
+ except:
+ raise ValidationError(
+ _('File: %(filename)s: Unable to parse IMO'),
+ params={'filename': filename},
+ )
+ try:
+ length = row['Length (m)']
+ except:
+ raise ValidationError(
+ _('File: %(filename)s: Unable to parse Lenth (m)'),
+ params={'filename': filename},
+ )
+ try:
+ max_speed = row['Max Draft (m)']
+ except:
+ raise ValidationError(
+ _('File: %(filename)s: Unable to parse Max Speed (m/s)'),
+ params={'filename': filename},
+ )
+ try:
+ max_draft = row['Max Draft (m)']
+ except:
+ raise ValidationError(
+ _('File: %(filename)s: Unable to parse Max Draft (m)'),
+ params={'filename': filename},
+ )
+ return vessels_csv
class ImportCruisesForm(forms.Form):
- cruises_csv = forms.FileField()
+ cruises_csv = forms.FileField(
+ widget=forms.ClearableFileInput(
+ attrs={
+ 'multiple': True
+ }
+ ),
+ required=False
+ )
+
+ def clean_cruises_csv(self):
+ cruises_csv = self.files.getlist('cruises_csv')
+ counter = 0
+ for csv_file in cruises_csv:
+ filename = csv_file.name[:-4]
+ counter += 1
+ cache.set('validation_progress',{
+ 'progress': counter,
+ 'total': len(cruises_csv),
+ 'file': filename
+ })
+ try:
+ csv_file.seek(0)
+ reader = csv.DictReader(io.StringIO(csv_file.read().decode('utf-8')))
+ headers = reader.fieldnames
+ except:
+ raise ValidationError(
+ _('File: %(filename)s: Unable to decode file headers'),
+ params={'filename': filename},
+ )
+ for row in reader:
+ try:
+ cuid = row['CUID']
+ except:
+ raise ValidationError(
+ _('File: %(filename)s: Unable to parse CUID'),
+ params={'filename': filename},
+ )
+ try:
+ cruise_start_date = parser.parse(row['cruiseStartDateTime']).date()
+ cruise_stop_date = parser.parse(row['cruiseStopDateTime']).date()
+ except:
+ raise ValidationError(
+ _('File: %(filename)s: Unable to parse Cruise Start/Stop Dates'),
+ params={'filename': filename},
+ )
+ try:
+ vessel_name_csv = row['ShipName'].strip()
+ except:
+ raise ValidationError(
+ _('File: %(filename)s: Unable to parse Vessel Name'),
+ params={'filename': filename},
+ )
+ return cruises_csv
@@ -83,21 +301,65 @@ def validate_cal_files(csv_files,ext_files):
_('File: %(filename)s, %(value)s: Unable to parse Calibration Date from Filename'),
params={'value': cal_date_string, 'filename': cal_csv.name},
)
+ try:
+ deployment = Deployment.objects.filter(
+ deployment_to_field_date__year=cal_date_date.year,
+ deployment_to_field_date__month=cal_date_date.month,
+ deployment_to_field_date__day=cal_date_date.day,
+ )
+ assert len(deployment) < 2
+ except:
+ raise ValidationError(
+ _('File: %(filename)s, %(value)s: More than one existing Deployment associated with File Deployment Date'),
+ params={'value': cal_date_string, 'filename': cal_csv.name},
+ )
+ try:
+ custom_field = Field.objects.get(field_name='Manufacturer Serial Number')
+ except:
+ raise ValidationError(
+ _('Global Custom Field "Manufacturer Serial Number" must be created prior to import'),
+ )
+ try:
+ inv_manufacturer_serial = FieldValue.objects.get(inventory=inventory_item,field=custom_field,is_current=True)
+ except FieldValue.DoesNotExist:
+ inv_keys = {'field_value': ''}
+ inv_manufacturer_serial = SimpleNamespace(**inv_keys)
for idx, row in enumerate(reader):
row_data = row.items()
for key, value in row_data:
+ if key == 'serial':
+ try:
+ csv_manufacturer_serial = value.strip()
+ except:
+ raise ValidationError(
+ _('File: %(filename)s, Row %(row)s: Cannot parse Manufacturer Serial Number'),
+ params={'row': idx, 'filename': cal_csv.name},
+ )
+ if len(inv_manufacturer_serial.field_value) > 0 and len(csv_manufacturer_serial) > 0:
+ try:
+ assert csv_manufacturer_serial == inv_manufacturer_serial.field_value
+ except:
+ raise ValidationError(
+ _('File: %(filename)s, Row %(row)s: Manufacturer Serial Number differs between Inventory Item (%(inv_msn)s) and file (%(csv_msn)s)'),
+ params={'row': idx, 'filename': cal_csv.name, 'inv_msn': inv_manufacturer_serial.field_value, 'csv_msn':csv_manufacturer_serial},
+ )
if key == 'name':
calibration_name = value.strip()
try:
- cal_name_item = CoefficientName.objects.get(
- calibration_name = calibration_name,
- coeff_name_event = inventory_item.part.coefficient_name_events.first()
- )
+ assert len(calibration_name) > 0
except:
raise ValidationError(
- _('File: %(filename)s, Calibration Name: %(value)s, Row %(row)s: Unable to find Calibration item with this Name'),
+ _('File: %(filename)s, Calibration Name: %(value)s, Row %(row)s: Calibration Name is blank'),
params={'value': calibration_name, 'row': idx, 'filename': cal_csv.name},
)
+ try:
+ cal_name_item = CoefficientName.objects.get(
+ calibration_name = calibration_name,
+ coeff_name_event = inventory_item.part.coefficient_name_events.first()
+ )
+ except CoefficientName.DoesNotExist:
+ calname_keys = {'value_set_type': 'sl'}
+ cal_name_item = SimpleNamespace(**calname_keys)
elif key == 'value':
valset_keys = {'cal_dec_places': inventory_item.part.cal_dec_places}
mock_valset_instance = SimpleNamespace(**valset_keys)
@@ -109,8 +371,10 @@ def validate_cal_files(csv_files,ext_files):
params={'value': calibration_name,'row': idx, 'filename': cal_csv.name},
)
if '[' in raw_valset:
+ cal_name_item.value_set_type = '1d'
raw_valset = raw_valset[1:-1]
if 'SheetRef' in raw_valset:
+ cal_name_item.value_set_type = '2d'
ext_finder_filename = "__".join((cal_csv_filename,calibration_name))
try:
ref_file = [file for file in ext_files if ext_finder_filename in file.name][0]
@@ -136,14 +400,15 @@ def validate_cal_files(csv_files,ext_files):
params={'value': calibration_name, 'row': idx, 'filename': cal_csv.name},
)
-
+#
class ImportCalibrationForm(forms.Form):
- cal_csv = forms.FileField(
+ calibration_csv = forms.FileField(
widget=forms.ClearableFileInput(
attrs={
'multiple': True
}
- )
+ ),
+ required = False
)
user_draft = forms.ModelMultipleChoiceField(
queryset = User.objects.all().exclude(groups__name__in=['inventory only']).order_by('username'),
@@ -151,8 +416,8 @@ class ImportCalibrationForm(forms.Form):
label = 'Select Reviewers'
)
- def clean_cal_csv(self):
- cal_files = self.files.getlist('cal_csv')
+ def clean_calibration_csv(self):
+ cal_files = self.files.getlist('calibration_csv')
csv_files = []
ext_files = []
for file in cal_files:
diff --git a/roundabout/ooi_ci_tools/tasks.py b/roundabout/ooi_ci_tools/tasks.py
index ef01bce6e..bcbfacfcf 100644
--- a/roundabout/ooi_ci_tools/tasks.py
+++ b/roundabout/ooi_ci_tools/tasks.py
@@ -19,18 +19,25 @@
# If not, see .
"""
+
import csv
import datetime
import io
+import re
+from decimal import Decimal
from types import SimpleNamespace
from celery import shared_task
+from dateutil import parser
from django.core.cache import cache
from roundabout.calibrations.forms import parse_valid_coeff_vals
from roundabout.calibrations.models import CoefficientName, CoefficientValueSet, CalibrationEvent
-from roundabout.inventory.models import Inventory, Action
+from roundabout.configs_constants.models import ConfigName, ConfigValue, ConfigEvent
+from roundabout.cruises.models import Cruise, Vessel
+from roundabout.inventory.models import Inventory, Action, Deployment
from roundabout.inventory.utils import _create_action_history
+from roundabout.userdefinedfields.models import Field, FieldValue
@shared_task(bind = True)
@@ -43,29 +50,71 @@ def parse_cal_files(self):
counter = 0
for cal_csv in csv_files:
counter+=1
- self.update_state(state='PROGRESS', meta = {'progress': counter, 'total': len(cal_csv)})
+ self.update_state(state='PROGRESS', meta = {'progress': counter, 'total': len(csv_files)})
cal_csv_filename = cal_csv.name[:-4]
cal_csv.seek(0)
reader = csv.DictReader(io.StringIO(cal_csv.read().decode('utf-8')))
headers = reader.fieldnames
coeff_val_sets = []
+ config_val_sets = []
+ const_val_sets = []
inv_serial = cal_csv.name.split('__')[0]
cal_date_string = cal_csv.name.split('__')[1][:8]
inventory_item = Inventory.objects.get(serial_number=inv_serial)
cal_date_date = datetime.datetime.strptime(cal_date_string, "%Y%m%d").date()
- csv_event = CalibrationEvent.objects.create(
+ custom_field = Field.objects.get(field_name='Manufacturer Serial Number')
+ try:
+ inv_manufacturer_serial = FieldValue.objects.get(inventory=inventory_item,field=custom_field,is_current=True)
+ except FieldValue.DoesNotExist:
+ inv_manufacturer_serial = FieldValue.objects.create(inventory=inventory_item,field=custom_field,is_current=True)
+ try:
+ deployment = Deployment.objects.get(
+ deployment_to_field_date__year=cal_date_date.year,
+ deployment_to_field_date__month=cal_date_date.month,
+ deployment_to_field_date__day=cal_date_date.day,
+ )
+ except Deployment.DoesNotExist:
+ deployment = None
+ conf_event, created = ConfigEvent.objects.get_or_create(
+ configuration_date = cal_date_date,
+ inventory = inventory_item,
+ config_type = 'conf',
+ deployment = deployment
+ )
+ cnst_event, created = ConfigEvent.objects.get_or_create(
+ configuration_date = cal_date_date,
+ inventory = inventory_item,
+ config_type = 'cnst',
+ deployment = deployment
+ )
+ csv_event, created = CalibrationEvent.objects.get_or_create(
calibration_date = cal_date_date,
inventory = inventory_item
)
for idx, row in enumerate(reader):
row_data = row.items()
for key, value in row_data:
+ if key == 'serial':
+ csv_manufacturer_serial = value.strip()
+ if len(inv_manufacturer_serial.field_value) == 0 and len(csv_manufacturer_serial) > 0:
+ inv_manufacturer_serial.field_value = csv_manufacturer_serial
+ inv_manufacturer_serial.save()
if key == 'name':
calibration_name = value.strip()
- cal_name_item = CoefficientName.objects.get(
- calibration_name = calibration_name,
- coeff_name_event = inventory_item.part.coefficient_name_events.first()
- )
+ try:
+ cal_name_item = CoefficientName.objects.get(
+ calibration_name = calibration_name,
+ coeff_name_event = inventory_item.part.coefficient_name_events.first()
+ )
+ except CoefficientName.DoesNotExist:
+ cal_name_item = None
+ try:
+ config_name_item = ConfigName.objects.get(
+ name = calibration_name,
+ config_name_event = inventory_item.part.config_name_events.first()
+ )
+ except ConfigName.DoesNotExist:
+ config_name_item = None
elif key == 'value':
valset_keys = {'cal_dec_places': inventory_item.part.cal_dec_places}
mock_valset_instance = SimpleNamespace(**valset_keys)
@@ -81,18 +130,226 @@ def parse_cal_files(self):
raw_valset = contents
elif key == 'notes':
notes = value.strip()
- coeff_val_set = CoefficientValueSet(
- coefficient_name = cal_name_item,
- value_set = raw_valset,
- notes = notes
- )
- coeff_val_sets.append(coeff_val_set)
+ if cal_name_item:
+ coeff_val_set = {
+ 'coefficient_name': cal_name_item,
+ 'value_set': raw_valset,
+ 'notes': notes
+ }
+ coeff_val_sets.append(coeff_val_set)
+ if config_name_item:
+ if config_name_item.config_type == 'conf':
+ config_val_set = {
+ 'config_name': config_name_item,
+ 'config_value': raw_valset,
+ 'notes': notes,
+ 'config_event': conf_event
+ }
+ config_val_sets.append(config_val_set)
+ if config_name_item.config_type == 'cnst':
+ const_val_set = {
+ 'config_name': config_name_item,
+ 'config_value': raw_valset,
+ 'notes': notes,
+ 'config_event': cnst_event
+ }
+ const_val_sets.append(const_val_set)
if user_draft.exists():
- draft_users = user_draft
- for user in draft_users:
- csv_event.user_draft.add(user)
- for valset in coeff_val_sets:
- valset.calibration_event = csv_event
- valset.save()
- parse_valid_coeff_vals(valset)
- _create_action_history(csv_event, Action.CALCSVIMPORT, user)
+ for draft_user in user_draft:
+ csv_event.user_draft.add(draft_user)
+ if len(coeff_val_sets) >= 1:
+ for valset in coeff_val_sets:
+ valset['calibration_event'] = csv_event
+ coeff_val_set, created = CoefficientValueSet.objects.update_or_create(
+ coefficient_name = valset['coefficient_name'],
+ calibration_event = valset['calibration_event'],
+ defaults = {
+ 'value_set': valset['value_set'],
+ 'notes': valset['notes'],
+ }
+ )
+ parse_valid_coeff_vals(coeff_val_set)
+ _create_action_history(csv_event, Action.CALCSVIMPORT, user)
+ else:
+ csv_event.delete()
+ if len(config_val_sets) >= 1:
+ for valset in config_val_sets:
+ valset['config_event'] = conf_event
+ coeff_val_set, created = ConfigValue.objects.update_or_create(
+ config_name = valset['config_name'],
+ config_event = valset['config_event'],
+ defaults = {
+ 'config_value': valset['config_value'],
+ 'notes': valset['notes'],
+ }
+ )
+ _create_action_history(conf_event, Action.CALCSVIMPORT, user)
+ else:
+ conf_event.delete()
+ if len(const_val_sets) >= 1:
+ for valset in const_val_sets:
+ valset['config_event'] = cnst_event
+ const_val_set, created = ConfigValue.objects.update_or_create(
+ config_name = valset['config_name'],
+ config_event = valset['config_event'],
+ defaults = {
+ 'config_value': valset['config_value'],
+ 'notes': valset['notes'],
+ }
+ )
+ _create_action_history(cnst_event, Action.CALCSVIMPORT, user)
+ else:
+ cnst_event.delete()
+ cache.delete('user')
+ cache.delete('user_draft')
+ cache.delete('ext_files')
+ cache.delete('csv_files')
+
+
+
+@shared_task(bind=True)
+def parse_cruise_files(self):
+ cruises_files = cache.get('cruises_files')
+ for csv_file in cruises_files:
+ # Set up the Django file object for CSV DictReader
+ csv_file.seek(0)
+ reader = csv.DictReader(io.StringIO(csv_file.read().decode('utf-8')))
+ # Get the column headers to save with parent TempImport object
+ headers = reader.fieldnames
+ # Set up data lists for returning results
+ cruises_created = []
+ cruises_updated = []
+
+ for row in reader:
+ cuid = row['CUID']
+ cruise_start_date = parser.parse(row['cruiseStartDateTime'])
+ cruise_stop_date = parser.parse(row['cruiseStopDateTime'])
+ vessel_obj = None
+ # parse out the vessel name to match its formatting from Vessel CSV
+ vessel_name_csv = row['ShipName'].strip()
+ if vessel_name_csv == 'N/A':
+ vessel_name_csv = None
+
+ if vessel_name_csv:
+ # update or create Cruise object based on CUID field
+ vessel_obj, vessel_created = Vessel.objects.get_or_create(
+ vessel_name = vessel_name_csv,
+ )
+
+ # update or create Cruise object based on CUID field
+ cruise_obj, created = Cruise.objects.update_or_create(
+ CUID = cuid,
+ defaults = {
+ 'notes': row['notes'],
+ 'cruise_start_date': cruise_start_date,
+ 'cruise_stop_date': cruise_stop_date,
+ 'vessel': vessel_obj,
+ },
+ )
+
+ if created:
+ cruises_created.append(cruise_obj)
+ else:
+ cruises_updated.append(cruise_obj)
+ cache.delete('cruises_files')
+
+
+@shared_task(bind=True)
+def parse_vessel_files(self):
+ vessels_files = cache.get('vessels_files')
+ for csv_file in vessels_files:
+ # Set up the Django file object for CSV DictReader
+ csv_file.seek(0)
+ reader = csv.DictReader(io.StringIO(csv_file.read().decode('utf-8')))
+ # Get the column headers to save with parent TempImport object
+ headers = reader.fieldnames
+ # Set up data lists for returning results
+ vessels_created = []
+ vessels_updated = []
+ for row in reader:
+ vessel_name = row['Vessel Name'].strip()
+ MMSI_number = None
+ IMO_number = None
+ length = None
+ max_speed = None
+ max_draft = None
+ active = re.sub(r'[()]', '', row['Active'])
+ R2R = row['R2R']
+
+ if row['MMSI#']:
+ MMSI_number = int(re.sub('[^0-9]','', row['MMSI#']))
+
+ if row['IMO#']:
+ IMO_number = int(re.sub('[^0-9]','', row['IMO#']))
+
+ if row['Length (m)']:
+ length = Decimal(row['Length (m)'])
+
+ if row['Max Speed (m/s)']:
+ max_speed = Decimal(row['Max Speed (m/s)'])
+
+ if row['Max Draft (m)']:
+ max_draft = Decimal(row['Max Draft (m)'])
+
+ if active:
+ if active == 'Y':
+ active = True
+ else:
+ active = False
+ if R2R:
+ if R2R == 'Y':
+ R2R = True
+ else:
+ R2R = False
+
+ # update or create Vessel object based on vessel_name field
+ vessel_obj, created = Vessel.objects.update_or_create(
+ vessel_name = vessel_name,
+ defaults = {
+ 'prefix': row['Prefix'],
+ 'vessel_designation': row['Vessel Designation'],
+ 'ICES_code': row['ICES Code'],
+ 'operator': row['Operator'],
+ 'call_sign': row['Call Sign'],
+ 'MMSI_number': MMSI_number,
+ 'IMO_number': IMO_number,
+ 'length': length,
+ 'max_speed': max_speed,
+ 'max_draft': max_draft,
+ 'designation': row['Designation'],
+ 'active': active,
+ 'R2R': R2R,
+ },
+ )
+
+ if created:
+ vessels_created.append(vessel_obj)
+ else:
+ vessels_updated.append(vessel_obj)
+ cache.delete('vessels_files')
+
+@shared_task(bind=True)
+def parse_deployment_files(self):
+ csv_files = cache.get('dep_files')
+ for csv_file in csv_files:
+ csv_file.seek(0)
+ reader = csv.DictReader(io.StringIO(csv_file.read().decode('utf-8')))
+ headers = reader.fieldnames
+ deployments = []
+ for row in reader:
+ if row['mooring.uid'] not in deployments:
+ # get Assembly number from RefDes as that seems to be most consistent across CSVs
+ ref_des = row['Reference Designator']
+ assembly = ref_des.split('-')[0]
+ # build data dict
+ mooring_uid_dict = {'mooring.uid': row['mooring.uid'], 'assembly': assembly, 'rows': []}
+ deployments.append(mooring_uid_dict)
+
+ deployment = next((deployment for deployment in deployments if deployment['mooring.uid']== row['mooring.uid']), False)
+ for key, value in row.items():
+ deployment['rows'].append({key: value})
+
+ print(deployments[0])
+ for row in deployments[0]['rows']:
+ print(row)
+ cache.delete('dep_files')
diff --git a/roundabout/ooi_ci_tools/urls.py b/roundabout/ooi_ci_tools/urls.py
index 6438f3aef..e98be5ce5 100644
--- a/roundabout/ooi_ci_tools/urls.py
+++ b/roundabout/ooi_ci_tools/urls.py
@@ -35,6 +35,6 @@
#path('import/calibrations/upload/', view=views.ImportCalibrationsUploadView.as_view(), name='import_calibrations_upload'),
path('import/upload/success/', view=views.ImportUploadSuccessView.as_view(), name='import_upload_success'),
#Import Calibrations
- path('import/calibrations/upload/', view=views.import_calibrations, name='import_calibrations_upload'),
+ path('import/csv/upload/', view=views.import_csv, name='import_csv'),
path('import/calibrations/status/', view=views.upload_status, name='upload_status'),
]
diff --git a/roundabout/ooi_ci_tools/views.py b/roundabout/ooi_ci_tools/views.py
index 00f904a5e..1a428d6d0 100644
--- a/roundabout/ooi_ci_tools/views.py
+++ b/roundabout/ooi_ci_tools/views.py
@@ -33,8 +33,10 @@
from django.views.generic import TemplateView, FormView
from roundabout.cruises.models import Cruise, Vessel
+from roundabout.assemblies.models import Assembly
from .forms import ImportDeploymentsForm, ImportVesselsForm, ImportCruisesForm, ImportCalibrationForm
-from .tasks import parse_cal_files
+from .models import *
+from .tasks import parse_cal_files, parse_cruise_files, parse_vessel_files, parse_deployment_files
# Github CSV file importer for Vessels
@@ -73,7 +75,7 @@ def form_valid(self, form):
length = Decimal(row['Length (m)'])
if row['Max Speed (m/s)']:
- max_speed = Decimal(row['Max Draft (m)'])
+ max_speed = Decimal(row['Max Speed (m/s)'])
if row['Max Draft (m)']:
max_draft = Decimal(row['Max Draft (m)'])
@@ -139,8 +141,8 @@ def form_valid(self, form):
for row in reader:
cuid = row['CUID']
- cruise_start_date = parser.parse(row['cruiseStartDateTime']).date()
- cruise_stop_date = parser.parse(row['cruiseStopDateTime']).date()
+ cruise_start_date = parser.parse(row['cruiseStartDateTime'])
+ cruise_stop_date = parser.parse(row['cruiseStopDateTime'])
vessel_obj = None
# parse out the vessel name to match its formatting from Vessel CSV
vessel_name_csv = row['ShipName'].strip()
@@ -195,7 +197,7 @@ def form_valid(self, form):
headers = reader.fieldnames
deployments = []
for row in reader:
- if row['mooring.uid'] not in deployments:
+ if not any(dict['mooring.uid'] == row['mooring.uid'] for dict in deployments):
# get Assembly number from RefDes as that seems to be most consistent across CSVs
ref_des = row['Reference Designator']
assembly = ref_des.split('-')[0]
@@ -203,13 +205,26 @@ def form_valid(self, form):
mooring_uid_dict = {'mooring.uid': row['mooring.uid'], 'assembly': assembly, 'rows': []}
deployments.append(mooring_uid_dict)
- deployment = next((deployment for deployment in deployments if deployment['mooring.uid']== row['mooring.uid']), False)
- for key, value in row.items():
- deployment['rows'].append({key: value})
+ deployment = next((deployment for deployment in deployments if deployment['mooring.uid'] == row['mooring.uid']), False)
+ deployment['rows'].append(row)
+
+ print(deployments)
+ for deployment in deployments:
+ # get the Assembly template for this Build
+ assembly_qs = Assembly.objects.filter(assembly_number=deployment['assembly'])
+ if assembly_qs:
+ if assembly_qs.count() == 1:
+ assembly = assembly_qs[0]
+ print(assembly)
+ print(deployment['mooring.uid'])
+ else:
+ raise ValueError("Too many results")
+ else:
+ raise ValueError("No results")
- print(deployments[0])
- for row in deployments[0]['rows']:
- print(row)
+ for row in deployments[0]['rows']:
+ pass
+ #print(row['sensor.uid'])
return super(ImportDeploymentsUploadView, self).form_valid(form)
@@ -221,33 +236,6 @@ class ImportUploadSuccessView(TemplateView):
template_name = "ooi_ci_tools/import_upload_success.html"
-# CSV File Uploader for GitHub Calibration Coefficients
-class ImportCalibrationsUploadView(LoginRequiredMixin, FormView):
- form_class = ImportCalibrationForm
- template_name = 'ooi_ci_tools/import_calibrations_upload_form.html'
-
-
- def form_valid(self, form):
- cal_files = self.request.FILES.getlist('cal_csv')
- csv_files = []
- ext_files = []
- for file in cal_files:
- ext = file.name[-3:]
- if ext == 'ext':
- ext_files.append(file)
- if ext == 'csv':
- csv_files.append(file)
- cache.set('user', self.request.user, timeout=None)
- cache.set('user_draft', form.cleaned_data['user_draft'], timeout=None)
- cache.set('ext_files', ext_files, timeout=None)
- cache.set('csv_files', csv_files, timeout=None)
- job = parse_cal_files.delay()
- cache.set('import_task', job.task_id, timeout=None)
- return super(ImportCalibrationsUploadView, self).form_valid(form)
-
- def get_success_url(self):
- return reverse('ooi_ci_tools:import_upload_success', )
-
def upload_status(request):
# import_task = cache.get('import_task')
@@ -261,31 +249,73 @@ def upload_status(request):
'progress': result,
})
-def import_calibrations(request):
+# Deployment CSV Importer
+def import_deployments(csv_files):
+ cache.set('dep_files',csv_files, timeout=None)
+ job = parse_deployment_files.delay()
+
+
+# Cruise CSV Importer
+def import_cruises(cruises_files):
+ cache.set('cruises_files', cruises_files, timeout=None)
+ job = parse_cruise_files.delay()
+
+# Vessel CSV Importer
+def import_vessels(vessels_files):
+ cache.set('vessels_files', vessels_files, timeout=None)
+ job = parse_vessel_files.delay()
+
+# Calibration CSV Importer
+def import_calibrations(cal_files, user, user_draft):
+ csv_files = []
+ ext_files = []
+ for file in cal_files:
+ ext = file.name[-3:]
+ if ext == 'ext':
+ ext_files.append(file)
+ if ext == 'csv':
+ csv_files.append(file)
+ cache.set('user', user, timeout=None)
+ cache.set('user_draft', user_draft, timeout=None)
+ cache.set('ext_files', ext_files, timeout=None)
+ cache.set('csv_files', csv_files, timeout=None)
+ job = parse_cal_files.delay()
+ cache.set('import_task', job.task_id, timeout=None)
+
+# CSV Importer View
+# Activates parsing tasks based on selected files
+def import_csv(request):
confirm = ""
if request.method == "POST":
- form = ImportCalibrationForm(request.POST, request.FILES)
- if form.is_valid():
- cal_files = request.FILES.getlist('cal_csv')
- csv_files = []
- ext_files = []
- for file in cal_files:
- ext = file.name[-3:]
- if ext == 'ext':
- ext_files.append(file)
- if ext == 'csv':
- csv_files.append(file)
- cache.set('user', request.user, timeout=None)
- cache.set('user_draft', form.cleaned_data['user_draft'], timeout=None)
- cache.set('ext_files', ext_files, timeout=None)
- cache.set('csv_files', csv_files, timeout=None)
- job = parse_cal_files.delay()
- cache.set('import_task', job.task_id, timeout=None)
- return redirect(reverse("ooi_ci_tools:import_calibrations_upload") + "?confirm=True")
+ cal_form = ImportCalibrationForm(request.POST, request.FILES)
+ dep_form = ImportDeploymentsForm(request.POST, request.FILES)
+ cruises_form = ImportCruisesForm(request.POST, request.FILES)
+ vessels_form = ImportVesselsForm(request.POST, request.FILES)
+ cal_files = request.FILES.getlist('calibration_csv')
+ dep_files = request.FILES.getlist('deployments_csv')
+ cruises_file = request.FILES.getlist('cruises_csv')
+ vessels_file = request.FILES.getlist('vessels_csv')
+ if cal_form.is_valid() and len(cal_files) >= 1:
+ import_calibrations(cal_files, request.user, cal_form.cleaned_data['user_draft'])
+ confirm = "True"
+ if dep_form.is_valid() and len(dep_files) >= 1:
+ import_deployments(dep_files)
+ confirm = "True"
+ if cruises_form.is_valid() and len(cruises_file) >= 1:
+ import_cruises(cruises_file)
+ confirm = "True"
+ if vessels_form.is_valid() and len(vessels_file) >= 1:
+ import_vessels(vessels_file)
+ confirm = "True"
else:
- form = ImportCalibrationForm()
- confirm = request.GET.get("confirm")
- return render(request, 'ooi_ci_tools/import_calibrations_upload_form.html', {
- "form": form,
+ cal_form = ImportCalibrationForm()
+ dep_form = ImportDeploymentsForm()
+ cruises_form = ImportCruisesForm()
+ vessels_form = ImportVesselsForm()
+ return render(request, 'ooi_ci_tools/import_tool.html', {
+ "form": cal_form,
+ 'dep_form': dep_form,
+ 'cruises_form': cruises_form,
+ 'vessels_form': vessels_form,
'confirm': confirm
})
diff --git a/roundabout/parts/forms.py b/roundabout/parts/forms.py
index b278757df..2081e57bc 100644
--- a/roundabout/parts/forms.py
+++ b/roundabout/parts/forms.py
@@ -1,7 +1,7 @@
"""
# Copyright (C) 2019-2020 Woods Hole Oceanographic Institution
#
-# This file is part of the Roundabout Database project ("RDB" or
+# This file is part of the Roundabout Database project ("RDB" or
# "ooicgsn-roundabout").
#
# ooicgsn-roundabout is free software: you can redistribute it and/or modify
@@ -23,11 +23,13 @@
import re
from django import forms
+from django.shortcuts import get_object_or_404
from django.core.exceptions import ValidationError
from django.forms.models import inlineformset_factory
from django.template.defaultfilters import slugify
from django_summernote.widgets import SummernoteWidget
from bootstrap_datepicker_plus import DatePickerInput, DateTimePickerInput
+from mptt.forms import TreeNodeChoiceField
from .models import Part, PartType, Documentation, Revision
from roundabout.locations.models import Location
@@ -183,3 +185,25 @@ class Meta:
'name': 'Part Type Name',
'ccc_toggle': 'Enable Configs, Constants, and Calibration Coefficients'
}
+
+
+"""
+Custom Deletion form for PartType
+User needs to be able to choose a new PartType for existing Part Templates or they
+disappear from tree nav
+"""
+class PartTypeDeleteForm(forms.Form):
+ new_part_type = TreeNodeChoiceField(label='Select new Part Type', queryset=PartType.objects.all())
+
+ def __init__(self, *args, **kwargs):
+ part_type_pk= kwargs.pop('pk')
+ part_type_to_delete = get_object_or_404(PartType, id=part_type_pk)
+
+ super(PartTypeDeleteForm, self).__init__(*args, **kwargs)
+ # Check if this PartType has IPart Templates, remove new field if false
+ if not part_type_to_delete.parts.exists():
+ self.fields['new_part_type'].required = False
+ self.fields['new_part_type'].widget = forms.HiddenInput()
+
+ # remove this object from the possible replacements
+ self.fields['new_part_type'].queryset = PartType.objects.exclude(id=part_type_to_delete.id)
diff --git a/roundabout/parts/migrations/0011_merge_20201015_1401.py b/roundabout/parts/migrations/0011_merge_20201015_1401.py
new file mode 100644
index 000000000..2e56e7e3b
--- /dev/null
+++ b/roundabout/parts/migrations/0011_merge_20201015_1401.py
@@ -0,0 +1,14 @@
+# Generated by Django 3.0.10 on 2020-10-15 14:01
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('parts', '0010_merge_20201015_1353'),
+ ('parts', '0010_auto_20201014_1253'),
+ ]
+
+ operations = [
+ ]
diff --git a/roundabout/parts/migrations/0012_merge_20201016_1512.py b/roundabout/parts/migrations/0012_merge_20201016_1512.py
new file mode 100644
index 000000000..41b8496eb
--- /dev/null
+++ b/roundabout/parts/migrations/0012_merge_20201016_1512.py
@@ -0,0 +1,14 @@
+# Generated by Django 3.0.10 on 2020-10-16 15:12
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('parts', '0011_merge_20201016_1334'),
+ ('parts', '0011_merge_20201015_1401'),
+ ]
+
+ operations = [
+ ]
diff --git a/roundabout/parts/migrations/0013_merge_20201106_1900.py b/roundabout/parts/migrations/0013_merge_20201106_1900.py
new file mode 100644
index 000000000..2ab155f55
--- /dev/null
+++ b/roundabout/parts/migrations/0013_merge_20201106_1900.py
@@ -0,0 +1,14 @@
+# Generated by Django 3.0.10 on 2020-11-06 19:00
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('parts', '0012_merge_20201016_1512'),
+ ('parts', '0012_merge_20201106_1538'),
+ ]
+
+ operations = [
+ ]
diff --git a/roundabout/parts/migrations/0013_merge_20201110_1515.py b/roundabout/parts/migrations/0013_merge_20201110_1515.py
new file mode 100644
index 000000000..505b2ab7b
--- /dev/null
+++ b/roundabout/parts/migrations/0013_merge_20201110_1515.py
@@ -0,0 +1,14 @@
+# Generated by Django 3.0.10 on 2020-11-10 15:15
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('parts', '0012_merge_20201106_1538'),
+ ('parts', '0012_merge_20201016_1512'),
+ ]
+
+ operations = [
+ ]
diff --git a/roundabout/parts/migrations/0014_merge_20201116_1621.py b/roundabout/parts/migrations/0014_merge_20201116_1621.py
new file mode 100644
index 000000000..cf10b1b29
--- /dev/null
+++ b/roundabout/parts/migrations/0014_merge_20201116_1621.py
@@ -0,0 +1,14 @@
+# Generated by Django 3.0.10 on 2020-11-16 16:21
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('parts', '0013_merge_20201110_1515'),
+ ('parts', '0013_merge_20201106_1900'),
+ ]
+
+ operations = [
+ ]
diff --git a/roundabout/parts/migrations/0015_auto_20201116_1949.py b/roundabout/parts/migrations/0015_auto_20201116_1949.py
new file mode 100644
index 000000000..c5f6c5cfd
--- /dev/null
+++ b/roundabout/parts/migrations/0015_auto_20201116_1949.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.0.10 on 2020-11-16 19:49
+
+import django.core.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('parts', '0014_merge_20201116_1621'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='part',
+ name='cal_dec_places',
+ field=models.IntegerField(blank=True, default=15, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(32)]),
+ ),
+ ]
diff --git a/roundabout/parts/models.py b/roundabout/parts/models.py
index d6996cb8b..4bb55f6e5 100644
--- a/roundabout/parts/models.py
+++ b/roundabout/parts/models.py
@@ -57,7 +57,7 @@ class Part(models.Model):
note = models.TextField(blank=True)
custom_fields = JSONField(blank=True, null=True)
user_defined_fields = models.ManyToManyField(Field, blank=True, related_name='parts')
- cal_dec_places = models.IntegerField(validators=[MinValueValidator(0), MaxValueValidator(32)], null=False, blank=True, default=8)
+ cal_dec_places = models.IntegerField(validators=[MinValueValidator(0), MaxValueValidator(32)], null=False, blank=True, default=15)
class Meta:
ordering = ['name']
diff --git a/roundabout/parts/views.py b/roundabout/parts/views.py
index 4a08ef0cd..922b1a2f1 100644
--- a/roundabout/parts/views.py
+++ b/roundabout/parts/views.py
@@ -31,7 +31,7 @@
from django.template.defaultfilters import slugify
from .models import Part, PartType, Revision, Documentation
-from .forms import PartForm, PartTypeForm, RevisionForm, DocumentationFormset, RevisionFormset, PartUdfAddFieldForm, PartUdfFieldSetValueForm
+from .forms import PartForm, PartTypeForm, RevisionForm, DocumentationFormset, RevisionFormset, PartUdfAddFieldForm, PartUdfFieldSetValueForm, PartTypeDeleteForm
from roundabout.calibrations.forms import PartCalNameFormset
from roundabout.locations.models import Location
from roundabout.inventory.models import Inventory
@@ -827,13 +827,45 @@ def get_success_url(self):
return reverse('parts:parts_type_home', )
-class PartsTypeDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
- model = PartType
+class PartsTypeDeleteView(LoginRequiredMixin, PermissionRequiredMixin, FormView):
+ form_class = PartTypeDeleteForm
template_name = 'parts/part_type_confirm_delete.html'
- success_url = reverse_lazy('parts:parts_type_home')
permission_required = 'parts.delete_part'
redirect_field_name = 'home'
+ def get_context_data(self, **kwargs):
+ context = super(PartsTypeDeleteView, self).get_context_data(**kwargs)
+ part_type = PartType.objects.get(id=self.kwargs['pk'])
+
+ context.update({
+ 'part_type': part_type
+ })
+ return context
+
+ def get_form_kwargs(self):
+ kwargs = super(PartsTypeDeleteView, self).get_form_kwargs()
+ if 'pk' in self.kwargs:
+ kwargs['pk'] = self.kwargs['pk']
+ return kwargs
+
+ def form_valid(self, form):
+ new_part_type = form.cleaned_data['new_part_type']
+ part_type_to_delete = PartType.objects.get(id=self.kwargs['pk'])
+
+ # Need to check if there's Part Templates. If so, need move them to new Part Type.
+ if part_type_to_delete.parts.exists():
+ for part in part_type_to_delete.parts.all():
+ part.part_type = new_part_type
+ part.save()
+
+ # Delete the Part Type object
+ part_type_to_delete.delete()
+ return HttpResponseRedirect(self.get_success_url())
+
+ def get_success_url(self):
+ return reverse('parts:parts_type_home')
+
+
# Direct detail view
class PartsTypeDetailView(LoginRequiredMixin, DetailView):
model = PartType
@@ -852,6 +884,7 @@ def post(self, request, *args, **kwargs):
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
+
# AJAX Views
class PartsTypeAjaxDetailView(LoginRequiredMixin , DetailView):
diff --git a/roundabout/search/mixins.py b/roundabout/search/mixins.py
index f5b115f81..dcb746d21 100644
--- a/roundabout/search/mixins.py
+++ b/roundabout/search/mixins.py
@@ -26,7 +26,7 @@
class TableExportStream(TableExport):
- def table_to_dataset(self, table, exclude_columns):
+ def table_to_dataset(self, table, exclude_columns, dataset_kwargs=None):
"""A generator that returns a tablib dataset for each row of the table."""
table_rows = table.as_values(exclude_columns=exclude_columns)
headers = next(table_rows)
diff --git a/roundabout/search/urls.py b/roundabout/search/urls.py
index 5051d54e7..9f0498cbe 100644
--- a/roundabout/search/urls.py
+++ b/roundabout/search/urls.py
@@ -21,6 +21,7 @@
from django.urls import path
+from . import user_search
from . import views
app_name = 'search'
@@ -33,4 +34,5 @@
path('parts',view=views.PartTableView.as_view(),name='part'),
path('assembly', view=views.AssemblyTableView.as_view(), name='assembly'),
path('actions', view=views.ActionTableView.as_view(), name='action'),
+ path('user', view=user_search.UserSearchView.as_view(), name='user'),
]
diff --git a/roundabout/search/user_search.py b/roundabout/search/user_search.py
new file mode 100644
index 000000000..35d60a31f
--- /dev/null
+++ b/roundabout/search/user_search.py
@@ -0,0 +1,262 @@
+"""
+# Copyright (C) 2019-2020 Woods Hole Oceanographic Institution
+#
+# This file is part of the Roundabout Database project ("RDB" or
+# "ooicgsn-roundabout").
+#
+# ooicgsn-roundabout is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# ooicgsn-roundabout is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with ooicgsn-roundabout in the COPYING.md file at the project root.
+# If not, see .
+"""
+
+import django_tables2 as tables2
+from django import forms
+from django.contrib.auth.mixins import LoginRequiredMixin
+from django.db.models import Q
+from django.utils.html import format_html
+from django.views.generic import TemplateView
+from django_tables2.columns import Column, DateTimeColumn, ManyToManyColumn, BooleanColumn
+from django_tables2_column_shifter.tables import ColumnShiftTable
+
+from roundabout.builds.models import BuildAction
+from roundabout.calibrations.models import CalibrationEvent, CoefficientNameEvent
+from roundabout.configs_constants.models import ConfigEvent, ConfigNameEvent, ConstDefaultEvent, ConfigDefaultEvent
+from roundabout.inventory.models import Action, DeploymentAction
+from roundabout.users.models import User
+
+
+# ========= TABLE BASES ========== #
+
+class UserTableBase(tables2.Table):
+ class Meta:
+ template_name = "django_tables2/bootstrap4.html"
+ attrs = {'style': 'display: block; overflow-x: auto;'}
+ model = None
+ title = None
+
+class CCCUserTableBase(UserTableBase):
+ class Meta(UserTableBase.Meta):
+ fields = ['approved', 'user_draft', 'user_approver', 'created_at', 'detail']
+ approved = BooleanColumn()
+ user_approver = ManyToManyColumn(verbose_name='Approvers', accessor='user_approver', transform=lambda x: x.name, default='')
+ user_draft = ManyToManyColumn(verbose_name='Reviewers', accessor='user_draft', transform=lambda x: x.name, default='')
+ created_at = DateTimeColumn(verbose_name='Date Entered', accessor='created_at', format='Y-m-d H:i')
+ detail = Column(verbose_name='Note', accessor='detail')
+
+class ActionUserTableBase(UserTableBase):
+ class Meta(UserTableBase.Meta):
+ fields = ['action_type', 'user__name', 'created_at', 'detail']
+ user__name = Column(verbose_name='User')
+
+# ========= TABLES ========== #
+
+## CCC Events ##
+
+class CalibrationTable(CCCUserTableBase):
+ class Meta(CCCUserTableBase.Meta):
+ model = CalibrationEvent
+ title = 'Calibration Events'
+ fields = ['inventory__serial_number'] + CCCUserTableBase.Meta.fields
+ inventory__serial_number = Column(verbose_name='Inventory', linkify=dict(viewname="inventory:inventory_detail", args=[tables2.A('inventory__pk')]))
+ value_names = ManyToManyColumn(verbose_name='Coefficient Names', accessor='coefficient_value_sets', transform=lambda x: x.coefficient_name)
+ #value_notes = ManyToManyColumn(verbose_name='Coefficient Notes', accessor='coefficient_value_sets',
+ # transform=lambda x: format_html('{}: [{}]
'.format(x.coefficient_name,x.notes)) if x.notes else '', separator='\n')
+
+class ConfigConstTable(CCCUserTableBase):
+ class Meta(CCCUserTableBase.Meta):
+ model = ConfigEvent
+ title = 'Configuration/Constant Events'
+ fields = ['inventory__serial_number'] + CCCUserTableBase.Meta.fields
+ inventory__serial_number = Column(verbose_name='Inventory', linkify=dict(viewname="inventory:inventory_detail", args=[tables2.A('inventory__pk')]))
+ value_names = ManyToManyColumn(verbose_name='Config/Constant Names', accessor='config_values', transform=lambda x: x.config_name)
+ #value_notes = ManyToManyColumn(verbose_name='Config/Constant Notes', accessor='config_values',
+ # transform=lambda x: format_html('{}: [{}]
'.format(x.config_name,x.notes)) if x.notes else '', separator='\n')
+
+
+## CCC Name Events ##
+
+class CoefficientNameEventTable(CCCUserTableBase):
+ class Meta(CCCUserTableBase.Meta):
+ model = CoefficientNameEvent
+ title = 'Calibration Name-Events'
+ fields = ['part__name'] + CCCUserTableBase.Meta.fields
+ part__name = Column(verbose_name='Part', linkify=dict(viewname="parts:parts_detail", args=[tables2.A('part__pk')]))
+ value_names = ManyToManyColumn(verbose_name='Coefficient Names', accessor='coefficient_names', transform=lambda x: x.calibration_name)
+
+class ConfigNameEventTable(CCCUserTableBase):
+ class Meta(CCCUserTableBase.Meta):
+ model = ConfigNameEvent
+ title = 'Configuration/Constant Name-Events'
+ fields = ['part__name'] + CCCUserTableBase.Meta.fields
+ part__name = Column(verbose_name='Part', linkify=dict(viewname="parts:parts_detail", args=[tables2.A('part__pk')]))
+ value_names = ManyToManyColumn(verbose_name='Configuration Names', accessor='config_names', transform=lambda x: x.name)
+
+
+## CCC Default Events ##
+
+class ConfigDefaultEventTable(CCCUserTableBase):
+ class Meta(CCCUserTableBase.Meta):
+ model = ConfigDefaultEvent
+ title = 'Configuration Default-Events'
+ fields = ['assembly_part__part__name'] + CCCUserTableBase.Meta.fields
+ assembly_part__part__name = Column(verbose_name='Inventory', linkify=dict(viewname="assemblies:assemblypart_detail", args=[tables2.A('assembly_part__pk')]))
+ value_names = ManyToManyColumn(verbose_name='Configurations', accessor='config_defaults',
+ transform=lambda x: format_html('{}: {}
'.format(x.config_name,x.default_value)), separator='\n')
+
+class ConstDefaultEventTable(CCCUserTableBase):
+ class Meta(CCCUserTableBase.Meta):
+ model = ConstDefaultEvent
+ title = 'Constant Default-Events'
+ fields = ['inventory__serial_number'] + CCCUserTableBase.Meta.fields
+ inventory__serial_number = Column(verbose_name='Inventory', linkify=dict(viewname="inventory:inventory_detail", args=[tables2.A('inventory__pk')]))
+ value_names = ManyToManyColumn(verbose_name='Constants', accessor='constant_defaults',
+ transform=lambda x: format_html('{}: {}
'.format(x.config_name,x.default_value)), separator='\n')
+
+## Actions ##
+
+class ActionTable(ActionUserTableBase):
+ class Meta(ActionUserTableBase.Meta):
+ model = Action
+ title = 'Misc. Actions'
+
+class BuildActionTable(ActionUserTableBase):
+ class Meta(ActionUserTableBase.Meta):
+ model = BuildAction
+ title = 'Build Actions'
+ fields = ['build'] + ActionUserTableBase.Meta.fields
+ build = Column(linkify=dict(viewname="builds:builds_detail", args=[tables2.A('build__pk')]))
+
+class DeploymentActionTable(ActionUserTableBase):
+ class Meta(ActionUserTableBase.Meta):
+ model = DeploymentAction
+ title = 'Deployment Actions'
+ fields = ['deployment'] + ActionUserTableBase.Meta.fields
+
+ # workaround linking to deployment.
+ def render_deployment(self, record):
+ from django.urls import reverse
+ build_url = reverse("builds:builds_detail", args=[record.deployment.build.pk])
+ deployment_anchor = '#deployment-{}-'.format(record.deployment.pk) # doesn't work, anchor doesn't exist
+ deployment_anchor = '#deployments' # next best anchor that does work
+ html_string = '{}'.format(build_url+deployment_anchor, record.deployment)
+ return format_html(html_string)
+
+
+# ========= FORM STUFF ========= #
+
+class ListTextWidget(forms.TextInput):
+ def __init__(self, data_list, name, *args, **kwargs):
+ super(ListTextWidget, self).__init__(*args, **kwargs)
+ self._name = name
+ self._list = data_list
+ self.attrs.update({'list':'list__{}'.format(self._name)})
+
+ def render(self, name, value, attrs=None, renderer=None):
+ text_html = super(ListTextWidget, self).render(name, value, attrs=attrs)
+ data_list = ''
+ return (text_html + data_list)
+
+
+class UserSearchForm(forms.Form):
+ q = forms.CharField(required=True, label='Name')
+ ccc_role = forms.ChoiceField(label='CCC Role',choices=[('both','Both'),('app','Approver'),('rev','Reviewer')])
+ ccc_status = forms.ChoiceField(label='CCC Status',choices=[('all','Show All'),('app','Show Approved'),('uapp','Show UnApproved')])
+
+ def __init__(self, *args, **kwargs):
+ default_userlist = User.objects.all().values_list('username',flat=True)
+ userlist = kwargs.pop('data_list', default_userlist)
+ super(UserSearchForm, self).__init__(*args, **kwargs)
+ self.fields['q'].widget=ListTextWidget(userlist, name='userlist')
+
+
+# ========= VIEWS ========= #
+
+class UserSearchView(LoginRequiredMixin, tables2.MultiTableMixin, TemplateView):
+ template_name = 'search/ReviewerApproverSearch.html'
+ form_class = UserSearchForm
+ table_pagination = {"per_page": 10}
+
+ tables = [CalibrationTable,
+ ConfigConstTable,
+ CoefficientNameEventTable,
+ ConfigNameEventTable,
+ ConfigDefaultEventTable,
+ ConstDefaultEventTable,
+ DeploymentActionTable,
+ BuildActionTable,
+ ActionTable]
+
+ def __init__(self,*args,**kwargs):
+ super().__init__(*args,**kwargs)
+
+ # disable ColumnShiftTable behavior
+ for table in self.tables:
+ if issubclass(table,ColumnShiftTable):
+ table.shift_table_column = False
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ for table in context['tables']:
+ table.attrs['title'] = table.Meta.title if hasattr(table.Meta,'title') else table.__name__.replace('Table','s')
+ return context
+
+ def get_tables_data(self):
+ if 'q' in self.request.GET:
+ user_query = self.request.GET.get('q')
+ ccc_role = self.request.GET.get('ccc_role',None)
+ ccc_status = self.request.GET.get('ccc_status',None)
+ else: # defaults
+ user_query = self.request.user.username
+ ccc_role = 'both'
+ ccc_status = 'all'
+
+ ccc_Q_approver = Q(user_approver__username__icontains=user_query) | Q(user_approver__name__icontains=user_query)
+ ccc_Q_draft = Q(user_draft__username__icontains=user_query) | Q(user_draft__name__icontains=user_query)
+ action_Q = Q(user__username__icontains=user_query) | Q(user__name__icontains=user_query)
+
+ if ccc_role == 'both':
+ ccc_Q = ccc_Q_approver | ccc_Q_draft
+ elif ccc_role == 'app': ccc_Q = ccc_Q_approver
+ elif ccc_role == 'rev': ccc_Q = ccc_Q_draft
+ else: ccc_Q = Q(pk__in=[]) # else select none
+
+ if ccc_status=='uapp':
+ ccc_Q = ccc_Q & Q(approved=False)
+ elif ccc_status=='app':
+ ccc_Q = ccc_Q & Q(approved=True)
+ # else show all CCCs regardless of approval status
+
+ qs_list = []
+ for table in self.tables:
+ if table.Meta.model in [Action, BuildAction, DeploymentAction]:
+ action_qs = table.Meta.model.objects.select_related('user').filter(action_Q)
+ qs_list.append(action_qs)
+ else: # it's a CCC_qs
+ ccc_model = table.Meta.model
+ ccc_qs = ccc_model.objects.prefetch_related('user_approver','user_draft').filter(ccc_Q)
+ qs_list.append(ccc_qs)
+
+ return qs_list
+
+ def get(self, request, *args, **kwargs):
+ initial = dict(q=request.user.username, ccc_role='both', ccc_status='all')
+ if 'q' in request.GET:
+ form = self.form_class(request.GET)
+ else:
+ form = self.form_class(initial)
+ context = self.get_context_data()
+ context['form'] = form
+ return self.render_to_response(context)
diff --git a/roundabout/search/views.py b/roundabout/search/views.py
index bcaf4f132..8ea08f7d3 100644
--- a/roundabout/search/views.py
+++ b/roundabout/search/views.py
@@ -27,10 +27,7 @@
import django_tables2 as tables
from django.contrib.auth.mixins import LoginRequiredMixin
-#from django.template.defaultfilters import register
-#from django.shortcuts import render, get_object_or_404
#from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector
-#from django.http import HttpResponseRedirect, HttpResponse, JsonResponse, QueryDict
from django.db.models import Q, Count, OuterRef, Subquery
from django.shortcuts import redirect
from django.urls import reverse
@@ -67,18 +64,19 @@ def searchbar_redirect(request):
elif model=='calibrations':
if fnmatch(query.strip(),'????-??-??'):
query = query.strip()
- getstr = '?f=.0.calibration_event__calibration_date&l=.0.exact&q=.0.{query}'
+ getstr = '?f=.0.calibration_event__calibration_date&l=.0.date&q=.0.{query}'
else: getstr = '?f=.0.calibration_event__inventory__serial_number&f=.0.calibration_event__inventory__part__name&f=.0.coefficient_name__calibration_name&f=.0.calibration_event__user_approver__any__name&f=.0.calibration_event__user_draft__any__name&f=.0.notes&l=.0.icontains&q=.0.{query}'
elif model == 'configconsts':
if fnmatch(query.strip(), '????-??-??'):
query = query.strip()
- getstr = '?f=.0.config_event__configuration_date&l=.0.exact&q=.0.{query}'
+ getstr = '?f=.0.config_event__configuration_date&l=.0.date&q=.0.{query}'
else:
getstr = '?f=.0.config_event__inventory__serial_number&f=.0.config_event__inventory__part__name&f=.0.config_name__name&f=.0.config_event__user_approver__any__name&f=.0.config_event__user_draft__any__name&f=.0.notes&l=.0.icontains&q=.0.{query}'
elif model=='part': getstr = '?f=.0.part_number&f=.0.name&f=.0.friendly_name&l=.0.icontains&q=.0.{query}'
elif model == 'build': getstr = '?f=.0.build_number&f=.0.assembly__name&f=.0.assembly__assembly_type__name&f=.0.assembly__description&f=.0.build_notes&f=.0.location__name&l=.0.icontains&q=.0.{query}'
elif model == 'assembly': getstr = '?f=.0.assembly_number&f=.0.name&f=.0.assembly_type__name&f=.0.description&l=.0.icontains&q=.0.{query}'
elif model == 'action': getstr = '?f=.0.action_type&f=.0.user__name&f=.0.detail&f=.0.location__name&f=.0.inventory__serial_number&f=.0.inventory__part__name&l=.0.icontains'+'&q=.0.{query}'
+ elif model == 'user': getstr = '?ccc_role=both&ccc_status=all'+'&q={query}'
getstr = getstr.format(query=query)
resp['Location'] += getstr
return resp
@@ -99,6 +97,7 @@ def get_search_cards(self):
queries = self.request.GET.getlist('q')
negas = self.request.GET.getlist('n')
+ # the +['t'] corresponds to the t of "... for c,r,v,t in ..." below
fields = [unquote(f).split('.',2)+['f'] for f in fields]
lookups = [unquote(l).split('.',2)+['l'] for l in lookups]
queries = [unquote(q).split('.',2)+['q'] for q in queries]
@@ -121,19 +120,35 @@ def get_search_cards(self):
query = [v for v,t in row_items if t=='q']
nega = [v for v,t in row_items if t=='n']
try:
- assert len(fields) >=1
- assert len(lookup)==1
- assert len(query)==1 and query[0]
+ assert len(fields) >= 1
+ assert len(lookup) == 1
+ assert len(query) == 1
assert len(nega) <= 1
+ lookup,query = lookup[0],query[0]
+ multi_bool = len(fields) > 1
except AssertionError:
continue #skip
+ # Searching for Null/None field values
+ if lookup == 'isnull':
+ query = True if query == 'True' else False
+
+ # querying for empty strings
+ if query == '' and lookup not in ['exact','iexact']:
+ continue #skip empty
+
+ # hack: implicitly search for usernames in addition to a user's name
+ for f in fields[:]:
+ if 'user__name' in f:
+ extra_f = f.replace('user__name','user__username')
+ fields.append(extra_f)
+
row = dict( #row_id=row_id,
fields=fields,
- lookup=lookup[0],
- query=query[0],
+ lookup=lookup,
+ query=query,
nega=bool(nega),
- multi=len(fields) > 1)
+ multi=multi_bool)
rows.append(row)
# choice field hack
@@ -221,6 +236,7 @@ def make_Qkwarg(field,row):
Q_kwarg = {'id__in': matched_model_IDs}
return Q_kwarg
+
cards = self.get_search_cards()
final_Qs = []
@@ -286,8 +302,10 @@ def get_context_data(self, **kwargs):
avail_lookups = [dict(value='icontains',text='Contains'),
dict(value='exact', text='Exact'),
+ dict(value='date', text='Date'),
dict(value='gte', text='>='),
- dict(value='lte', text='<='),]
+ dict(value='lte', text='<='),
+ dict(value='isnull', text='Is-Null')]
context['avail_lookups'] = json.dumps(avail_lookups)
lcats = dict(
@@ -300,6 +318,7 @@ def get_context_data(self, **kwargs):
ITER_LOOKUP=['in']+['icontains', 'exact'],
EXACT_LOOKUP = ['exact'],
BOOL_LOOKUP = ['exact','iexact'], )
+ lcats = {k:v+['isnull'] for k,v in lcats.items()}
context['lookup_categories'] = json.dumps(lcats)
avail_fields_sans_col_args = self.get_avail_fields()
@@ -625,7 +644,7 @@ class ActionTableView(GenericSearchTableView):
def get_avail_fields():
avail_fields = [dict(value="action_type", text="Action Type", legal_lookup='STR_LOOKUP'),
dict(value="user__name", text="User", legal_lookup='STR_LOOKUP'),
- dict(value="created_at", text="Timestamp", legal_lookup='DATETIME_LOOKUP'),
+ dict(value="created_at", text="Timestamp", legal_lookup='DATE_LOOKUP'),
dict(value="detail", text="Detail", legal_lookup='STR_LOOKUP'),
dict(value="location__name",text="Location",legal_lookup='STR_LOOKUP'),
dict(value="inventory__serial_number", text="Inventory: Serial Number", legal_lookup='STR_LOOKUP'),
@@ -672,6 +691,8 @@ def get_queryset(self):
# final query results must be CalibrationEvents.
calibration_event_ids = qs.values_list('calibration_event__id', flat=True)
qs = CalibrationEvent.objects.filter(id__in=calibration_event_ids)
+ qs = qs.prefetch_related(*[pf.replace('calibration_event__','') for pf in self.query_prefetch if pf.startswith('calibration_event__')])
+ qs = qs.select_related('inventory__part__part_type').exclude(inventory__part__part_type__ccc_toggle=False)
return qs
def get_table_kwargs(self):
# since search model is CoefficientValueSet and results are CalibrationEvents,
@@ -686,7 +707,7 @@ def get_table_kwargs(self):
class ConfigConstTableView(GenericSearchTableView):
model = ConfigValue
table_class = ConfigConstTable
- query_prefetch = ['config_name','config_event','config_event__inventory','config_event__inventory__part','config_event__user_approver','config_event__draft_approver']
+ query_prefetch = ['config_name','config_event','config_event__inventory','config_event__inventory__part','config_event__user_approver','config_event__user_draft']
@staticmethod
def get_avail_fields():
@@ -712,6 +733,8 @@ def get_queryset(self):
# final query results must be CalibrationEvents.
config_event_ids = qs.values_list('config_event__id', flat=True)
qs = ConfigEvent.objects.filter(id__in=config_event_ids)
+ qs = qs.prefetch_related(*[pf.replace('config_event__','') for pf in self.query_prefetch if pf.startswith('config_event__')])
+ qs = qs.select_related('inventory__part__part_type').exclude(inventory__part__part_type__ccc_toggle=False)
return qs
def get_table_kwargs(self):
# since search model is ConfigValue and results are ConfigEvents,
@@ -723,6 +746,3 @@ def get_table_kwargs(self):
return {'extra_columns':[]}
-# TODO see sn CGINS-DOSTAD-00134 for approver/reviewer search functionality
-# http://0.0.0.0:8000/search/calibrations?f=.0.calibration_event__inventory__serial_number&l=.0.icontains&q=.0.CGINS-DOSTAD-00134
-# http://0.0.0.0:8000/search/inventory?f=.0.serial_number&l=.0.icontains&q=.0.CGINS-DOSTAD-00134
diff --git a/roundabout/static/css/project.css b/roundabout/static/css/project.css
index ccb181284..fb05f528c 100644
--- a/roundabout/static/css/project.css
+++ b/roundabout/static/css/project.css
@@ -193,10 +193,6 @@ a.wh {
color: #eee !important;
}
-#inventory-tabs table th {
- border-top: none;
-}
-
#inventory-tabs .list-group-item {
padding-right: 0;
}
@@ -215,6 +211,10 @@ a.wh {
font-size: 12px;
}
+.action-table th {
+ border-top: none;
+}
+
#history {
max-height:500px;
overflow-y: scroll;
@@ -269,6 +269,11 @@ a.wh {
white-space: nowrap;
}
+.item-data-table th {
+ width: 28%;
+ color: #666;
+}
+
/* Template Mode styling for apps that are considered Templates */
.template #detail-view {
border-color: #28a745;
@@ -293,7 +298,6 @@ a.wh {
footer {
background-color: white;
- color: white;
}
/* Table Header Ordering */
diff --git a/roundabout/static/js/form-search.js b/roundabout/static/js/form-search.js
index 30870da1b..0df248345 100644
--- a/roundabout/static/js/form-search.js
+++ b/roundabout/static/js/form-search.js
@@ -124,12 +124,6 @@ function create_row(card_idx, model, row_index,row_data=null,field_options=null)
init_nega = row_data['nega']
init_multi = row_data['multi']
}
- else{
- if (model === 'Inventory'){ init_fields = [] }
- else if (model === 'Part'){ init_fields = [] }
- else if (model === 'Build'){ init_fields = [] }
- else if (model === 'Assembly'){ init_fields = [] }
- }
const row_id = `qfield-row_c${card_idx}_r${row_index}`
let row = `
- {{ label_builds_app_singular }} Number: {{ build.build_number }}
+
Print Barcode
@@ -187,58 +190,65 @@
{{ build }}
- {{ label_assemblies_app_singular }} Template: {{ build.assembly }}
-
- {{ label_assemblies_app_singular }} Revision: {{ build.assembly_revision.revision_code }}
-
- {{ label_assemblies_app_singular }} Type: {{ build.assembly.assembly_type }}
+
+
+
+ {{ label_builds_app_singular }} Number |
+ {{ build.build_number }} |
+
+
+ {{ label_assemblies_app_singular }} Template |
+
+
+ {{ build.assembly }} | Revision: {{ build.assembly_revision.revision_code }}
+
+
+
+
+ |
+
+
+ {{ label_assemblies_app_singular }} Type |
+ {{ build.assembly.assembly_type }} |
+
+ {% if build.build_notes %}
+
+ {{ label_builds_app_singular }} Notes |
+ {{ build.build_notes }} |
+
+ {% endif %}
+
+ Current Location |
+
+
+ {{ build.location }}
+
+
+
+
+ |
+
+
+ Total Time in Field |
+ {{ build.time_at_sea|time_at_sea_display }} |
+
+
+
- {{ label_builds_app_singular }} Notes: {{ build.build_notes }}
+ {% if percent_complete %}
+ Percent Built
-
-
-
-
{{ label_builds_app_singular }} Status
-
- {% if build.flag %}
-
- {% endif %}
-
-
Current Location: {{ build.location }}
-
-
Total Time in Field: {{ build.time_at_sea|time_at_sea_display }}
-
- {% if percent_complete %}
-
Percent Built
-
-
- {% if percent_complete == 0 %} {{ percent_complete }}% {% endif %}
-
- {{ percent_complete }}%
-
-
- {% endif %}
-
-
-
-
-
-
-
-
+ {% endif %}
{% if build.is_deployed %}
-
+
Current {{ label_deployments_app_singular }} Data
- {{ label_deployments_app_singular }} Number: {{ current_deployment.deployment_number }}
- Final {{ label_deployments_app_singular }} Location: {{ current_deployment.deployed_location }}
- {% if current_deployment.cruise_deployed %}
- Cruise Deployed On:
- {{ current_deployment.cruise_deployed }}
-
- {% endif %}
- {% if current_deployment.cruise_recovered %}
- Cruise Recovered On:
- {{ current_deployment.cruise_recovered }}
-
- {% endif %}
-
- Current {{ label_deployments_app_singular }} Time in Field: {{ current_deployment.deployment_time_in_field|time_at_sea_display }}
-
{{ label_deployments_app_singular }} Status
-
{{ current_deployment.deployment_progress_bar|get_item:'status_label' }}
-
- {% if current_deployment.current_status != 'startdeployment' and current_deployment.current_status != 'deploymentburnin' %}
- {{ label_deployments_app_singular }} to Field Date: {{ current_deployment.deployment_to_field_date|date:"n/j/y H:i" }}
- Latitude: {{ current_deployment.latitude}}
- Longitude: {{ current_deployment.longitude }}
- Depth: {{ current_deployment.depth }} m
- {% endif %}
+
+
+
+ {{ label_deployments_app_singular }} Number |
+ {{ current_deployment.deployment_number }} |
+
+ {% if current_deployment.deployed_location %}
+
+ Final {{ label_deployments_app_singular }} Location |
+
+
+ {{ current_deployment.deployed_location }}
+
+
+
+
+ |
+
+ {% endif %}
+ {% if current_deployment.cruise_deployed %}
+
+ Cruise Deployed On |
+
+
+ {{ current_deployment.cruise_deployed }}
+
+
+
+
+ |
+
+ {% endif %}
+ {% if current_deployment.cruise_recovered %}
+
+ Cruise Recovered On |
+
+
+ {{ current_deployment.cruise_recovered }}
+
+
+
+
+ |
+
+ {% endif %}
+
+ Current {{ label_deployments_app_singular }} Time in Field |
+ {{ current_deployment.deployment_time_in_field|time_at_sea_display }} |
+
+ {% if current_deployment.current_status != 'startdeployment' and current_deployment.current_status != 'deploymentburnin' %}
+
+ {{ label_deployments_app_singular }} to Field Date |
+ {{ current_deployment.deployment_to_field_date|date:"n/j/Y H:i" }} |
+
+ {% if current_deployment.latitude %}
+
+ Latitude |
+ {{ current_deployment.latitude }} ° N |
+
+ {% endif %}
+ {% if current_deployment.longitude %}
+
+ Longitude |
+ {{ current_deployment.longitude }} ° E |
+
+ {% endif %}
+ {% if current_deployment.depth %}
+
+ Depth |
+ {{ current_deployment.depth }} m |
+
+ {% endif %}
+ {% endif %}
+
+
+
{% endif %}
diff --git a/roundabout/templates/builds/deployment_detail.html b/roundabout/templates/builds/deployment_detail.html
index 21afc8cca..37ccda1f1 100644
--- a/roundabout/templates/builds/deployment_detail.html
+++ b/roundabout/templates/builds/deployment_detail.html
@@ -21,7 +21,7 @@
{% load common_tags %}
- {{ deployment.build.build_number }} - {{ label_deployments_app_singular }}: {{ deployment }}
+ {{ deployment.build }} - {{ label_deployments_app_singular }}: {{ deployment }}
@@ -35,47 +35,97 @@
-
-
Build:
- {{ deployment.build }}
-
-
-
{{ label_deployments_app_singular }} Location: {{ deployment.deployed_location }}
-
- {% if deployment.deployment_to_field_date %}
-
{{ label_deployments_app_singular }} To Field Date: {{ deployment.deployment_to_field_date }}
- {% endif %}
-
- {% if deployment.deployment_recovery_date %}
-
{{ label_deployments_app_singular }} Recovery Date: {{ deployment.deployment_recovery_date }}
- {% endif %}
- {% if deployment.cruise_deployed %}
-
Cruise Deployed On:
- {{ deployment.cruise_deployed }}
-
- {% endif %}
-
- {% if deployment.cruise_recovered %}
-
Cruise Recovered On:
- {{ deployment.cruise_recovered }}
-
- {% endif %}
-
- {% if deployment.deployment_time_in_field %}
-
Deployment Time in Field: {{ deployment.deployment_time_in_field|time_at_sea_display }}
- {% endif %}
-
- {% if deployment.current_status != 'startdeployment' and deployment.current_status != 'deploymentburnin' %}
-
Latitude: {{ deployment.latitude}}
-
Longitude: {{ deployment.longitude }}
-
Depth: {{ deployment.depth }} m
- {% endif %}
+
+
+
+ {{ label_builds_app_singular }} |
+
+ {{ deployment.build }}
+
+
+
+
+ |
+
+ {% if deployment.deployed_location %}
+
+ {{ label_deployments_app_singular }} Location |
+
+ {{ deployment.deployed_location }}
+
+
+
+ |
+
+ {% endif %}
+ {% if deployment.deployment_to_field_date %}
+
+ {{ label_deployments_app_singular }} to Field Date |
+ {{ deployment.deployment_to_field_date|date:"n/j/Y H:i" }} |
+
+ {% endif %}
+ {% if deployment.deployment_recovery_date %}
+
+ {{ label_deployments_app_singular }} Recovery Date |
+ {{ deployment.deployment_recovery_date|date:"n/j/Y H:i" }} |
+
+ {% endif %}
+ {% if deployment.cruise_deployed %}
+
+ Cruise Deployed On |
+
+ {{ deployment.cruise_deployed }}
+
+
+
+ |
+
+ {% endif %}
+ {% if deployment.cruise_recovered %}
+
+ Cruise Recovered On |
+
+ {{ deployment.cruise_recovered }}
+
+
+
+ |
+
+ {% endif %}
+ {% if deployment.deployment_time_in_field %}
+
+ {{ label_deployments_app_singular }} Time in Field |
+ {{ deployment.deployment_time_in_field|time_at_sea_display }} |
+
+ {% endif %}
+ {% if deployment.current_status != 'startdeployment' and deployment.current_status != 'deploymentburnin' %}
+ {% if deployment.latitude %}
+
+ Latitude |
+ {{ deployment.latitude }} ° N |
+
+ {% endif %}
+ {% if deployment.longitude %}
+
+ Longitude |
+ {{ deployment.longitude }} ° E |
+
+ {% endif %}
+ {% if deployment.depth %}
+
+ Depth |
+ {{ deployment.depth }} m |
+
+ {% endif %}
+ {% endif %}
+
+
diff --git a/roundabout/templates/calibrations/cal_name_detail.html b/roundabout/templates/calibrations/cal_name_detail.html
index b2236430e..380285286 100644
--- a/roundabout/templates/calibrations/cal_name_detail.html
+++ b/roundabout/templates/calibrations/cal_name_detail.html
@@ -89,6 +89,7 @@ Calibrations