diff --git a/.circleci/config.yml b/.circleci/config.yml
index 6e96cb650..1fb0611dd 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -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
diff --git a/.envs.example/.local/.django b/.envs.example/.local/.django
index b05d29f77..a21308588 100644
--- a/.envs.example/.local/.django
+++ b/.envs.example/.local/.django
@@ -19,6 +19,9 @@ DJANGO_SU_NAME=admin
DJANGO_SU_EMAIL=admin@example.com
DJANGO_SU_PASSWORD=admin
+# Site Domain
+RDB_SITE_URL=https://yourdomain.com
+
# Settings for RDB serial number generation functions
# ------------------------------------------------------------------------------
# Default pattern - "1, 2, 3, ... etc."
diff --git a/.envs.example/.production/.django b/.envs.example/.production/.django
index c22d1c8fb..fd2bc47af 100644
--- a/.envs.example/.production/.django
+++ b/.envs.example/.production/.django
@@ -44,6 +44,9 @@ DJANGO_SU_NAME=admin
DJANGO_SU_EMAIL=admin@example.com
DJANGO_SU_PASSWORD=admin
+# Site Domain
+RDB_SITE_URL=https://yourdomain.com
+
# Settings for RDB serial number generation functions
# ------------------------------------------------------------------------------
# Default pattern - "1, 2, 3, ... etc."
diff --git a/compose/local/tests/Dockerfile b/compose/local/tests/Dockerfile
index 3c12a49d1..86052349a 100644
--- a/compose/local/tests/Dockerfile
+++ b/compose/local/tests/Dockerfile
@@ -1,5 +1,6 @@
FROM ubuntu:18.04
RUN apt-get update && apt-get install -y --no-install-recommends apt-utils
+RUN apt-get update
RUN apt-get install curl -y && apt-get install npm -y
# Tools for Circleci debugging
RUN apt-get install iputils-ping -y && apt-get install net-tools -y
diff --git a/config/settings/base.py b/config/settings/base.py
index 233ef1753..2d3c9eb34 100644
--- a/config/settings/base.py
+++ b/config/settings/base.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
@@ -98,6 +98,10 @@
'import_export', # simple model import/export using admin interface
'django_tables2', # interactive tables views
'django_tables2_column_shifter', # show/hide tables2 columns
+ 'django_filters', #filters for API searching
+ 'dynamic_rest', # dynamic functionality for API
+ 'rest_framework.authtoken',
+ 'rest_flex_fields',
]
LOCAL_APPS = [
'roundabout.users.apps.UsersAppConfig',
@@ -114,6 +118,7 @@
'roundabout.cruises',
'roundabout.calibrations',
'roundabout.configs_constants',
+ 'roundabout.field_instances',
'roundabout.search',
'roundabout.exports',
]
@@ -301,12 +306,20 @@
# When you enable API versioning, the request.version attribute will contain a string
# that corresponds to the version requested in the incoming client request.
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
+ 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
'DEFAULT_AUTHENTICATION_CLASSES': (
- 'rest_framework_simplejwt.authentication.JWTAuthentication',
+ 'rest_framework.authentication.SessionAuthentication',
+ 'rest_framework.authentication.TokenAuthentication',
),
- #'DEFAULT_PERMISSION_CLASSES': [
- # 'rest_framework.permissions.IsAuthenticated',
- #]
+ 'DEFAULT_RENDERER_CLASSES': (
+ 'rest_framework.renderers.JSONRenderer',
+ 'roundabout.core.api.renderers.BrowsableAPIRendererWithoutForms',
+ ),
+ 'DEFAULT_PAGINATION_CLASS': 'drf_link_header_pagination.LinkHeaderPagination',
+ 'PAGE_SIZE': 30,
+ 'DEFAULT_PERMISSION_CLASSES': [
+ 'rest_framework.permissions.IsAuthenticated',
+ ]
}
# Summernote CONFIGURATION
diff --git a/config/urls.py b/config/urls.py
index af85afab1..a6207c7d2 100644
--- a/config/urls.py
+++ b/config/urls.py
@@ -26,10 +26,6 @@
from django.views.generic import TemplateView
from django.views import defaults as default_views
-from rest_framework_simplejwt.views import (
- TokenObtainPairView,
- TokenRefreshView,
-)
urlpatterns = [
path("", TemplateView.as_view(template_name="pages/home.html"), name="home"),
@@ -60,14 +56,9 @@
path('export/', include('roundabout.exports.urls', namespace='export')),
path('calibrations/', include('roundabout.calibrations.urls', namespace='calibrations')),
path('configs_constants/', include('roundabout.configs_constants.urls', namespace='configs_constants')),
+ path('field_instances/', include('roundabout.field_instances.urls', namespace='field_instances')),
# API urls
- path('api/v1/', include('roundabout.inventory.api.urls')),
- path('api/v1/', include('roundabout.locations.api.urls')),
- path('api/v1/', include('roundabout.parts.api.urls')),
- path('api/v1/', include('roundabout.assemblies.api.urls')),
- # API JWT token paths
- path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
- path('api/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
+ path('api/v1/', include('roundabout.core.api.urls', namespace='api_v1')),
#Summernote WYSIWYG
path('summernote/', include('django_summernote.urls')),
] + static(
diff --git a/requirements/base.txt b/requirements/base.txt
index d143f4130..d21fdb013 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -17,6 +17,10 @@ django-redis==4.10.0 # https://github.com/niwinz/django-redis
djangorestframework==3.10.3 # https://github.com/encode/django-rest-framework
djangorestframework_simplejwt==4.3.0
coreapi==2.3.3 # https://github.com/core-api/python-client
+django-filter==2.2.0
+dynamic-rest==1.9.6
+djangorestframework-link-header-pagination==0.1.1
+drf-flex-fields==0.8.6
# Your custom requirements go here
django-mptt==0.9.1
diff --git a/roundabout/admintools/admin.py b/roundabout/admintools/admin.py
index dbe41e04c..ef22e022b 100644
--- a/roundabout/admintools/admin.py
+++ b/roundabout/admintools/admin.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
diff --git a/roundabout/admintools/forms.py b/roundabout/admintools/forms.py
index 7d81781e4..d0dbc56e1 100644
--- a/roundabout/admintools/forms.py
+++ b/roundabout/admintools/forms.py
@@ -56,80 +56,88 @@ class ImportCalibrationForm(forms.Form):
def clean_cal_csv(self):
cal_files = self.files.getlist('cal_csv')
- for cal_csv in cal_files:
- cal_csv.seek(0)
- ext = cal_csv.name[-3:]
+ csv_files = []
+ ext_files = []
+ for file in cal_files:
+ ext = file.name[-3:]
if ext == 'ext':
- continue
+ ext_files.append(file)
if ext == 'csv':
- reader = csv.DictReader(io.StringIO(cal_csv.read().decode('utf-8')))
- headers = reader.fieldnames
- try:
- inv_serial = cal_csv.name.split('__')[0]
- inventory_item = Inventory.objects.get(serial_number=inv_serial)
- except:
- raise ValidationError(
- _('%(value)s: Unable to find Inventory item with this Serial Number'),
- params={'value': inv_serial},
- )
- try:
- cal_date_string = cal_csv.name.split('__')[1][:8]
- cal_date_date = datetime.datetime.strptime(cal_date_string, "%Y%m%d").date()
- except:
- raise ValidationError(
- _('%(value)s: Unable to parse Calibration Date from Filename'),
- params={'value': cal_date_string},
- )
- for idx, row in enumerate(reader):
- row_data = row.items()
- for key, value in row_data:
- 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()
- )
- except:
- raise ValidationError(
- _('Row %(row)s, %(value)s: Unable to find Calibration item with this Name'),
- params={'value': calibration_name, 'row': idx},
- )
- elif key == 'value':
- valset_keys = {'cal_dec_places': inventory_item.part.cal_dec_places}
- mock_valset_instance = SimpleNamespace(**valset_keys)
- try:
- raw_valset = str(value)
- if '[' in raw_valset:
- raw_valset = raw_valset[1:-1]
- if 'SheetRef' in raw_valset:
- for file in cal_files:
- file.seek(0)
- file_extension = file.name[-3:]
- if file_extension == 'ext':
- cal_ext_split = file.name.split('__')
- inv_ext_serial = cal_ext_split[0]
- cal_ext_date = cal_ext_split[1]
- cal_ext_name = cal_ext_split[2][:-4]
- if (inv_ext_serial == inv_serial) and (cal_ext_date == cal_date_string) and (cal_ext_name == calibration_name):
- reader = io.StringIO(file.read().decode('utf-8'))
- contents = reader.getvalue()
- raw_valset = contents
- except:
- raise ValidationError(
- _('Row %(row)s: Unable to parse Calibration Coefficient value(s)'),
- params={'row': idx},
- )
- validate_coeff_vals(mock_valset_instance, cal_name_item.value_set_type, raw_valset)
- elif key == 'notes':
+ csv_files.append(file)
+ for cal_csv in 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
+ try:
+ inv_serial = cal_csv.name.split('__')[0]
+ inventory_item = Inventory.objects.get(serial_number=inv_serial)
+ except:
+ raise ValidationError(
+ _('File: %(filename)s, %(value)s: Unable to find Inventory item with this Serial Number'),
+ params={'value': inv_serial, 'filename': cal_csv.name},
+ )
+ try:
+ cal_date_string = cal_csv.name.split('__')[1][:8]
+ cal_date_date = datetime.datetime.strptime(cal_date_string, "%Y%m%d").date()
+ except:
+ raise ValidationError(
+ _('File: %(filename)s, %(value)s: Unable to parse Calibration Date from Filename'),
+ params={'value': cal_date_string, 'filename': cal_csv.name},
+ )
+ for idx, row in enumerate(reader):
+ row_data = row.items()
+ for key, value in row_data:
+ 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()
+ )
+ except:
+ raise ValidationError(
+ _('File: %(filename)s, Calibration Name: %(value)s, Row %(row)s: Unable to find Calibration item with this Name'),
+ params={'value': calibration_name, 'row': idx, 'filename': cal_csv.name},
+ )
+ elif key == 'value':
+ valset_keys = {'cal_dec_places': inventory_item.part.cal_dec_places}
+ mock_valset_instance = SimpleNamespace(**valset_keys)
+ try:
+ raw_valset = str(value)
+ except:
+ raise ValidationError(
+ _('File: %(filename)s, Calibration Name: %(value)s, Row %(row)s, %(value)s: Unable to parse Calibration Coefficient value(s)'),
+ params={'value': calibration_name,'row': idx, 'filename': cal_csv.name},
+ )
+ if '[' in raw_valset:
+ raw_valset = raw_valset[1:-1]
+ if 'SheetRef' in raw_valset:
+ ext_finder_filename = "__".join((cal_csv_filename,calibration_name))
try:
- notes = value.strip()
+ ref_file = [file for file in ext_files if ext_finder_filename in file.name][0]
+ assert len(ref_file) > 0
except:
raise ValidationError(
- _('Row %(row)s: Unable to parse Calibration Coefficient note(s)'),
- params={'row': idx},
+ _('File: %(filename)s, Calibration Name: %(value)s, Row %(row)s: No associated .ext file selected'),
+ params={'value': calibration_name, 'row': idx, 'filename': cal_csv.name},
)
- return cal_csv
+ ref_file.seek(0)
+ reader = io.StringIO(ref_file.read().decode('utf-8'))
+ contents = reader.getvalue()
+ raw_valset = contents
+ validate_coeff_vals(mock_valset_instance, cal_name_item.value_set_type, raw_valset, filename = ref_file.name, cal_name = calibration_name)
+ else:
+ validate_coeff_vals(mock_valset_instance, cal_name_item.value_set_type, raw_valset, filename = cal_csv.name, cal_name = calibration_name)
+ elif key == 'notes':
+ try:
+ notes = value.strip()
+ except:
+ raise ValidationError(
+ _('File: %(filename)s, Calibration Name: %(value)s, Row %(row)s: Unable to parse Calibration Coefficient note(s)'),
+ params={'value': calibration_name, 'row': idx, 'filename': cal_csv.name},
+ )
+ return cal_files
class PrinterForm(forms.ModelForm):
diff --git a/roundabout/admintools/models.py b/roundabout/admintools/models.py
index 2dfac189b..077c20df5 100644
--- a/roundabout/admintools/models.py
+++ b/roundabout/admintools/models.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
@@ -20,14 +20,16 @@
"""
from django.db import models
+from django.utils import timezone
from mptt.models import MPTTModel, TreeForeignKey
from django.contrib.postgres.fields import JSONField
from roundabout.assemblies.models import AssemblyType
from roundabout.parts.models import Part
+from roundabout.users.models import User
+from roundabout.cruises.models import Cruise
# AdminTool models
-
class Printer(models.Model):
PRINTER_TYPES = (
('Brady', 'Brady'),
diff --git a/roundabout/admintools/urls.py b/roundabout/admintools/urls.py
index 5ca548f47..101622e1e 100644
--- a/roundabout/admintools/urls.py
+++ b/roundabout/admintools/urls.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
diff --git a/roundabout/admintools/views.py b/roundabout/admintools/views.py
index f1d4aa639..c6f0e5f48 100644
--- a/roundabout/admintools/views.py
+++ b/roundabout/admintools/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
@@ -30,7 +30,6 @@
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse, reverse_lazy
-from django.db import IntegrityError
from django.views.generic import View, DetailView, ListView, RedirectView, UpdateView, CreateView, DeleteView, TemplateView, FormView
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.core.exceptions import ValidationError
@@ -41,9 +40,9 @@
from .models import *
from roundabout.userdefinedfields.models import FieldValue, Field
from roundabout.inventory.models import Inventory, Action
-from roundabout.parts.models import Part, Revision
+from roundabout.parts.models import Part, Revision, Documentation, PartType
from roundabout.locations.models import Location
-from roundabout.assemblies.models import AssemblyType, Assembly, AssemblyPart
+from roundabout.assemblies.models import AssemblyType, Assembly, AssemblyPart, AssemblyRevision
from roundabout.assemblies.views import _make_tree_copy
from roundabout.inventory.utils import _create_action_history
from roundabout.calibrations.models import CoefficientName, CoefficientValueSet, CalibrationEvent
@@ -56,76 +55,75 @@ def trigger_error(request):
division_by_zero = 1 / 0
-# CSV File Uploader for GitHub Calibration Coefficients
+# CSV File Uploader for GitHub Calibration Coefficients
class ImportCalibrationsUploadView(LoginRequiredMixin, FormView):
form_class = ImportCalibrationForm
template_name = 'admintools/import_calibrations_upload_form.html'
def form_valid(self, form):
cal_files = self.request.FILES.getlist('cal_csv')
- for cal_csv in cal_files:
- ext = cal_csv.name[-3:]
+ csv_files = []
+ ext_files = []
+ for file in cal_files:
+ ext = file.name[-3:]
if ext == 'ext':
- continue
+ ext_files.append(file)
if ext == 'csv':
- cal_csv.seek(0)
- reader = csv.DictReader(io.StringIO(cal_csv.read().decode('utf-8')))
- headers = reader.fieldnames
- coeff_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(
- 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 == '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()
- )
- elif key == 'value':
- valset_keys = {'cal_dec_places': inventory_item.part.cal_dec_places}
- mock_valset_instance = SimpleNamespace(**valset_keys)
- raw_valset = str(value)
- if '[' in raw_valset:
- raw_valset = raw_valset[1:-1]
- if 'SheetRef' in raw_valset:
- for file in cal_files:
- file.seek(0)
- file_extension = file.name[-3:]
- if file_extension == 'ext':
- cal_ext_split = file.name.split('__')
- inv_ext_serial = cal_ext_split[0]
- cal_ext_date = cal_ext_split[1]
- cal_ext_name = cal_ext_split[2][:-4]
- if (inv_ext_serial == inv_serial) and (cal_ext_date == cal_date_string) and (cal_ext_name == calibration_name):
- reader = io.StringIO(file.read().decode('utf-8'))
- contents = reader.getvalue()
- raw_valset = contents
- validate_coeff_vals(mock_valset_instance, cal_name_item.value_set_type, raw_valset)
- 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 form.cleaned_data['user_draft'].exists():
- draft_users = form.cleaned_data['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, self.request.user)
+ csv_files.append(file)
+ for cal_csv in 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 = []
+ 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(
+ 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 == '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()
+ )
+ elif key == 'value':
+ valset_keys = {'cal_dec_places': inventory_item.part.cal_dec_places}
+ mock_valset_instance = SimpleNamespace(**valset_keys)
+ raw_valset = str(value)
+ if '[' in raw_valset:
+ raw_valset = raw_valset[1:-1]
+ if 'SheetRef' in raw_valset:
+ ext_finder_filename = "__".join((cal_csv_filename,calibration_name))
+ ref_file = [file for file in ext_files if ext_finder_filename in file.name][0]
+ ref_file.seek(0)
+ reader = io.StringIO(ref_file.read().decode('utf-8'))
+ contents = reader.getvalue()
+ raw_valset = contents
+ validate_coeff_vals(mock_valset_instance, cal_name_item.value_set_type, raw_valset)
+ 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 form.cleaned_data['user_draft'].exists():
+ draft_users = form.cleaned_data['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, self.request.user)
return super(ImportCalibrationsUploadView, self).form_valid(form)
def get_success_url(self):
@@ -408,84 +406,121 @@ class ImportInventoryUploadSuccessView(TemplateView):
# Assembly Template import tool
# Import an existing Assembly template from a separate RDB instance
+# Makes a copy of the Assembly Revisiontree starting at "root_part",
+# move to new Revision, reparenting it to "parent"
+def _api_import_assembly_parts_tree(headers, root_part_url, new_revision, parent=None):
+ params = {'expand': 'part'}
+ assembly_part_request = requests.get(root_part_url, params=params, headers=headers, verify=False)
+ assembly_part_data = assembly_part_request.json()
+ # Need to validate that the Part template exists before creating AssemblyPart
+ try:
+ part_obj = Part.objects.get(part_number=assembly_part_data['part']['part_number'])
+ except Part.DoesNotExist:
+ params = {'expand': 'part_type,revisions.documentation'}
+ part_request = requests.get(assembly_part_data['part']['url'], params=params, headers=headers, verify=False)
+ part_data = part_request.json()
+ print(part_data)
+
+ try:
+ part_type = PartType.objects.get(name=part_data['part_type']['name'])
+ except PartType.DoesNotExist:
+ # No matching AssemblyType, add it from the API request data
+ part_type = PartType.objects.create(name=part_data['part_type']['name'])
+
+ part_obj = Part.objects.create(
+ name = part_data['name'],
+ friendly_name = part_data['friendly_name'],
+ part_type = part_type,
+ part_number = part_data['part_number'],
+ unit_cost = part_data['unit_cost'],
+ refurbishment_cost = part_data['refurbishment_cost'],
+ note = part_data['note'],
+ cal_dec_places = part_data['cal_dec_places'],
+ )
+ # Create all Revisions objects for this Part
+ for revision in part_data['revisions']:
+ revision_obj = Revision.objects.create(
+ revision_code = revision['revision_code'],
+ unit_cost = revision['unit_cost'],
+ refurbishment_cost = revision['refurbishment_cost'],
+ created_at = revision['created_at'],
+ part = part_obj,
+ )
+ # Create all Documentation objects for this Revision
+ for doc in revision['documentation']:
+ doc_obj = Documentation.objects.create(
+ name = doc['name'],
+ doc_type = doc['doc_type'],
+ doc_link = doc['doc_link'],
+ revision = revision_obj,
+ )
+ # Now create the Assembly Part
+ assembly_part_obj = AssemblyPart.objects.create(
+ assembly_revision = new_revision,
+ part = part_obj,
+ parent=parent,
+ note = assembly_part_data['note'],
+ order = assembly_part_data['order']
+ )
+ # Loop through the tree
+ for child_url in assembly_part_data['children']:
+ _api_import_assembly_parts_tree(headers, child_url, new_revision, assembly_part_obj)
+
+ return True
+
# View to make API request to a separate RDB instance and copy an Assembly Template
class ImportAssemblyAPIRequestCopyView(LoginRequiredMixin, PermissionRequiredMixin, View):
permission_required = 'assemblies.add_assembly'
def get(self, request, *args, **kwargs):
+ import_url = request.GET.get('import_url')
+ api_token = request.GET.get('api_token')
+
+ if not import_url:
+ return HttpResponse("No import_url query paramater data")
+
+ if not api_token:
+ return HttpResponse("No api_token query paramater data")
+ #api_token = '92e4efc1731d7ed2c31bf76c8d08ab2a34d3ce6d'
+ headers = {
+ 'Authorization': 'Token ' + api_token,
+ }
+ params = {'expand': 'assembly_type,assembly_revisions'}
# Get the Assembly data from RDB API
- request_url = 'https://rdb-demo.whoi.edu/api/v1/assemblies/13/'
- assembly_request = requests.get(request_url, verify=False)
+ #import_url = 'https://rdb-demo.whoi.edu/api/v1/assembly-templates/assemblies/8/'
+ assembly_request = requests.get(import_url, params=params, headers=headers, verify=False)
new_assembly = assembly_request.json()
# Get or create new parent Temp Assembly
- temp_assembly_obj, created = TempImportAssembly.objects.get_or_create(name=new_assembly['name'],
- assembly_number=new_assembly['assembly_number'],
- description=new_assembly['description'],)
- # If already exists, reset all the related items
- error_count = 0
- error_parts = []
- if not created:
- temp_assembly_obj.temp_assembly_parts.all().delete()
-
+ assembly_obj, created = Assembly.objects.get_or_create(name=new_assembly['name'],
+ assembly_number=new_assembly['assembly_number'],
+ description=new_assembly['description'],)
+ print(assembly_obj)
try:
assembly_type = AssemblyType.objects.get(name=new_assembly['assembly_type']['name'])
- import_error = False
except AssemblyType.DoesNotExist:
- assembly_type = None
- import_error = True
- import_error_msg = 'Assembly Type does not exist in this RDB. Please add it, and try again.'
-
- if not import_error:
- # add Assembly Type to the parent object
- temp_assembly_obj.assembly_type = assembly_type
- temp_assembly_obj.save()
-
- # import all Assembly Parts to temp table
- for assembly_part in new_assembly['assembly_parts']:
- # Need to validate that the Part template exists
- try:
- part = Part.objects.get(part_number=assembly_part['part']['part_number'])
- except Part.DoesNotExist:
- part = None
- import_error = True
- error_count += 1
- error_parts.append(assembly_part['part']['part_number'])
- import_error_msg = 'Part Number does not exist in this RDB. Please add Part Template, and try again.'
-
- if not import_error:
- temp_assembly_part_obj = TempImportAssemblyPart(assembly=temp_assembly_obj,
- part=part,
- previous_id=assembly_part['id'],
- previous_parent=assembly_part['parent'],
- note=assembly_part['note'],
- order=assembly_part['order'])
- temp_assembly_part_obj.save()
- print(temp_assembly_part_obj)
-
- # run through the temp Assembly Parts again to set the correct Parent structure for MPTT
- for temp_assembly_part in temp_assembly_obj.temp_assembly_parts.all():
- # Check if there's a previous_parent, if so we need to find the correct new object
- if temp_assembly_part.previous_parent:
- parent_obj = TempImportAssemblyPart.objects.get(previous_id=temp_assembly_part.previous_parent)
- temp_assembly_part.parent = parent_obj
- temp_assembly_part.save()
-
- # Rebuild the TempImportAssemblyPart MPTT tree to ensure correct structure for copying
- TempImportAssemblyPart._tree_manager.rebuild()
-
- # copy the Temp Assembly to real destination table
- assembly_obj = Assembly(name=temp_assembly_obj.name,
- assembly_type=temp_assembly_obj.assembly_type,
- assembly_number=temp_assembly_obj.assembly_number,
- description=temp_assembly_obj.description)
- assembly_obj.save()
-
- for ap in temp_assembly_obj.temp_assembly_parts.all():
- if ap.is_root_node():
- _make_tree_copy(ap, assembly_obj, ap.parent)
-
- return HttpResponse('
New Assembly Template Imported! - %s
Errors count: %s
%s
' % (import_error, error_count, error_parts))
+ # No matching AssemblyType, add it from the API request data
+ assembly_type = AssemblyType.objects.create(name=new_assembly['assembly_type']['name'])
+ print(assembly_type)
+ assembly_obj.assembly_type = assembly_type
+ assembly_obj.save()
+
+ # Create all Revisions
+ for rev in new_assembly['assembly_revisions']:
+ assembly_revision_obj = AssemblyRevision.objects.create(
+ revision_code = rev['revision_code'],
+ revision_note = rev['revision_note'],
+ created_at = rev['created_at'],
+ assembly = assembly_obj,
+ )
+ print(assembly_revision_obj)
+
+ for root_url in rev['assembly_parts_roots']:
+ tree_created = _api_import_assembly_parts_tree(headers, root_url, assembly_revision_obj)
+
+ print(tree_created)
+ #AssemblyPart._tree_manager.rebuild()
+ return HttpResponse('New Assembly Template Imported! - %s
' % (assembly_obj))
# Printer functionality
@@ -507,6 +542,7 @@ class PrinterCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView)
def get_success_url(self):
return reverse('admintools:printers_home', )
+
class PrinterUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
model = Printer
form_class = PrinterForm
diff --git a/roundabout/assemblies/api/serializers.py b/roundabout/assemblies/api/serializers.py
index ab811e1ca..5b507514a 100644
--- a/roundabout/assemblies/api/serializers.py
+++ b/roundabout/assemblies/api/serializers.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
@@ -20,43 +20,133 @@
"""
from rest_framework import serializers
+from rest_framework.reverse import reverse
+from rest_flex_fields import FlexFieldsModelSerializer
-from ..models import Assembly, AssemblyPart, AssemblyType
+from ..models import Assembly, AssemblyPart, AssemblyType, AssemblyRevision
+from roundabout.parts.models import Part
from roundabout.parts.api.serializers import PartSerializer
+from roundabout.core.api.serializers import RecursiveFieldSerializer
+API_VERSION = 'api_v1'
-class AssemblyPartSerializer(serializers.ModelSerializer):
- part = PartSerializer(read_only=True)
+class AssemblyTypeSerializer(FlexFieldsModelSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name = API_VERSION + ':assembly-templates/assembly-types-detail',
+ lookup_field = 'pk',
+ )
+ assemblies = serializers.HyperlinkedRelatedField(
+ view_name = API_VERSION + ':assembly-templates/assemblies-detail',
+ many = True,
+ read_only = True,
+ lookup_field = 'pk',
+ )
class Meta:
- model = AssemblyPart
- fields = ['id', 'part', 'parent', 'note', 'order' ]
+ model = AssemblyType
+ fields = ['id', 'url', 'name', 'assemblies']
- @staticmethod
- def setup_eager_loading(queryset):
- """ Perform necessary prefetching of data. """
- queryset = queryset.select_related('part')
+ expandable_fields = {
+ 'assemblies': ('roundabout.assemblies.api.serializers.AssemblySerializer', {'many': True})
+ }
- return queryset
+class AssemblySerializer(FlexFieldsModelSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name = API_VERSION + ':assembly-templates/assemblies-detail',
+ lookup_field = 'pk',
+ )
+ assembly_revisions = serializers.HyperlinkedRelatedField(
+ view_name = API_VERSION + ':assembly-templates/assembly-revisions-detail',
+ many = True,
+ read_only = True,
+ lookup_field = 'pk',
+ )
+ assembly_type = serializers.HyperlinkedRelatedField(
+ view_name = API_VERSION + ':assembly-templates/assembly-types-detail',
+ lookup_field = 'pk',
+ queryset = AssemblyType.objects
+ )
-class AssemblyTypeSerializer(serializers.ModelSerializer):
class Meta:
- model = AssemblyType
- fields = ['name']
+ model = Assembly
+ fields = ['id', 'url', 'name', 'assembly_number', 'description', 'assembly_type', 'assembly_revisions' ]
+
+ expandable_fields = {
+ 'assembly_type': 'roundabout.assemblies.api.serializers.AssemblyTypeSerializer',
+ 'assembly_revisions': ('roundabout.assemblies.api.serializers.AssemblyRevisionSerializer', {'many': True})
+ }
-class AssemblySerializer(serializers.ModelSerializer):
- assembly_parts = AssemblyPartSerializer(many=True, read_only=True)
- assembly_type = AssemblyTypeSerializer(read_only=True)
+class AssemblyRevisionSerializer(FlexFieldsModelSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name = API_VERSION + ':assembly-templates/assembly-revisions-detail',
+ lookup_field = 'pk',
+ )
+ assembly_parts_roots = serializers.SerializerMethodField('get_assembly_parts_roots')
+ assembly_parts = serializers.HyperlinkedRelatedField(
+ view_name = API_VERSION + ':assembly-templates/assembly-parts-detail',
+ many = True,
+ read_only = True,
+ lookup_field = 'pk',
+ )
+ assembly = serializers.HyperlinkedRelatedField(
+ view_name = API_VERSION + ':assembly-templates/assemblies-detail',
+ lookup_field = 'pk',
+ queryset = Assembly.objects
+ )
class Meta:
- model = Assembly
- fields = ['id', 'name', 'assembly_number', 'description', 'assembly_type', 'assembly_parts' ]
+ model = AssemblyRevision
+ fields = ['id', 'url', 'revision_code', 'revision_note', 'created_at', 'assembly', 'assembly_parts_roots', 'assembly_parts']
- @staticmethod
- def setup_eager_loading(queryset):
- """ Perform necessary prefetching of data. """
- queryset = queryset.prefetch_related('assembly_parts')
+ expandable_fields = {
+ 'assembly': 'roundabout.assemblies.api.serializers.AssemblySerializer',
+ 'assembly_parts': ('roundabout.assemblies.api.serializers.AssemblyPartSerializer', {'many': True}),
+ }
+
+ def get_assembly_parts_roots(self, obj):
+ # Get all the Root AssemblyParts only
+ assembly_parts = obj.assembly_parts.filter(parent__isnull=True)
+ assembly_parts_list = [reverse(API_VERSION + ':assembly-templates/assembly-parts-detail', kwargs={'pk': assembly_part.id}, request=self.context['request']) for assembly_part in assembly_parts]
+ return assembly_parts_list
+
+
+class AssemblyPartSerializer(FlexFieldsModelSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name = API_VERSION + ':assembly-templates/assembly-parts-detail',
+ lookup_field = 'pk',
+ )
+ parent = serializers.HyperlinkedRelatedField(
+ view_name = API_VERSION + ':assembly-templates/assembly-parts-detail',
+ lookup_field = 'pk',
+ queryset = AssemblyPart.objects,
+ allow_null=True,
+ )
+ children = serializers.HyperlinkedRelatedField(
+ view_name = API_VERSION + ':assembly-templates/assembly-parts-detail',
+ many = True,
+ read_only = True,
+ lookup_field = 'pk',
+ )
+ assembly_revision = serializers.HyperlinkedRelatedField(
+ view_name = API_VERSION + ':assembly-templates/assembly-revisions-detail',
+ lookup_field = 'pk',
+ queryset = AssemblyRevision.objects,
+ )
+ part = serializers.HyperlinkedRelatedField(
+ view_name = API_VERSION + ':part-templates/parts-detail',
+ lookup_field = 'pk',
+ queryset = Part.objects,
+ )
+
+ class Meta:
+ model = AssemblyPart
+ fields = ['id', 'url', 'assembly_revision', 'order', 'part', 'parent', 'children', 'note', ]
- return queryset
+ expandable_fields = {
+ 'part': PartSerializer,
+ 'assembly_revision': 'roundabout.assemblies.api.serializers.AssemblyRevisionSerializer',
+ 'parent': 'roundabout.assemblies.api.serializers.AssemblyPartSerializer',
+ 'children': ('roundabout.assemblies.api.serializers.AssemblyPartSerializer', {'many': True})
+ }
diff --git a/roundabout/assemblies/api/views.py b/roundabout/assemblies/api/views.py
index 2fb3ba5e9..4361810df 100644
--- a/roundabout/assemblies/api/views.py
+++ b/roundabout/assemblies/api/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
@@ -19,18 +19,35 @@
# If not, see .
"""
+from django.db.models import Prefetch
from rest_framework import generics, viewsets, filters
-from ..models import Assembly
-from .serializers import AssemblySerializer, AssemblyPartSerializer
+from rest_framework.permissions import IsAuthenticated
+
+from ..models import Assembly, AssemblyRevision, AssemblyPart, AssemblyType
+from .serializers import AssemblySerializer, AssemblyRevisionSerializer, AssemblyPartSerializer, AssemblyTypeSerializer
class AssemblyViewSet(viewsets.ModelViewSet):
serializer_class = AssemblySerializer
- search_fields = ['name']
- filter_backends = (filters.SearchFilter,)
-
- def get_queryset(self):
- queryset = Assembly.objects.all()
- # Set up eager loading to avoid N+1 selects
- queryset = self.get_serializer_class().setup_eager_loading(queryset)
- return queryset
+ permission_classes = (IsAuthenticated,)
+ queryset = Assembly.objects.all()
+
+
+class AssemblyTypeViewSet(viewsets.ModelViewSet):
+ serializer_class = AssemblyTypeSerializer
+ permission_classes = (IsAuthenticated,)
+ queryset = AssemblyType.objects.all()
+
+
+class AssemblyRevisionViewSet(viewsets.ModelViewSet):
+ serializer_class = AssemblyRevisionSerializer
+ permission_classes = (IsAuthenticated,)
+ queryset = AssemblyRevision.objects.all()
+ queryset = AssemblyRevision.objects.prefetch_related(Prefetch('assembly_parts',
+ queryset=AssemblyPart.objects.order_by('-parent_id')))
+
+
+class AssemblyPartViewSet(viewsets.ModelViewSet):
+ serializer_class = AssemblyPartSerializer
+ permission_classes = (IsAuthenticated,)
+ queryset = AssemblyPart.objects.all()
diff --git a/roundabout/assemblies/models.py b/roundabout/assemblies/models.py
index 518dca95d..60b154b3d 100644
--- a/roundabout/assemblies/models.py
+++ b/roundabout/assemblies/models.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
diff --git a/roundabout/calibrations/api/serializers.py b/roundabout/calibrations/api/serializers.py
new file mode 100644
index 000000000..71e173db1
--- /dev/null
+++ b/roundabout/calibrations/api/serializers.py
@@ -0,0 +1,117 @@
+"""
+# 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 rest_framework import serializers
+from rest_flex_fields import FlexFieldsModelSerializer
+
+from ..models import CalibrationEvent, CoefficientValueSet, CoefficientName, CoefficientNameEvent, CoefficientValue
+from roundabout.parts.api.serializers import PartSerializer
+
+class CalibrationEventSerializer(FlexFieldsModelSerializer):
+ class Meta:
+ model = CalibrationEvent
+ fields = [
+ 'id',
+ 'created_at',
+ 'updated_at',
+ 'calibration_date',
+ 'user_draft',
+ 'user_approver',
+ 'inventory',
+ 'deployment',
+ 'approved',
+ 'detail',
+ 'coefficient_value_sets'
+ ]
+
+ expandable_fields = {
+ 'coefficient_value_sets': ('roundabout.calibrations.api.serializers.CoefficientValueSetSerializer', {'many': True})
+ }
+
+
+class CoefficientNameEventSerializer(FlexFieldsModelSerializer):
+ class Meta:
+ model = CoefficientNameEvent
+ fields = [
+ 'id',
+ 'created_at',
+ 'updated_at',
+ 'user_draft',
+ 'user_approver',
+ 'part',
+ 'approved',
+ 'detail',
+ 'coefficient_names'
+ ]
+
+ expandable_fields = {
+ 'part': PartSerializer,
+ 'coefficient_names': ('roundabout.calibrations.api.serializers.CoefficientNameSerializer', {'many': True})
+ }
+
+
+class CoefficientNameSerializer(FlexFieldsModelSerializer):
+ class Meta:
+ model = CoefficientName
+ fields = [
+ 'id',
+ 'calibration_name',
+ 'value_set_type',
+ 'sigfig_override',
+ 'created_at',
+ 'part',
+ 'coeff_name_event',
+ ]
+
+
+class CoefficientValueSetSerializer(FlexFieldsModelSerializer):
+ class Meta:
+ model = CoefficientValueSet
+ fields = [
+ 'id',
+ 'value_set',
+ 'notes',
+ 'created_at',
+ 'coefficient_name',
+ 'calibration_event',
+ 'coefficient_values',
+ ]
+
+ expandable_fields = {
+ 'calibration_event': 'roundabout.calibrations.api.serializers.CalibrationEventSerializer',
+ 'coefficient_name': 'roundabout.calibrations.api.serializers.CoefficientNameSerializer',
+ 'coefficient_values': ('roundabout.calibrations.api.serializers.CoefficientValueSerializer', {'many': True})
+ }
+
+
+class CoefficientValueSerializer(FlexFieldsModelSerializer):
+ class Meta:
+ model = CoefficientValue
+ fields = [
+ 'id',
+ 'value',
+ 'original_value',
+ 'notation_format',
+ 'sigfig',
+ 'row',
+ 'created_at',
+ 'coeff_value_set',
+ ]
diff --git a/roundabout/locations/api/urls.py b/roundabout/calibrations/api/views.py
similarity index 56%
rename from roundabout/locations/api/urls.py
rename to roundabout/calibrations/api/views.py
index 6991ac770..d8c486c62 100644
--- a/roundabout/locations/api/urls.py
+++ b/roundabout/calibrations/api/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
@@ -19,14 +19,19 @@
# If not, see .
"""
-from django.urls import path, include
-from rest_framework.routers import DefaultRouter, SimpleRouter
-from .views import LocationViewSet
+from rest_framework import generics, filters, viewsets
+from rest_framework.permissions import IsAuthenticated
+from ..models import CalibrationEvent, CoefficientNameEvent
+from .serializers import CalibrationEventSerializer, CoefficientNameEventSerializer
-# Create a router and register our viewsets with it.
-router = SimpleRouter()
-router.register(r'locations', LocationViewSet )
-urlpatterns = [
- path('', include(router.urls) ),
-]
+class CalibrationEventViewSet(viewsets.ModelViewSet):
+ serializer_class = CalibrationEventSerializer
+ permission_classes = (IsAuthenticated,)
+ queryset = CalibrationEvent.objects.all()
+
+
+class CoefficientNameEventViewSet(viewsets.ModelViewSet):
+ serializer_class = CoefficientNameEventSerializer
+ permission_classes = (IsAuthenticated,)
+ queryset = CoefficientNameEvent.objects.all()
diff --git a/roundabout/calibrations/forms.py b/roundabout/calibrations/forms.py
index 9b3099cda..b66531103 100644
--- a/roundabout/calibrations/forms.py
+++ b/roundabout/calibrations/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
@@ -31,11 +31,11 @@
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
-# Event form
+# Event form
# Inputs: Effective Date and Approval
class CalibrationEventForm(forms.ModelForm):
class Meta:
- model = CalibrationEvent
+ model = CalibrationEvent
fields = ['calibration_date','user_draft']
labels = {
'calibration_date': 'Calibration Date',
@@ -44,7 +44,7 @@ class Meta:
widgets = {
'calibration_date': DatePickerInput(
options={
- "format": "MM/DD/YYYY",
+ "format": "MM/DD/YYYY",
"showClose": True,
"showClear": True,
"showTodayButton": True,
@@ -61,7 +61,7 @@ def clean_user_draft(self):
user_draft = self.cleaned_data.get('user_draft')
return user_draft
- def save(self, commit = True):
+ def save(self, commit = True):
event = super(CalibrationEventForm, self).save(commit = False)
if commit:
event.save()
@@ -73,11 +73,11 @@ def save(self, commit = True):
return event
-# CoefficientName Event form
-# Inputs: Reviewers
+# CoefficientName Event form
+# Inputs: Reviewers
class CoefficientNameEventForm(forms.ModelForm):
class Meta:
- model = CoefficientNameEvent
+ model = CoefficientNameEvent
fields = ['user_draft']
labels = {
'user_draft': 'Reviewers'
@@ -94,7 +94,7 @@ def clean_user_draft(self):
user_draft = self.cleaned_data.get('user_draft')
return user_draft
- def save(self, commit = True):
+ def save(self, commit = True):
event = super(CoefficientNameEventForm, self).save(commit = False)
if commit:
event.save()
@@ -104,11 +104,11 @@ def save(self, commit = True):
event.user_approver.remove(user)
event.save()
return event
-
+
# CoefficientValueSet form
-# Inputs: Coefficient values and notes per Part Calibration
+# Inputs: Coefficient values and notes per Part Calibration
class CoefficientValueSetForm(forms.ModelForm):
class Meta:
model = CoefficientValueSet
@@ -154,7 +154,7 @@ def clean_value_set(self):
else:
return validate_coeff_vals(self.instance, set_type, raw_set)
- def save(self, commit = True):
+ def save(self, commit = True):
value_set = super(CoefficientValueSetForm, self).save(commit = False)
if commit:
value_set.save()
@@ -167,13 +167,27 @@ def save(self, commit = True):
class CoefficientNameForm(forms.ModelForm):
class Meta:
model = CoefficientName
- fields = ['calibration_name', 'value_set_type', 'sigfig_override']
+ fields = ['calibration_name', 'value_set_type', 'sigfig_override', 'deprecated']
labels = {
'calibration_name': 'Name',
'value_set_type': 'Type',
- 'sigfig_override': 'Significant Figures'
+ 'sigfig_override': 'Significant Figures',
+ 'deprecated': 'Deprecated'
+ }
+ widgets = {
+ 'deprecated': forms.CheckboxInput()
}
+ def __init__(self, *args, **kwargs):
+ super(CoefficientNameForm, self).__init__(*args, **kwargs)
+ if self.instance.deprecated:
+ self.fields['calibration_name'].widget.attrs.update(
+ {
+ 'readonly': True,
+ 'style': 'cursor: not-allowed; pointer-events: none; background-color: #d5dfed;'
+ }
+ )
+
def clean_sigfig_override(self):
raw_sigfig = self.cleaned_data.get('sigfig_override')
try:
@@ -222,11 +236,11 @@ def clean_original_value(self):
orig_val = self.cleaned_data.get('original_value')
return orig_val
- def save(self, commit = True):
+ def save(self, commit = True):
coeff_val_inst = super(CoefficientValueForm, self).save(commit = False)
coeff_val_inst.value = round(
- coeff_val_inst.original_value,
- sigfigs = coeff_val_inst.sigfig,
+ coeff_val_inst.original_value,
+ sigfigs = coeff_val_inst.sigfig,
notation = coeff_val_inst.notation_format
)
coeff_val_inst.save()
@@ -234,10 +248,10 @@ def save(self, commit = True):
# Calibration Copy Form
-# Inputs: Part
+# Inputs: Part
class CalPartCopyForm(forms.Form):
part_select = forms.ModelChoiceField(
- queryset = Part.objects.filter(part_type__name='Instrument'),
+ queryset = Part.objects.filter(part_type__ccc_toggle=True),
required=False,
label = 'Copy Calibrations from Part'
)
@@ -245,7 +259,7 @@ class CalPartCopyForm(forms.Form):
def __init__(self, *args, **kwargs):
self.part_id = kwargs.pop('part_id')
super(CalPartCopyForm, self).__init__(*args, **kwargs)
- self.fields['part_select'].queryset = Part.objects.filter(part_type__name='Instrument',coefficient_name_events__gt=0).exclude(id__in=str(self.part_id))
+ self.fields['part_select'].queryset = Part.objects.filter(part_type__ccc_toggle=True,coefficient_name_events__gt=0).exclude(id__in=str(self.part_id))
def clean_part_select(self):
part_select = self.cleaned_data.get('part_select')
@@ -265,36 +279,36 @@ def save(self):
# Coefficient ValueSet form instance generator for CalibrationEvents
EventValueSetFormset = inlineformset_factory(
- CalibrationEvent,
- CoefficientValueSet,
+ CalibrationEvent,
+ CoefficientValueSet,
form=CoefficientValueSetForm,
- fields=('coefficient_name', 'value_set', 'notes'),
- extra=0,
+ fields=('coefficient_name', 'value_set', 'notes'),
+ extra=0,
can_delete=True
)
# Coefficient Name form instance generator for Parts
PartCalNameFormset = inlineformset_factory(
- CoefficientNameEvent,
- CoefficientName,
- form=CoefficientNameForm,
- fields=('calibration_name', 'value_set_type', 'sigfig_override'),
- extra=1,
+ CoefficientNameEvent,
+ CoefficientName,
+ form=CoefficientNameForm,
+ fields=('calibration_name', 'value_set_type', 'sigfig_override', 'deprecated'),
+ extra=1,
can_delete=True
)
# Coefficient Value form instance generator for CoefficientValueSets
ValueSetValueFormset = inlineformset_factory(
- CoefficientValueSet,
- CoefficientValue,
+ CoefficientValueSet,
+ CoefficientValue,
form=CoefficientValueForm,
- fields=('original_value', 'sigfig', 'notation_format'),
- extra=0,
+ fields=('original_value', 'sigfig', 'notation_format'),
+ extra=0,
can_delete=True
)
# Validator for 1-D, comma-separated Coefficient value arrays
-def validate_coeff_array(coeff_1d_array, valset_inst, val_set_index = 0):
+def validate_coeff_array(coeff_1d_array, valset_inst, val_set_index = 0, filename = '', cal_name = ''):
error_row_index = val_set_index + 1
for idx, val in enumerate(coeff_1d_array):
val = val.strip()
@@ -303,8 +317,8 @@ def validate_coeff_array(coeff_1d_array, valset_inst, val_set_index = 0):
rounded_coeff_val = round(val)
except:
raise ValidationError(
- _('Row: %(row)s, Column: %(column)s, %(value)s is an invalid Number. Please enter a valid Number (Digits + 1 optional decimal point).'),
- params={'row': error_row_index, 'value': val, 'column': error_col_index},
+ _('File: %(filename)s, Calibration Name: %(cal_name)s, Row: %(row)s, Column: %(column)s, %(value)s is an invalid Number. Please enter a valid Number (Digits + 1 optional decimal point).'),
+ params={'row': error_row_index, 'value': val, 'column': error_col_index, 'filename': filename, 'cal_name': cal_name},
)
else:
coeff_dec_places = rounded_coeff_val[::-1].find('.')
@@ -312,8 +326,8 @@ def validate_coeff_array(coeff_1d_array, valset_inst, val_set_index = 0):
assert coeff_dec_places <= valset_inst.cal_dec_places
except:
raise ValidationError(
- _('Row: %(row)s, Column: %(column)s, %(value)s Exceeded %(dec_places)s-digit decimal place maximum.'),
- params={'row': error_row_index, 'dec_places': valset_inst.cal_dec_places, 'value': val, 'column': error_col_index},
+ _('File: %(filename)s, Calibration Name: %(cal_name)s, Row: %(row)s, Column: %(column)s, %(value)s Exceeded Instrument %(dec_places)s-digit decimal place maximum.'),
+ params={'row': error_row_index, 'dec_places': valset_inst.cal_dec_places, 'value': val, 'column': error_col_index, 'filename': filename, 'cal_name': cal_name},
)
else:
try:
@@ -321,8 +335,8 @@ def validate_coeff_array(coeff_1d_array, valset_inst, val_set_index = 0):
assert len(digits_only) <= 32
except:
raise ValidationError(
- _('Row: %(row)s, Column: %(column)s, %(value)s Exceeded 32-digit max length'),
- params={'row': error_row_index, 'column': error_col_index, 'value': val},
+ _('File: %(filename)s, Calibration Name: %(cal_name)s, Row: %(row)s, Column: %(column)s, %(value)s Exceeded 32-digit max length'),
+ params={'row': error_row_index, 'column': error_col_index, 'value': val, 'filename': filename, 'cal_name': cal_name},
)
else:
continue
@@ -331,7 +345,7 @@ def validate_coeff_array(coeff_1d_array, valset_inst, val_set_index = 0):
# Validator for Coefficient values within a CoefficientValueSet
# Checks for numeric-type, part-based decimal place limit, number of digits limit
# Displays array index/value of invalid input
-def validate_coeff_vals(valset_inst, set_type, coeff_val_set):
+def validate_coeff_vals(valset_inst, set_type, coeff_val_set, filename = '', cal_name = ''):
if set_type == 'sl':
try:
coeff_batch = coeff_val_set.split(',')
@@ -341,7 +355,7 @@ def validate_coeff_vals(valset_inst, set_type, coeff_val_set):
_('More than 1 value associated with Single input type')
)
else:
- validate_coeff_array(coeff_batch, valset_inst)
+ validate_coeff_array(coeff_batch, valset_inst, 0, filename, cal_name)
return coeff_val_set
elif set_type == '1d':
@@ -352,7 +366,7 @@ def validate_coeff_vals(valset_inst, set_type, coeff_val_set):
_('Unable to parse 1D array')
)
else:
- validate_coeff_array(coeff_batch, valset_inst)
+ validate_coeff_array(coeff_batch, valset_inst, 0, filename, cal_name)
return coeff_val_set
elif set_type == '2d':
@@ -365,7 +379,7 @@ def validate_coeff_vals(valset_inst, set_type, coeff_val_set):
else:
for row_index, row_set in enumerate(coeff_2d_array):
coeff_1d_array = row_set.split(',')
- validate_coeff_array(coeff_1d_array, valset_inst, row_index)
+ validate_coeff_array(coeff_1d_array, valset_inst, row_index, filename, cal_name)
return coeff_val_set
@@ -376,7 +390,7 @@ def find_notation(val):
if ( 'e' in val ):
notation = 'sci'
return notation
-
+
# Parses Coefficient Value significant digits
def find_sigfigs(val):
@@ -395,7 +409,7 @@ def parse_coeff_1d_array(coeff_1d_array, value_set_instance, row_index = 0):
notation = find_notation(val)
sigfig = find_sigfigs(val)
coeff_val_obj = CoefficientValue(
- coeff_value_set = value_set_instance,
+ coeff_value_set = value_set_instance,
value = val,
original_value = val,
notation_format = notation,
@@ -461,5 +475,5 @@ def validate_part_select(to_part, from_part):
raise ValidationError(
_('Duplicate Calibration Names exist between Parts. Please select Part with unique Calibration Names.')
)
- else:
- pass
\ No newline at end of file
+ else:
+ pass
diff --git a/roundabout/calibrations/migrations/0021_coefficientname_deprecated.py b/roundabout/calibrations/migrations/0021_coefficientname_deprecated.py
new file mode 100644
index 000000000..c863e1777
--- /dev/null
+++ b/roundabout/calibrations/migrations/0021_coefficientname_deprecated.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.13 on 2020-09-08 14:01
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('calibrations', '0020_auto_20200901_1711'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='coefficientname',
+ name='deprecated',
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/roundabout/calibrations/migrations/0022_merge_20200923_1323.py b/roundabout/calibrations/migrations/0022_merge_20200923_1323.py
new file mode 100644
index 000000000..5deb8bd2a
--- /dev/null
+++ b/roundabout/calibrations/migrations/0022_merge_20200923_1323.py
@@ -0,0 +1,14 @@
+# Generated by Django 2.2.13 on 2020-09-23 13:23
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('calibrations', '0021_coefficientname_deprecated'),
+ ('calibrations', '0021_auto_20200916_1607'),
+ ]
+
+ operations = [
+ ]
diff --git a/roundabout/calibrations/models.py b/roundabout/calibrations/models.py
index c81bc1759..ccea40c33 100644
--- a/roundabout/calibrations/models.py
+++ b/roundabout/calibrations/models.py
@@ -111,6 +111,7 @@ def get_object_type(self):
calibration_name = models.CharField(max_length=255, unique=False, db_index=True)
value_set_type = models.CharField(max_length=3, choices=VALUE_SET_TYPE, null=False, blank=False, default="sl")
sigfig_override = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(20)], null=False, blank=True, default=3, help_text='Part-based default if sigfigs cannot be captured from input')
+ deprecated = models.BooleanField(null=False, default=False)
created_at = models.DateTimeField(default=timezone.now)
part = models.ForeignKey(Part, related_name='coefficient_names', on_delete=models.CASCADE, null=True)
coeff_name_event = models.ForeignKey(CoefficientNameEvent, related_name='coefficient_names', on_delete=models.CASCADE, null=True)
diff --git a/roundabout/calibrations/views.py b/roundabout/calibrations/views.py
index f2a2fb912..daba6ea68 100644
--- a/roundabout/calibrations/views.py
+++ b/roundabout/calibrations/views.py
@@ -50,7 +50,7 @@ def get(self, request, *args, **kwargs):
self.object = None
inv_inst = Inventory.objects.get(id=self.kwargs['pk'])
coeff_event = inv_inst.part.coefficient_name_events.first()
- cal_names = coeff_event.coefficient_names.all()
+ cal_names = coeff_event.coefficient_names.exclude(deprecated=True)
form_class = self.get_form_class()
form = self.get_form(form_class)
form.fields['user_draft'].required = True
diff --git a/roundabout/configs_constants/forms.py b/roundabout/configs_constants/forms.py
index f4aedbefb..37795bc76 100644
--- a/roundabout/configs_constants/forms.py
+++ b/roundabout/configs_constants/forms.py
@@ -139,12 +139,14 @@ class Meta:
fields = [
'name',
'config_type',
- 'include_with_calibrations'
+ 'include_with_calibrations',
+ 'deprecated'
]
labels = {
'name': 'Configuration/Constant Name',
'config_type': 'Type',
- 'include_with_calibrations': 'Export with Calibrations'
+ 'include_with_calibrations': 'Export with Calibrations',
+ 'deprecated': 'Deprecated'
}
widgets = {
'name': forms.TextInput(
@@ -152,8 +154,18 @@ class Meta:
'required': False
}
),
- 'include_with_calibrations': forms.CheckboxInput()
+ 'include_with_calibrations': forms.CheckboxInput(),
+ 'deprecated': forms.CheckboxInput()
}
+ def __init__(self, *args, **kwargs):
+ super(ConfigNameForm, self).__init__(*args, **kwargs)
+ if self.instance.deprecated:
+ self.fields['name'].widget.attrs.update(
+ {
+ 'readonly': True,
+ 'style': 'cursor: not-allowed; pointer-events: none; background-color: #d5dfed;'
+ }
+ )
# Constant Default form
@@ -278,7 +290,7 @@ def clean_config_name(self):
# Inputs: Part
class ConfPartCopyForm(forms.Form):
from_part = forms.ModelChoiceField(
- queryset = Part.objects.filter(part_type__name='Instrument'),
+ queryset = Part.objects.filter(part_type__ccc_toggle=True),
required=False,
label = 'Copy Configurations/Constants from Part'
)
@@ -310,7 +322,7 @@ def save(self):
ConfigValue,
form=ConfigValueForm,
fields=('config_name', 'config_value', 'notes'),
- extra=1,
+ extra=0,
can_delete=True
)
@@ -322,7 +334,8 @@ def save(self):
fields=(
'name',
'config_type',
- 'include_with_calibrations'
+ 'include_with_calibrations',
+ 'deprecated'
),
extra=1,
can_delete=True
diff --git a/roundabout/configs_constants/migrations/0018_configname_deprecated.py b/roundabout/configs_constants/migrations/0018_configname_deprecated.py
new file mode 100644
index 000000000..58bbf48a8
--- /dev/null
+++ b/roundabout/configs_constants/migrations/0018_configname_deprecated.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.13 on 2020-09-25 14:47
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('configs_constants', '0017_configevent_config_type'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='configname',
+ name='deprecated',
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/roundabout/configs_constants/models.py b/roundabout/configs_constants/models.py
index f4618deb5..39f3a65c6 100644
--- a/roundabout/configs_constants/models.py
+++ b/roundabout/configs_constants/models.py
@@ -120,6 +120,7 @@ def get_object_type(self):
name = models.CharField(max_length=255, unique=False, db_index=True)
config_type = models.CharField(max_length=4, choices=CONFIG_TYPE, null=False, blank=False, default="cnst")
created_at = models.DateTimeField(default=timezone.now)
+ deprecated = models.BooleanField(null=False, default=False)
part = models.ForeignKey(Part, related_name='config_names', on_delete=models.CASCADE, null=True)
include_with_calibrations = models.BooleanField(null=False, default=False)
config_name_event = models.ForeignKey(ConfigNameEvent, related_name='config_names', on_delete=models.CASCADE, null=True)
diff --git a/roundabout/configs_constants/views.py b/roundabout/configs_constants/views.py
index f712aaa18..52c06292c 100644
--- a/roundabout/configs_constants/views.py
+++ b/roundabout/configs_constants/views.py
@@ -55,6 +55,7 @@ def get(self, request, *args, **kwargs):
names = ConfigName.objects.filter(config_name_event = inv_inst.part.config_name_events.first(), config_type = 'cnst')
else:
names = ConfigName.objects.filter(config_name_event = inv_inst.part.config_name_events.first(), config_type = 'conf')
+ names = names.exclude(deprecated = True)
form_class = self.get_form_class()
form = self.get_form(form_class)
form.fields['user_draft'].required = True
@@ -188,63 +189,12 @@ class ConfigEventValueUpdate(LoginRequiredMixin, PermissionRequiredMixin, AjaxFo
def get(self, request, *args, **kwargs):
self.object = self.get_object()
cfg_type = self.kwargs['cfg_type']
- if cfg_type == 1:
- part_config_names = ConfigName.objects.filter(config_name_event = self.object.inventory.part.config_name_events.first(), config_type = 'cnst')
- else:
- part_config_names = ConfigName.objects.filter(config_name_event = self.object.inventory.part.config_name_events.first(), config_type = 'conf')
- event_config_names = self.object.config_values.all()
- extra_rows = len(part_config_names) - len (event_config_names)
form_class = self.get_form_class()
form = self.get_form(form_class)
form.fields['user_draft'].required = False
- ConfigEventValueAddFormset = inlineformset_factory(
- ConfigEvent,
- ConfigValue,
- form=ConfigValueForm,
- fields=('config_name', 'config_value', 'notes'),
- extra=extra_rows,
- can_delete=True
- )
- config_event_value_form = ConfigEventValueAddFormset(
+ config_event_value_form = ConfigEventValueFormset(
instance=self.object
)
- for idx,name in enumerate(part_config_names):
- if cfg_type == 1:
- try:
- default_value = ConstDefault.objects.get(const_event = self.object.inventory.constant_default_events.first(), config_name = name).default_value
- except ConstDefault.DoesNotExist:
- default_value = ''
- if cfg_type == 2:
- try:
- default_value = ConfigDefault.objects.get(conf_def_event = self.object.inventory.assembly_part.config_default_events.first(), config_name = name).default_value
- except ConfigDefault.DoesNotExist:
- default_value = ''
- try:
- config_value = ConfigValue.objects.get(config_event = self.object, config_name = name)
- except ConfigValue.DoesNotExist:
- config_value = ''
-
- if default_value != '' and config_value != '':
- config_event_value_form.forms[idx].initial = {
- 'config_name': name,
- 'config_value': default_value,
- 'notes': config_value.notes
- }
- if default_value != '' and config_value == '':
- config_event_value_form.forms[idx].initial = {
- 'config_name': name,
- 'config_value': default_value
- }
- if default_value == '' and config_value != '':
- config_event_value_form.forms[idx].initial = {
- 'config_name': name,
- 'config_value': config_value,
- 'notes': config_value.notes
- }
- if default_value == '' and config_value == '':
- config_event_value_form.forms[idx].initial = {
- 'config_name': name
- }
return self.render_to_response(
self.get_context_data(
form=form,
@@ -595,7 +545,7 @@ def get(self, request, *args, **kwargs):
self.object = None
inv_inst = Inventory.objects.get(id=self.kwargs['pk'])
conf_name_event = inv_inst.part.config_name_events.first()
- const_names = conf_name_event.config_names.filter(config_type='cnst')
+ const_names = conf_name_event.config_names.filter(config_type='cnst', deprecated = False)
form_class = self.get_form_class()
form = self.get_form(form_class)
form.fields['user_draft'].required = True
@@ -698,38 +648,30 @@ def get(self, request, *args, **kwargs):
self.object = self.get_object()
form_class = self.get_form_class()
form = self.get_form(form_class)
+ inv_inst = Inventory.objects.get(id=self.object.inventory.id)
+ conf_name_event = inv_inst.part.config_name_events.first()
+ const_names = conf_name_event.config_names.filter(config_type='cnst', deprecated = False).order_by('created_at')
form.fields['user_draft'].required = False
- conf_name_event = self.object.inventory.part.config_name_events.first()
- const_names = conf_name_event.config_names.filter(config_type='cnst')
- event_default_names = self.object.constant_defaults.all()
- extra_rows = len(const_names) - len(event_default_names)
EventDefaultAddFormset = inlineformset_factory(
ConstDefaultEvent,
ConstDefault,
form=ConstDefaultForm,
fields=('config_name', 'default_value'),
- extra=extra_rows,
+ extra=len(const_names) - len(self.object.constant_defaults.all()),
can_delete=True
)
event_default_form = EventDefaultAddFormset(
instance=self.object
)
for idx,name in enumerate(const_names):
- if self.object.inventory.constant_default_events.exists():
- const_def_event = self.object.inventory.constant_default_events.first()
- try:
- default_value = ConstDefault.objects.get(const_event = const_def_event, config_name = name).default_value
- except ConstDefault.DoesNotExist:
- default_value = ''
-
- event_default_form.forms[idx].initial = {
- 'config_name': name,
- 'default_value': default_value
- }
- else:
- event_default_form.forms[idx].initial = {
- 'config_name': name
- }
+ try:
+ default_value = ConstDefault.objects.get(config_name = name, const_event = self.object)
+ except ConstDefault.DoesNotExist:
+ default_value = ''
+ event_default_form.forms[idx].initial = {
+ 'config_name': name,
+ 'default_value': default_value
+ }
return self.render_to_response(
self.get_context_data(
form=form,
@@ -833,7 +775,7 @@ def get(self, request, *args, **kwargs):
self.object = None
assm_part_inst = AssemblyPart.objects.get(id=self.kwargs['pk'])
conf_name_event = assm_part_inst.part.config_name_events.first()
- conf_names = conf_name_event.config_names.filter(config_type='conf')
+ conf_names = conf_name_event.config_names.filter(config_type='conf', deprecated = False)
form_class = self.get_form_class()
form = self.get_form(form_class)
form.fields['user_draft'].required = True
@@ -937,36 +879,29 @@ def get(self, request, *args, **kwargs):
form_class = self.get_form_class()
form = self.get_form(form_class)
form.fields['user_draft'].required = False
- part_confname_event = self.object.assembly_part.part.config_name_events.first()
- part_config_names = part_confname_event.config_names.filter(config_type='conf')
- event_config_names = self.object.config_defaults.all()
- extra_rows = len(part_config_names) - len(event_config_names)
+ assm_part_inst = AssemblyPart.objects.get(id=self.object.assembly_part.id)
+ conf_name_event = assm_part_inst.part.config_name_events.first()
+ conf_names = conf_name_event.config_names.filter(config_type='conf', deprecated = False).order_by('created_at')
EventConfigDefaultAddFormset = inlineformset_factory(
ConfigDefaultEvent,
ConfigDefault,
form=ConfigDefaultForm,
fields=('config_name', 'default_value'),
- extra=extra_rows,
+ extra=len(conf_names) - len(self.object.config_defaults.all()),
can_delete=True
)
event_default_form = EventConfigDefaultAddFormset(
instance=self.object
)
- for idx,name in enumerate(part_config_names):
- if self.object.assembly_part.config_default_events.exists():
- conf_def_event = self.object.assembly_part.config_default_events.first()
- try:
- default_value = ConfigDefault.objects.get(conf_def_event = conf_def_event, config_name = name).default_value
- except ConfigDefault.DoesNotExist:
- default_value = ''
- event_default_form.forms[idx].initial = {
- 'config_name': name,
- 'default_value': default_value
- }
- else:
- event_default_form.forms[idx].initial = {
- 'config_name': name
- }
+ for idx,name in enumerate(conf_names):
+ try:
+ default_value = ConfigDefault.objects.get(config_name = name, conf_def_event = self.object)
+ except ConfigDefault.DoesNotExist:
+ default_value = ''
+ event_default_form.forms[idx].initial = {
+ 'config_name': name,
+ 'default_value': default_value
+ }
return self.render_to_response(
self.get_context_data(
form=form,
diff --git a/roundabout/core/api/__init__.py b/roundabout/core/api/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/roundabout/core/api/renderers.py b/roundabout/core/api/renderers.py
new file mode 100644
index 000000000..97cc148f8
--- /dev/null
+++ b/roundabout/core/api/renderers.py
@@ -0,0 +1,6 @@
+from rest_framework.renderers import BrowsableAPIRenderer
+
+class BrowsableAPIRendererWithoutForms(BrowsableAPIRenderer):
+ """Renders the browsable api, but excludes the forms."""
+ def get_rendered_html_form(self, data, view, method, request):
+ return None
diff --git a/roundabout/core/api/serializers.py b/roundabout/core/api/serializers.py
new file mode 100644
index 000000000..a372cad97
--- /dev/null
+++ b/roundabout/core/api/serializers.py
@@ -0,0 +1,7 @@
+from rest_framework import serializers
+
+# Need a "sub-serializer" to handle self refernce MPTT tree structures
+class RecursiveFieldSerializer(serializers.Serializer):
+ def to_representation(self, value):
+ serializer = self.parent.parent.__class__(value, context=self.context)
+ return serializer.data
diff --git a/roundabout/core/api/urls.py b/roundabout/core/api/urls.py
new file mode 100644
index 000000000..adfb6a01d
--- /dev/null
+++ b/roundabout/core/api/urls.py
@@ -0,0 +1,58 @@
+"""
+# 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 django.urls import path, include
+from rest_framework.routers import DefaultRouter, SimpleRouter
+from roundabout.inventory.api.views import InventoryViewSet, ActionViewSet, PhotoNoteViewSet
+from roundabout.assemblies.api.views import AssemblyViewSet, AssemblyRevisionViewSet, AssemblyPartViewSet, AssemblyTypeViewSet
+from roundabout.calibrations.api.views import CalibrationEventViewSet, CoefficientNameEventViewSet
+from roundabout.locations.api.views import LocationViewSet
+from roundabout.parts.api.views import PartViewSet, PartTypeViewSet, RevisionViewSet, DocumentationViewSet
+from roundabout.userdefinedfields.api.views import FieldViewSet, FieldValueViewSet
+
+# Create a router and register our viewsets with it.
+router = DefaultRouter()
+router.register(r'inventory', InventoryViewSet, 'inventory' )
+router.register(r'actions', ActionViewSet, 'actions' )
+router.register(r'photos', PhotoNoteViewSet, 'photos' )
+
+router.register(r'assembly-templates/assemblies', AssemblyViewSet, 'assembly-templates/assemblies' )
+router.register(r'assembly-templates/assembly-revisions', AssemblyRevisionViewSet, 'assembly-templates/assembly-revisions' )
+router.register(r'assembly-templates/assembly-types', AssemblyTypeViewSet, 'assembly-templates/assembly-types' )
+router.register(r'assembly-templates/assembly-parts', AssemblyPartViewSet, 'assembly-templates/assembly-parts' )
+
+router.register(r'calibrations/calibration-events', CalibrationEventViewSet, 'calibrations/calibration-events' )
+router.register(r'calibrations/coefficent-name-events', CoefficientNameEventViewSet, 'calibrations/coefficent-name-events' )
+
+router.register(r'locations', LocationViewSet, 'locations' )
+
+router.register(r'part-templates/parts', PartViewSet, 'part-templates/parts' )
+router.register(r'part-templates/part-types', PartTypeViewSet, 'part-templates/part-types' )
+router.register(r'part-templates/revisions', RevisionViewSet, 'part-templates/revisions' )
+router.register(r'part-templates/documents', DocumentationViewSet, 'part-templates/documents' )
+
+router.register(r'user-defined-fields/fields', FieldViewSet, 'user-defined-fields/fields' )
+router.register(r'user-defined-fields/field-values', FieldValueViewSet, 'user-defined-fields/field-values' )
+
+app_name = 'api_v1'
+urlpatterns = [
+ path('', include(router.urls) ),
+]
diff --git a/roundabout/field_instances/__init__.py b/roundabout/field_instances/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/roundabout/field_instances/admin.py b/roundabout/field_instances/admin.py
new file mode 100644
index 000000000..064b122bf
--- /dev/null
+++ b/roundabout/field_instances/admin.py
@@ -0,0 +1,6 @@
+from django.contrib import admin
+
+from .models import *
+
+# Register your models here.
+admin.site.register(FieldInstance)
diff --git a/roundabout/field_instances/apps.py b/roundabout/field_instances/apps.py
new file mode 100644
index 000000000..d7fffa6f1
--- /dev/null
+++ b/roundabout/field_instances/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class FieldInstancesConfig(AppConfig):
+ name = 'field_instances'
diff --git a/roundabout/parts/api/urls.py b/roundabout/field_instances/forms.py
similarity index 73%
rename from roundabout/parts/api/urls.py
rename to roundabout/field_instances/forms.py
index 8fd214595..1fa3daffa 100644
--- a/roundabout/parts/api/urls.py
+++ b/roundabout/field_instances/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
@@ -19,14 +19,13 @@
# If not, see .
"""
-from django.urls import path, include
-from rest_framework.routers import DefaultRouter, SimpleRouter
-from .views import PartViewSet
+from django import forms
-# Create a router and register our viewsets with it.
-router = SimpleRouter()
-router.register(r'parts', PartViewSet )
+from .models import FieldInstance
-urlpatterns = [
- path('', include(router.urls) ),
-]
+
+class FieldInstanceForm(forms.ModelForm):
+
+ class Meta:
+ model = FieldInstance
+ fields = '__all__'
diff --git a/roundabout/field_instances/migrations/0001_initial.py b/roundabout/field_instances/migrations/0001_initial.py
new file mode 100644
index 000000000..fc6ffdf5c
--- /dev/null
+++ b/roundabout/field_instances/migrations/0001_initial.py
@@ -0,0 +1,35 @@
+# Generated by Django 2.2.13 on 2020-08-16 18:00
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('cruises', '0012_auto_20200727_1746'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='FieldInstance',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(blank=True, db_index=True, max_length=255)),
+ ('start_date', models.DateTimeField(blank=True, default=django.utils.timezone.now, null=True)),
+ ('end_date', models.DateTimeField(blank=True, null=True)),
+ ('notes', models.TextField(blank=True)),
+ ('is_this_instance', models.BooleanField(default=False)),
+ ('cruise', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='field_instances', to='cruises.Cruise')),
+ ('users', models.ManyToManyField(related_name='field_instances', to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'ordering': ['-start_date'],
+ },
+ ),
+ ]
diff --git a/roundabout/field_instances/migrations/__init__.py b/roundabout/field_instances/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/roundabout/field_instances/models.py b/roundabout/field_instances/models.py
new file mode 100644
index 000000000..bea56df94
--- /dev/null
+++ b/roundabout/field_instances/models.py
@@ -0,0 +1,44 @@
+from django.db import models
+from django.utils import timezone
+
+from roundabout.users.models import *
+
+"""
+Model to track users and registrations of "in the field" RDB instances.
+These are instances that run strictly locally without internet access necessary.
+"""
+
+class FieldInstance(models.Model):
+ name = models.CharField(max_length=255, null=False, blank=True, db_index=True)
+ start_date = models.DateTimeField(default=timezone.now, null=True, blank=True)
+ end_date = models.DateTimeField(null=True, blank=True)
+ users = models.ManyToManyField('users.User', related_name='field_instances', blank=False)
+ cruise = models.ForeignKey('cruises.Cruise', related_name='field_instances',
+ on_delete=models.SET_NULL, null=True, blank=True)
+ notes = models.TextField(blank=True)
+ # True if this RDB instance is registered as a FieldInstance
+ is_this_instance = models.BooleanField(default=False)
+
+ class Meta:
+ ordering = ['-start_date']
+
+ def __str__(self):
+ return self.name
+
+ """
+ # Custom save method to deactive non-deployed users
+ def save(self, *args, **kwargs):
+ super(FieldInstance, self).save(*args, **kwargs)
+ print(self.cruise)
+ if self.is_this_instance:
+ for user in self.users.all():
+ print(user)
+ user.is_active = True
+ user.save()
+
+ users_deactivate = User.objects.exclude(id__in=self.users.all())
+ for user in users_deactivate:
+ print(user)
+ user.is_active = False
+ user.save()
+ """
diff --git a/roundabout/field_instances/requests.py b/roundabout/field_instances/requests.py
new file mode 100644
index 000000000..ab558454e
--- /dev/null
+++ b/roundabout/field_instances/requests.py
@@ -0,0 +1,339 @@
+import requests
+import json
+import random
+import os
+
+from django.conf import settings
+from django.http import HttpResponse
+from django.views.generic import View
+from django.db.models import Q
+from django.contrib.sites.shortcuts import get_current_site
+from rest_framework.reverse import reverse, reverse_lazy
+
+#from .models import FieldInstance
+from .models import *
+from roundabout.inventory.api.serializers import InventorySerializer, ActionSerializer
+from roundabout.inventory.models import Inventory, Action, PhotoNote
+from roundabout.userdefinedfields.api.serializers import FieldSerializer, FieldValueSerializer
+from roundabout.userdefinedfields.models import Field
+from roundabout.parts.models import Part, Revision
+from roundabout.locations.models import Location
+from roundabout.locations.api.serializers import LocationSerializer
+
+
+# Import environment variables from .env files
+import environ
+env = environ.Env()
+base_url = env('RDB_SITE_URL')
+api_version_url = '/api/v1'
+api_token = '7de11f0cd61a6d50899192c0d02a975b2b204c16'
+headers = {
+ 'Authorization': 'Token ' + api_token,
+}
+"""
+Main sync function to coordinate the different models
+"""
+def field_instance_sync_main(request, field_instance):
+ status_code = 401
+ pk_mappings = {}
+ # Sync base models needed for DB relationships
+ # Run Location sync
+ location_pk_mappings = _sync_request_locations(request, field_instance)
+ print(location_pk_mappings)
+ pk_mappings.update({'location_pk_mappings': location_pk_mappings})
+ # Run Field model sync
+ field_pk_mappings = _sync_request_fields(request, field_instance)
+ pk_mappings.update({'field_pk_mappings': field_pk_mappings})
+ # Sync Inventory items
+ inventory_respone = _sync_request_inventory(request, field_instance, pk_mappings)
+ status_code = inventory_respone
+ print('STATUS CODE: ', inventory_respone)
+ return status_code
+
+
+"""
+Function to sync NEW objects
+"""
+def _sync_new_objects(field_instance, model, api_url, serializer):
+ pk_mappings = []
+ new_objects = model.objects.filter(created_at__gte=field_instance.start_date)
+
+ if new_objects:
+ for obj in new_objects:
+ old_pk = obj.id
+ # serialize data for JSON request
+ serializer = serializer(obj)
+ data_dict = serializer.data
+ # Need to remap any Parent items that have new PKs
+ new_key = next((pk for pk in pk_mappings if pk['old_pk'] == data_dict['parent']), False)
+ if new_key:
+ print('NEW KEY: ' + new_key)
+ data_dict['parent'] = new_key['new_pk']
+ # These will be POST as new item, so remove id
+ data_dict.pop('id')
+ response = requests.post(api_url, json=data_dict)
+ print(f'{model._meta.model_name} RESPONSE: {response.text}')
+ print(f'{model._meta.model_name} CODE: {response.status_code}')
+ new_obj = response.json()[model._meta.model_name]
+ pk_mappings.append({'old_pk': old_pk, 'new_pk': new_obj['id']})
+ return pk_mappings
+
+
+"""
+Function to sync EXISTING objects
+"""
+def _sync_existing_objects(field_instance, model, api_url, serializer, pk_mappings):
+ status = None
+ # Get all existing fields in Home Base RDB that were updated
+ existing_objects = model.objects.filter(
+ Q(updated_at__gte=field_instance.start_date) & Q(created_at__lt=field_instance.start_date)
+ )
+ if existing_objects:
+ for obj in existing_objects:
+ # serialize data for JSON request
+ serializer = serializer(obj)
+ data_dict = serializer.data
+ # Need to remap any Parent items that have new PKs
+ new_key = next((pk for pk in pk_mappings if pk['old_pk'] == data_dict['parent']), False)
+ if new_key:
+ print('NEW KEY: ' + new_key)
+ data_dict['parent'] = new_key['new_pk']
+
+ url = F'{api_url}{obj.id}/'
+ response = requests.patch(url, json=data_dict, )
+ print(f'{model._meta.model_name} RESPONSE: {response.text}')
+ print(f'{model._meta.model_name} CODE: {response.status_code}')
+ status = response.status_code
+ return status
+
+
+"""
+Request function to sync Field Instance: Locations
+Args:
+request: Django request object
+field_values: queryset of FieldValue objects
+Returns:
+location_pk_mappings: array that maps old_pk to new_pk for new objects
+"""
+def _sync_request_locations(request, field_instance):
+ model = Location
+ serializer = LocationSerializer
+ api_url = base_url + reverse('locations-list')
+ # Sync new objects, return the pk_mappings for new items
+ pk_mappings = _sync_new_objects(field_instance, model, api_url, serializer)
+ status = _sync_existing_objects(field_instance, model, api_url, serializer, pk_mappings)
+
+ return pk_mappings
+
+
+"""
+Request function to sync Field Instance: Fields
+Args:
+request: Django request object
+field_values: queryset of FieldValue objects
+pk_mappings: array that maps old_pk to new_pk for new objects
+"""
+def _sync_request_fields(request, field_instance):
+ model = Field
+ serializer = FieldSerializer
+ api_url = base_url + reverse('api_v1:userdefined-fields/fields-list')
+ # Sync new objects, return the pk_mappings for new items
+ pk_mappings = _sync_new_objects(field_instance, model, api_url, serializer)
+ status = _sync_existing_objects(field_instance, model, api_url, serializer, pk_mappings)
+
+ """
+ field_url = base_url + reverse('userdefinedfields/fields-list')
+ field_pk_mappings = []
+ # Get new fields that were added
+ new_fields = Field.objects.filter(created_at__gte=field_instance.start_date)
+ if new_fields:
+ for field in new_fields:
+ old_pk = field['id']
+ # serialize data for JSON request
+ field_serializer = FieldSerializer(field)
+ field_dict = field_serializer.data
+ # These will be POST as new, so remove id
+ field_dict.pop('id')
+ response = requests.post(field_url, json=field_dict )
+ print('Field RESPONSE:', response.text)
+ print("Field CODE: ", response.status_code)
+ new_obj = response.json()
+ field_pk_mappings.append({'old_pk': old_pk, 'new_pk': new_obj['id']})
+
+ # Get all existing fields in Home Base RDB that were updated
+ existing_fields = Field.objects.filter(
+ Q(updated_at__gte=field_instance.start_date) & Q(created_at__lt=field_instance.start_date)
+ )
+ if existing_fields:
+ for field in existing_fields:
+ # serialize data for JSON request
+ field_serializer = FieldSerializer(field)
+ field_dict = field_serializer.data
+ url = base_url + reverse('userdefinedfields/fields-detail', kwargs={'pk': field.id},)
+ response = requests.patch(url, json=field_dict, )
+ print('Field RESPONSE:', response.text)
+ print("Field CODE: ", response.status_code)
+ """
+ return pk_mappings
+
+
+"""
+Request function to sync Field Instance: Inventory items
+Args:
+request: Django request object
+field_instance: FieldInstance object
+"""
+def _sync_request_inventory(request, field_instance, pk_mappings):
+ inventory_pk_mappings = []
+
+ ##### SYNC INVENTORY #####
+ #inventory_url = F"{base_url}/api/v1/inventory/"
+ inventory_url = base_url + reverse('api_v1:inventory-list')
+ print(inventory_url)
+ # Get new items that were added, these need special handling
+ new_inventory = Inventory.objects.filter(created_at__gte=field_instance.start_date).order_by('-parent')
+ #actions_add_qs = actions.filter(object_type=Action.INVENTORY).filter(action_type=Action.ADD).order_by('-parent')
+ if new_inventory:
+ #new_inventory = [action.inventory for action in actions_add_qs.all()]
+ print(new_inventory)
+ for item in new_inventory:
+ # check if serial number already exists
+ response = requests.get(inventory_url, params={'filter{serial_number}': item.serial_number}, headers={'Content-Type': 'application/json'}, )
+ if response.json():
+ # Need to change the Serial Number to avoid naming conflict
+ item.serial_number = item.serial_number + '-' + str(random.randint(1,1001))
+ #item.save()
+
+ inventory_serializer = InventorySerializer(
+ new_inventory,
+ many=True,
+ #context={'request': request, }
+ )
+ inventory_dict = inventory_serializer.data
+
+ for item in inventory_dict:
+ old_pk = item['id']
+ item.pop('id')
+ # Need to remap any Parent items that have new PKs
+ new_key = next((pk for pk in inventory_pk_mappings if pk['old_pk'] == item['parent']), False)
+ if new_key:
+ print('NEW KEY: ' + new_key)
+ item['parent'] = new_key['new_pk']
+
+ print(json.dumps(item))
+ """
+ response = requests.post(inventory_url, data=json.dumps(item), headers={'Content-Type': 'application/json'}, )
+ print('RESPONSE:', response.json())
+ new_obj = response.json()
+ inventory_pk_mappings.append({'old_pk': old_pk, 'new_pk': new_obj['id']})
+ """
+ print(inventory_pk_mappings)
+
+ # Get all existing items in Home Base RDB
+ existing_inventory = Inventory.objects.filter(
+ Q(updated_at__gte=field_instance.start_date) & Q(created_at__lt=field_instance.start_date)
+ )
+ if existing_inventory:
+ print(existing_inventory)
+ for inv in existing_inventory:
+ # serialize data for JSON request
+ inventory_serializer = InventorySerializer(inv)
+ inventory_dict = inventory_serializer.data
+ url = base_url + reverse('api_v1:inventory-detail', kwargs={'pk': inv.id},)
+ #url = F"{inventory_url}{inv.id}/"
+ print(url)
+ # Need to remap any Parent items that have new PKs
+ new_key = next((pk for pk in inventory_pk_mappings if pk['old_pk'] == inventory_dict['parent']), False)
+ if new_key:
+ print('NEW KEY: ', new_key)
+ inventory_dict['parent'] = new_key['new_pk']
+
+ response = requests.patch(url, json=inventory_dict, )
+ print('INVENTORY RESPONSE:', response.text)
+ print("INVENTORY CODE: ", response.status_code)
+
+ # post all new Actions for this item
+ action_response = _sync_request_actions(request, field_instance, inv, inventory_pk_mappings)
+ # post all Field Values for this item
+ field_value_response = _sync_request_field_values(request, field_instance, inv, field_pk_mappings, inventory_pk_mappings)
+
+ return response.status_code
+
+
+"""
+Request function to sync Field Instance: Actions
+Args:
+request: Django request object
+actions: queryset of Action objects
+pk_mappings: array that maps old_pk to new_pk for new objects
+"""
+def _sync_request_actions(request, field_instance, obj, inventory_pk_mappings=None):
+ action_url = base_url + reverse('api_v1:actions-list')
+ photo_url = base_url + reverse('api_v1:photos-list')
+ # Get all actions for this object
+ object_type = obj._meta.model_name
+ actions = obj.actions.filter(object_type=object_type).filter(created_at__gte=field_instance.start_date)
+
+ for action in actions:
+ # serialize data for JSON request
+ action_serializer = ActionSerializer(action)
+ action_dict = action_serializer.data
+ # These will be POST as new, so remove id
+ action_dict.pop('id')
+ response = requests.post(action_url, json=action_dict )
+ print('ACTION RESPONSE:', response.text)
+ print("ACTION CODE: ", response.status_code)
+ new_action = response.json()
+ # Upload any photos for new Action notes
+ if action.photos.exists():
+ for photo in action.photos.all():
+ multipart_form_data = {
+ 'photo': (photo.photo.name, photo.photo.file),
+ #'inventory': (None, photo.inventory.id),
+ 'action': (None, new_action['action']['id']),
+ 'user': (None, photo.user.id)
+ }
+ response = requests.post(photo_url, files=multipart_form_data )
+ print('PHOTO RESPONSE:', response.text)
+ print("PHOTO CODE: ", response.status_code)
+
+ return 'ACTIONS COMPLETE'
+
+
+"""
+Request function to sync Field Instance: FieldValues
+Args:
+request: Django request object
+field_instance: FieldInstance object
+inventory_item: Inventory object
+field_pk_mappings, inventory_pk_mappings: array that maps old_pk to new_pk for new objects
+"""
+def _sync_request_field_values(request, field_instance, inventory_item, field_pk_mappings, inventory_pk_mappings=None):
+ field_value_url = base_url + reverse('api_v1:userdefined-fields/field-values-list')
+ # Get new field values that were added
+ new_field_values = inventory_item.fieldvalues.filter(created_at__gte=field_instance.start_date)
+ for fv in new_field_values:
+ # serialize data for JSON request
+ fv_serializer = FieldValueSerializer(fv)
+ fv_dict = fv_serializer.data
+ # These will be POST as new, so remove id
+ fv_dict.pop('id')
+ response = requests.post(field_value_url, json=fv_dict )
+ print('Field Value RESPONSE:', response.text)
+ print("Field Value CODE: ", response.status_code)
+
+
+ # Get all existing field values in Home Base RDB that were updated
+ existing_field_values = inventory_item.fieldvalues.filter(
+ Q(updated_at__gte=field_instance.start_date) & Q(created_at__lt=field_instance.start_date)
+ )
+ for fv in existing_field_values:
+ # serialize data for JSON request
+ fv_serializer = FieldValueSerializer(fv)
+ fv_dict = fv_serializer.data
+ url = base_url + reverse('api_v1:userdefinedfields/field-values-detail', kwargs={'pk': fv.id},)
+ response = requests.post(field_value_url, json=fv_dict )
+ print('Field Value RESPONSE:', response.text)
+ print("Field Value CODE: ", response.status_code)
+
+ return 'FIELD VALUES COMPLETE'
diff --git a/roundabout/field_instances/tests.py b/roundabout/field_instances/tests.py
new file mode 100644
index 000000000..7ce503c2d
--- /dev/null
+++ b/roundabout/field_instances/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/roundabout/inventory/api/urls.py b/roundabout/field_instances/urls.py
similarity index 59%
rename from roundabout/inventory/api/urls.py
rename to roundabout/field_instances/urls.py
index dc1a20e84..5e731ea90 100644
--- a/roundabout/inventory/api/urls.py
+++ b/roundabout/field_instances/urls.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
@@ -19,14 +19,16 @@
# If not, see .
"""
-from django.urls import path, include
-from rest_framework.routers import DefaultRouter, SimpleRouter
-from .views import InventoryViewSet
+from django.urls import path
-# Create a router and register our viewsets with it.
-router = SimpleRouter()
-router.register(r'inventory', InventoryViewSet, 'inventory' )
+from . import views
+app_name = 'field_instances'
urlpatterns = [
- path('', include(router.urls) ),
+ path('sync-to-home/', view=views.FieldInstanceSyncToHomeView.as_view(), name='field_instance_sync_to_home'),
+ # CRUD views
+ path('', view=views.FieldInstanceListView.as_view(), name='field_instances_home'),
+ path('add/', view=views.FieldInstanceCreateView.as_view(), name='field_instances_add'),
+ path('edit//', view=views.FieldInstanceUpdateView.as_view(), name='field_instances_update'),
+ path('delete//', view=views.FieldInstanceDeleteView.as_view(), name='field_instances_delete'),
]
diff --git a/roundabout/field_instances/views.py b/roundabout/field_instances/views.py
new file mode 100644
index 000000000..315c236e4
--- /dev/null
+++ b/roundabout/field_instances/views.py
@@ -0,0 +1,90 @@
+"""
+# 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 django.shortcuts import render
+from django.conf import settings
+from django.urls import reverse, reverse_lazy
+from django.http import HttpResponse
+from django.views.generic import View, DetailView, ListView, RedirectView, UpdateView, CreateView, DeleteView, TemplateView, FormView
+from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
+
+
+from .requests import field_instance_sync_main
+from .models import *
+from .forms import *
+
+# Views to handle syncing requests
+# --------------------------------
+class FieldInstanceSyncToHomeView(View):
+
+ def get(self, request, *args, **kwargs):
+ # Get the FieldInstance object that is current
+ field_instance = FieldInstance.objects.filter(is_this_instance=True).first()
+ if not field_instance:
+ return HttpResponse('ERROR. This is not a Field Instance of RDB.')
+
+ sync_code = field_instance_sync_main(request, field_instance)
+ print(sync_code)
+
+ if sync_code == 200:
+ return HttpResponse("Code 200")
+ else:
+ return HttpResponse("API error")
+
+
+# Basic CBVs to handle CRUD operations
+# -----------------------------------
+
+class FieldInstanceListView(LoginRequiredMixin, ListView):
+ model = FieldInstance
+ template_name = 'field_instances/field_instance_list.html'
+ context_object_name = 'field_instances'
+
+
+class FieldInstanceCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
+ model = FieldInstance
+ form_class = FieldInstanceForm
+ template_name = 'field_instances/field_instance_form.html'
+ context_object_name = 'field_instance'
+ permission_required = 'field_instances.add_fieldinstance'
+ redirect_field_name = 'home'
+
+ def get_success_url(self):
+ return reverse('field_instances:field_instances_home', )
+
+
+class FieldInstanceUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
+ model = FieldInstance
+ form_class = FieldInstanceForm
+ template_name = 'field_instances/field_instance_form.html'
+ context_object_name = 'field_instance'
+ permission_required = 'field_instances.add_fieldinstance'
+ redirect_field_name = 'home'
+
+ def get_success_url(self):
+ return reverse('field_instances:field_instances_home', )
+
+
+class FieldInstanceDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
+ model = FieldInstance
+ success_url = reverse_lazy('field_instances:field_instances_home')
+ permission_required = 'field_instances.delete_fieldinstance'
+ redirect_field_name = 'home'
diff --git a/roundabout/inventory/api/serializers.py b/roundabout/inventory/api/serializers.py
index 4a2595be4..c94b2c45b 100644
--- a/roundabout/inventory/api/serializers.py
+++ b/roundabout/inventory/api/serializers.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
@@ -20,41 +20,86 @@
"""
from rest_framework import serializers
+from rest_flex_fields import FlexFieldsModelSerializer
-from ..models import Inventory
+from ..models import Inventory, Action, PhotoNote
from roundabout.locations.api.serializers import LocationSerializer
from roundabout.parts.api.serializers import PartSerializer
+from roundabout.calibrations.api.serializers import CalibrationEventSerializer
-class InventorySerializer(serializers.ModelSerializer):
- location = LocationSerializer(read_only=True)
- part = PartSerializer(read_only=True)
+class PhotoNoteSerializer(FlexFieldsModelSerializer):
+ class Meta:
+ model = PhotoNote
+ fields = ['id', 'photo', 'inventory', 'action', 'user']
+
+
+class ActionSerializer(FlexFieldsModelSerializer):
+ class Meta:
+ model = Action
+ fields = [
+ 'id', 'action_type', 'object_type', 'created_at', 'inventory', \
+ 'location', 'deployment', 'inventory_deployment', 'deployment_type', \
+ 'detail', 'user', 'build', 'parent', 'cruise', 'latitude', 'longitude', \
+ 'depth', 'calibration_event', 'const_default_event', 'config_event', \
+ 'config_default_event', 'photos',
+ ]
+
+ expandable_fields = {
+ 'location': LocationSerializer,
+ 'photos': (PhotoNoteSerializer, {'many': True}),
+ }
+
+
+
+class InventorySerializer(FlexFieldsModelSerializer):
custom_fields = serializers.SerializerMethodField('get_custom_fields')
class Meta:
model = Inventory
- fields = ['id', 'serial_number', 'part', 'location', 'custom_fields' ]
+ fields = [
+ 'id', 'serial_number', 'old_serial_number', 'part', 'location', 'revision', \
+ 'parent', 'children', 'build', 'assembly_part', 'assigned_destination_root', 'created_at', \
+ 'updated_at', 'detail', 'test_result', 'test_type', 'flag', 'time_at_sea', 'custom_fields',
+ 'calibration_events'
+ ]
+
+ expandable_fields = {
+ 'location': LocationSerializer,
+ 'part': PartSerializer,
+ 'parent': 'roundabout.inventory.api.serializers.InventorySerializer',
+ 'children': ('roundabout.inventory.api.serializers.InventorySerializer', {'many': True}),
+ 'calibration_events': (CalibrationEventSerializer, {'many': True}),
+ }
def get_custom_fields(self, obj):
# Get this item's custom fields with most recent Values
+ custom_fields = None
+
if obj.fieldvalues.exists():
obj_custom_fields = obj.fieldvalues.filter(is_current=True).select_related('field')
- else:
- obj_custom_fields = None
- # create initial empty dict
- custom_fields = {}
+ # create initial empty dict
+ custom_fields = {}
- if obj_custom_fields:
for field in obj_custom_fields:
custom_fields[field.field.field_name] = field.field_value
+ return custom_fields
+ else:
+ return custom_fields
+ def get_children(self, obj):
+ # Get this item's children
+ custom_fields = None
+ if obj.children.exists():
+ custom_fields = [child.id for child in obj.children.all()]
return custom_fields
- @staticmethod
- def setup_eager_loading(queryset):
- """ Perform necessary prefetching of data. """
- queryset = queryset.select_related('location').select_related('part')
- queryset = queryset.prefetch_related('fieldvalues')
- return queryset
+"""
+# Need a "sub-serializer" to handle self refernce MPTT tree structures
+class RecursiveField(serializers.Serializer):
+ def to_representation(self, value):
+ serializer = self.parent.parent.__class__(value, context=self.context)
+ return serializer.data
+"""
diff --git a/roundabout/inventory/api/views.py b/roundabout/inventory/api/views.py
index 98e23ce32..d6c22db7d 100644
--- a/roundabout/inventory/api/views.py
+++ b/roundabout/inventory/api/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
@@ -20,17 +20,38 @@
"""
from rest_framework import generics, viewsets, filters
-from ..models import Inventory
-from .serializers import InventorySerializer
+from rest_framework.permissions import IsAuthenticated
+from ..models import Inventory, Action, PhotoNote
+from .serializers import InventorySerializer, ActionSerializer, PhotoNoteSerializer
+
+
+class ActionViewSet(viewsets.ModelViewSet):
+ serializer_class = ActionSerializer
+ permission_classes = (IsAuthenticated,)
+ queryset = Action.objects.all()
class InventoryViewSet(viewsets.ModelViewSet):
serializer_class = InventorySerializer
- search_fields = ['serial_number']
- filter_backends = (filters.SearchFilter,)
+ permission_classes = (IsAuthenticated,)
+ queryset = Inventory.objects.all()
+ filterset_fields = ('serial_number',)
+
+
+class PhotoNoteViewSet(viewsets.ModelViewSet):
+ serializer_class = PhotoNoteSerializer
+ permission_classes = (IsAuthenticated,)
+ queryset = PhotoNote.objects.all()
+
+"""
+class InventoryFullTextViewSet(viewsets.ReadOnlyModelViewSet):
+ serializer_class = InventoryFullTextSerializer
+ #filter_backends = [DjangoFilterBackend]
+ filterset_fields = ['serial_number',]
def get_queryset(self):
queryset = Inventory.objects.all()
# Set up eager loading to avoid N+1 selects
queryset = self.get_serializer_class().setup_eager_loading(queryset)
return queryset
+"""
diff --git a/roundabout/inventory/migrations/0048_auto_20200801_1854.py b/roundabout/inventory/migrations/0048_auto_20200801_1854.py
new file mode 100644
index 000000000..63fc89587
--- /dev/null
+++ b/roundabout/inventory/migrations/0048_auto_20200801_1854.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.13 on 2020-08-01 18:54
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('inventory', '0047_auto_20200723_1620_squashed_0048_auto_20200727_1905'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='inventory',
+ name='test_result',
+ field=models.BooleanField(choices=[(None, '-'), (True, 'Pass'), (False, 'Fail')], null=True),
+ ),
+ ]
diff --git a/roundabout/inventory/migrations/0050_merge_20200819_1340.py b/roundabout/inventory/migrations/0050_merge_20200819_1340.py
new file mode 100644
index 000000000..893754bf9
--- /dev/null
+++ b/roundabout/inventory/migrations/0050_merge_20200819_1340.py
@@ -0,0 +1,14 @@
+# Generated by Django 2.2.13 on 2020-08-19 13:40
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('inventory', '0048_auto_20200801_1854'),
+ ('inventory', '0049_auto_20200817_2123'),
+ ]
+
+ operations = [
+ ]
diff --git a/roundabout/inventory/migrations/0053_merge_20200925_1748.py b/roundabout/inventory/migrations/0053_merge_20200925_1748.py
new file mode 100644
index 000000000..9984ac3e6
--- /dev/null
+++ b/roundabout/inventory/migrations/0053_merge_20200925_1748.py
@@ -0,0 +1,14 @@
+# Generated by Django 2.2.13 on 2020-09-25 17:48
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('inventory', '0052_auto_20200923_1919'),
+ ('inventory', '0050_merge_20200819_1340'),
+ ]
+
+ operations = [
+ ]
diff --git a/roundabout/inventory/migrations/0054_merge_20200928_2051.py b/roundabout/inventory/migrations/0054_merge_20200928_2051.py
new file mode 100644
index 000000000..964a7875a
--- /dev/null
+++ b/roundabout/inventory/migrations/0054_merge_20200928_2051.py
@@ -0,0 +1,14 @@
+# Generated by Django 2.2.13 on 2020-09-28 20:51
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('inventory', '0053_merge_20200925_1748'),
+ ('inventory', '0053_auto_20200928_1821'),
+ ]
+
+ operations = [
+ ]
diff --git a/roundabout/inventory/models.py b/roundabout/inventory/models.py
index 5fefabdf0..ddd88faad 100644
--- a/roundabout/inventory/models.py
+++ b/roundabout/inventory/models.py
@@ -84,13 +84,13 @@ class Inventory(MPTTModel):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
detail = models.TextField(blank=True)
- test_result = models.NullBooleanField(blank=False, choices=TEST_RESULTS)
+ test_result = models.BooleanField(null=True, blank=False, choices=TEST_RESULTS)
test_type = models.CharField(max_length=20, choices=TEST_TYPES, null=True, blank=True)
flag = models.BooleanField(choices=FLAG_TYPES, blank=False, default=False)
# Deprecated as of v1.5
_time_at_sea = models.DurationField(default=timedelta(minutes=0), null=True, blank=True)
- tracker = FieldTracker(fields=['location', 'parent', 'build'])
+ #tracker = FieldTracker(fields=['location', 'build'])
class MPTTMeta:
order_insertion_by = ['serial_number']
diff --git a/roundabout/inventory/views.py b/roundabout/inventory/views.py
index ffd569338..3266f0c71 100644
--- a/roundabout/inventory/views.py
+++ b/roundabout/inventory/views.py
@@ -525,7 +525,7 @@ def form_valid(self, form):
if default_value:
fieldvalue = FieldValue.objects.create(field=field, field_value=default_value.field_value,
- inventory=self.object, is_current=True, user=default_value.user)
+ inventory=self.object, is_current=True, user=default_value.user)
response = HttpResponseRedirect(self.get_success_url())
diff --git a/roundabout/locations/api/serializers.py b/roundabout/locations/api/serializers.py
index 6a84564e0..cad3dc709 100644
--- a/roundabout/locations/api/serializers.py
+++ b/roundabout/locations/api/serializers.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
@@ -20,10 +20,34 @@
"""
from rest_framework import serializers
+from rest_flex_fields import FlexFieldsModelSerializer
from ..models import Location
-class LocationSerializer(serializers.ModelSerializer):
+class LocationSerializer(serializers.HyperlinkedModelSerializer, FlexFieldsModelSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name='api_v1:locations-detail',
+ lookup_field='pk',
+ )
+ parent = serializers.HyperlinkedRelatedField(
+ view_name='api_v1:locations-detail',
+ lookup_field='pk',
+ queryset=Location.objects
+ )
+ children = serializers.HyperlinkedRelatedField(
+ view_name='api_v1:locations-detail',
+ many=True,
+ read_only=True,
+ lookup_field='pk',
+ )
+
class Meta:
model = Location
- fields = ('id', 'name', 'location_type', 'location_id', )
+ fields = ['id', 'url', 'name', 'parent', 'children', 'weight',
+ 'location_type', 'location_id', 'root_type', 'created_at',
+ ]
+
+ expandable_fields = {
+ 'parent': 'roundabout.locations.api.serializers.LocationSerializer',
+ 'children': ('roundabout.locations.api.serializers.LocationSerializer', {'many': True})
+ }
diff --git a/roundabout/locations/api/views.py b/roundabout/locations/api/views.py
index aec9c81a4..376c1c9c8 100644
--- a/roundabout/locations/api/views.py
+++ b/roundabout/locations/api/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
@@ -19,12 +19,14 @@
# If not, see .
"""
-from rest_framework import generics, viewsets, filters
+from rest_framework import generics, filters, viewsets
+from rest_framework.permissions import IsAuthenticated
+
from ..models import Location
from .serializers import LocationSerializer
class LocationViewSet(viewsets.ModelViewSet):
-
- queryset = Location.objects.all()
serializer_class = LocationSerializer
+ permission_classes = (IsAuthenticated,)
+ queryset = Location.objects.all()
diff --git a/roundabout/locations/migrations/0003_auto_20191023_1513.py b/roundabout/locations/migrations/0003_auto_20191023_1513.py
index a20237ed9..3979ee166 100644
--- a/roundabout/locations/migrations/0003_auto_20191023_1513.py
+++ b/roundabout/locations/migrations/0003_auto_20191023_1513.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
@@ -25,10 +25,18 @@
"""
from django.db import migrations
from django.apps import apps
+from mptt import register, managers
-Location = apps.get_model('locations', 'Location')
def create_root_locations(apps, schema_editor):
+ Location = apps.get_model('locations', 'Location')
+
+ manager = managers.TreeManager()
+ manager.model = Location
+
+ register(Location)
+ manager.contribute_to_class(Location, 'objects')
+
land, created = Location.objects.get_or_create(name='Land')
if created:
land.root_type = 'Land'
diff --git a/roundabout/locations/migrations/0004_auto_20200821_1809.py b/roundabout/locations/migrations/0004_auto_20200821_1809.py
new file mode 100644
index 000000000..0c83f8dba
--- /dev/null
+++ b/roundabout/locations/migrations/0004_auto_20200821_1809.py
@@ -0,0 +1,24 @@
+# Generated by Django 2.2.13 on 2020-08-21 18:09
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('locations', '0003_auto_20191023_1513'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='location',
+ name='created_at',
+ field=models.DateTimeField(default=django.utils.timezone.now),
+ ),
+ migrations.AddField(
+ model_name='location',
+ name='updated_at',
+ field=models.DateTimeField(auto_now=True),
+ ),
+ ]
diff --git a/roundabout/locations/models.py b/roundabout/locations/models.py
index 30c986efb..25432e99d 100644
--- a/roundabout/locations/models.py
+++ b/roundabout/locations/models.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
@@ -20,6 +20,7 @@
"""
from django.db import models
+from django.utils import timezone
from django.urls import reverse
from mptt.models import MPTTModel, TreeForeignKey
@@ -47,6 +48,8 @@ class Location(MPTTModel):
location_id = models.CharField(max_length=100, blank=True)
weight = models.IntegerField(default=0, blank=True, null=True)
root_type = models.CharField(max_length=20, choices=ROOT_TYPES, blank=True)
+ created_at = models.DateTimeField(default=timezone.now)
+ updated_at = models.DateTimeField(auto_now=True)
class MPTTMeta:
order_insertion_by = ['weight', 'name']
diff --git a/roundabout/parts/api/serializers.py b/roundabout/parts/api/serializers.py
index 745cfa3e8..a1ed6c2c2 100644
--- a/roundabout/parts/api/serializers.py
+++ b/roundabout/parts/api/serializers.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
@@ -20,10 +20,146 @@
"""
from rest_framework import serializers
-from ..models import Part, PartType
+from rest_flex_fields import FlexFieldsModelSerializer
+from ..models import Part, PartType, Revision, Documentation
+API_VERSION = 'api_v1'
+
+class PartTypeSerializer(FlexFieldsModelSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name = API_VERSION + ':part-templates/part-types-detail',
+ lookup_field = 'pk',
+ )
+ parent = serializers.HyperlinkedRelatedField(
+ view_name = API_VERSION + ':part-templates/part-types-detail',
+ lookup_field = 'pk',
+ queryset = PartType.objects
+ )
+ children = serializers.HyperlinkedRelatedField(
+ view_name = API_VERSION + ':part-templates/part-types-detail',
+ many = True,
+ read_only = True,
+ lookup_field = 'pk',
+ )
+ parts = serializers.HyperlinkedRelatedField(
+ view_name = API_VERSION +':part-templates/parts-detail',
+ many = True,
+ read_only = True,
+ lookup_field = 'pk',
+ )
+
+ class Meta:
+ model = PartType
+ fields = ['id', 'url', 'name', 'parent', 'children', 'parts',]
+
+ expandable_fields = {
+ 'parent': 'roundabout.parts.api.serializers.PartTypeSerializer',
+ 'children': ('roundabout.parts.api.serializers.PartTypeSerializer', {'many': True}),
+ 'parts': ('roundabout.userdefinedfields.api.serializers', {'many': True})
+ }
+
+
+class PartSerializer(FlexFieldsModelSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name = API_VERSION + ':part-templates/parts-detail',
+ lookup_field = 'pk',
+ )
+ part_type = serializers.HyperlinkedRelatedField(
+ view_name = API_VERSION + ':part-templates/part-types-detail',
+ lookup_field = 'pk',
+ queryset = PartType.objects
+ )
+ revisions= serializers.HyperlinkedRelatedField(
+ view_name = API_VERSION + ':part-templates/revisions-detail',
+ lookup_field = 'pk',
+ many = True,
+ read_only = True,
+ )
+ user_defined_fields= serializers.HyperlinkedRelatedField(
+ view_name = API_VERSION + ':user-defined-fields/fields-detail',
+ lookup_field = 'pk',
+ many = True,
+ read_only = True,
+ )
-class PartSerializer(serializers.ModelSerializer):
class Meta:
model = Part
- fields = ('id', 'name', 'friendly_name', 'part_number', 'unit_cost', 'refurbishment_cost' )
+ fields = [
+ 'id',
+ 'url',
+ 'name',
+ 'revisions',
+ 'part_type',
+ 'friendly_name',
+ 'part_number',
+ 'unit_cost',
+ 'refurbishment_cost',
+ 'note',
+ 'user_defined_fields',
+ 'cal_dec_places',
+ ]
+
+ expandable_fields = {
+ 'part_type': 'roundabout.parts.api.serializers.PartTypeSerializer',
+ 'revisions': ('roundabout.parts.api.serializers.RevisionSerializer', {'many': True}),
+ 'user_defined_fields': ('roundabout.userdefinedfields.api.serializers.FieldSerializer', {'many': True}),
+ }
+
+
+class RevisionSerializer(FlexFieldsModelSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name = API_VERSION + ':part-templates/revisions-detail',
+ lookup_field = 'pk',
+ )
+ part = serializers.HyperlinkedRelatedField(
+ view_name = API_VERSION + ':part-templates/parts-detail',
+ lookup_field = 'pk',
+ queryset = PartType.objects
+ )
+ documentation= serializers.HyperlinkedRelatedField(
+ view_name = API_VERSION + ':part-templates/documents-detail',
+ lookup_field = 'pk',
+ many = True,
+ read_only = True,
+ )
+
+ class Meta:
+ model = Revision
+ fields = [
+ 'id',
+ 'url',
+ 'revision_code',
+ 'part',
+ 'unit_cost',
+ 'refurbishment_cost',
+ 'note',
+ 'created_at',
+ 'documentation',
+ ]
+
+ expandable_fields = {
+ 'part': 'roundabout.parts.api.serializers.PartSerializer',
+ 'documentation': ('roundabout.parts.api.serializers.DocumentationSerializer', {'many': True})
+ }
+
+
+class DocumentationSerializer(FlexFieldsModelSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name = API_VERSION + ':part-templates/documents-detail',
+ lookup_field = 'pk',
+ )
+ revision = serializers.HyperlinkedRelatedField(
+ view_name = API_VERSION + ':part-templates/revisions-detail',
+ lookup_field = 'pk',
+ queryset = Revision.objects
+ )
+
+ class Meta:
+ model = Documentation
+ fields = [
+ 'id', 'url', 'name', 'doc_type', 'doc_link', 'revision',
+ ]
+
+ expandable_fields = {
+ 'revision': 'roundabout.parts.api.serializers.RevisionSerializer',
+ }
diff --git a/roundabout/parts/api/views.py b/roundabout/parts/api/views.py
index 11f911cea..916cd41ce 100644
--- a/roundabout/parts/api/views.py
+++ b/roundabout/parts/api/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
@@ -20,13 +20,30 @@
"""
from rest_framework import generics, viewsets, filters
-from ..models import Part
-from .serializers import PartSerializer
+from rest_framework.permissions import IsAuthenticated
+from ..models import Part, PartType, Revision, Documentation
+from .serializers import PartSerializer, PartTypeSerializer, RevisionSerializer, DocumentationSerializer
-class PartViewSet(viewsets.ModelViewSet):
+class PartTypeViewSet(viewsets.ModelViewSet):
+ serializer_class = PartTypeSerializer
+ permission_classes = (IsAuthenticated,)
+ queryset = PartType.objects.all()
- queryset = Part.objects.all()
+
+class PartViewSet(viewsets.ModelViewSet):
serializer_class = PartSerializer
- search_fields = ['part_number']
- filter_backends = (filters.SearchFilter,)
+ permission_classes = (IsAuthenticated,)
+ queryset = Part.objects.all()
+
+
+class RevisionViewSet(viewsets.ModelViewSet):
+ serializer_class = RevisionSerializer
+ permission_classes = (IsAuthenticated,)
+ queryset = Revision.objects.all()
+
+
+class DocumentationViewSet(viewsets.ModelViewSet):
+ serializer_class = DocumentationSerializer
+ permission_classes = (IsAuthenticated,)
+ queryset = Documentation.objects.all()
diff --git a/roundabout/parts/forms.py b/roundabout/parts/forms.py
index 92a4c8b20..b278757df 100644
--- a/roundabout/parts/forms.py
+++ b/roundabout/parts/forms.py
@@ -178,7 +178,8 @@ class PartTypeForm(forms.ModelForm):
class Meta:
model = PartType
- fields = ['name', 'parent' ]
+ fields = ['name', 'parent', 'ccc_toggle' ]
labels = {
- 'name': 'Part Type Name'
- }
+ 'name': 'Part Type Name',
+ 'ccc_toggle': 'Enable Configs, Constants, and Calibration Coefficients'
+ }
diff --git a/roundabout/parts/migrations/0004_auto_20191023_1735.py b/roundabout/parts/migrations/0004_auto_20191023_1735.py
index be301f017..0fd623500 100644
--- a/roundabout/parts/migrations/0004_auto_20191023_1735.py
+++ b/roundabout/parts/migrations/0004_auto_20191023_1735.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
@@ -25,14 +25,23 @@
"""
from django.db import migrations
from django.apps import apps
+from mptt import register, managers
-PartType = apps.get_model('parts', 'PartType')
def create_part_types(apps, schema_editor):
+ PartType = apps.get_model('parts', 'PartType')
part_types = ['Cable', 'Electrical', 'Instrument', 'Mechanical']
+ manager = managers.TreeManager()
+ manager.model = PartType
+
+ register(PartType)
+ manager.contribute_to_class(PartType, 'objects')
+
for part_type in part_types:
- obj, created = PartType.objects.get_or_create(name=part_type)
+ obj = PartType.objects.create(
+ name=part_type,
+ )
class Migration(migrations.Migration):
diff --git a/roundabout/parts/migrations/0009_parttype_ccc_toggle.py b/roundabout/parts/migrations/0009_parttype_ccc_toggle.py
new file mode 100644
index 000000000..e5b923a45
--- /dev/null
+++ b/roundabout/parts/migrations/0009_parttype_ccc_toggle.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.13 on 2020-09-30 13:47
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('parts', '0008_merge_20200617_1450'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='parttype',
+ name='ccc_toggle',
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/roundabout/parts/migrations/0010_merge_20201015_1353.py b/roundabout/parts/migrations/0010_merge_20201015_1353.py
new file mode 100644
index 000000000..4dc8d5ce5
--- /dev/null
+++ b/roundabout/parts/migrations/0010_merge_20201015_1353.py
@@ -0,0 +1,14 @@
+# Generated by Django 2.2.13 on 2020-10-15 13:53
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('parts', '0009_auto_20201014_2007'),
+ ('parts', '0009_parttype_ccc_toggle'),
+ ]
+
+ operations = [
+ ]
diff --git a/roundabout/parts/migrations/0010_merge_20201015_2202.py b/roundabout/parts/migrations/0010_merge_20201015_2202.py
new file mode 100644
index 000000000..e9a877e1f
--- /dev/null
+++ b/roundabout/parts/migrations/0010_merge_20201015_2202.py
@@ -0,0 +1,14 @@
+# Generated by Django 2.2.13 on 2020-10-15 22:02
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('parts', '0009_auto_20201014_2007'),
+ ('parts', '0009_parttype_ccc_toggle'),
+ ]
+
+ operations = [
+ ]
diff --git a/roundabout/parts/migrations/0011_merge_20201105_1645.py b/roundabout/parts/migrations/0011_merge_20201105_1645.py
new file mode 100644
index 000000000..1402ad4a3
--- /dev/null
+++ b/roundabout/parts/migrations/0011_merge_20201105_1645.py
@@ -0,0 +1,14 @@
+# Generated by Django 2.2.13 on 2020-11-05 16:45
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('parts', '0010_merge_20201015_1353'),
+ ('parts', '0010_merge_20201015_2202'),
+ ]
+
+ operations = [
+ ]
diff --git a/roundabout/parts/models.py b/roundabout/parts/models.py
index fdb480264..21ae2c406 100644
--- a/roundabout/parts/models.py
+++ b/roundabout/parts/models.py
@@ -34,6 +34,7 @@
class PartType(MPTTModel):
name = models.CharField(max_length=255, unique=False)
+ ccc_toggle = models.BooleanField(null=False, default=False)
parent = TreeForeignKey('self', null=True, blank=True, related_name='children', on_delete=models.SET_NULL)
class MPTTMeta:
diff --git a/roundabout/parts/urls.py b/roundabout/parts/urls.py
index 4509b52f9..49203358d 100644
--- a/roundabout/parts/urls.py
+++ b/roundabout/parts/urls.py
@@ -53,4 +53,5 @@
path('part_type/add/', view=views.PartsTypeCreateView.as_view(), name='parts_type_add'),
path('part_type/edit//', view=views.PartsTypeUpdateView.as_view(), name='parts_type_update'),
path('part_type/delete//', view=views.PartsTypeDeleteView.as_view(), name='parts_type_delete'),
+ path('part_type/check_ccc_enabled/', views.check_ccc_enabled, name='check_ccc_enabled'),
]
diff --git a/roundabout/parts/views.py b/roundabout/parts/views.py
index 6b507135c..909ae811c 100644
--- a/roundabout/parts/views.py
+++ b/roundabout/parts/views.py
@@ -91,6 +91,18 @@ def validate_part_number(request):
return JsonResponse(data)
+# Function to check if CCC Names are enabled for Part Type
+def check_ccc_enabled(request):
+ part_type_id = request.GET.get('part_type_id')
+ part_type = PartType.objects.get(id=part_type_id)
+
+
+ data = {
+ 'ccc_toggle': part_type.ccc_toggle,
+ }
+ return JsonResponse(data)
+
+
# Part Template Views
def load_parts_navtree(request):
diff --git a/roundabout/templates/calibrations/cal_name_detail.html b/roundabout/templates/calibrations/cal_name_detail.html
index 35ab60f05..b2236430e 100644
--- a/roundabout/templates/calibrations/cal_name_detail.html
+++ b/roundabout/templates/calibrations/cal_name_detail.html
@@ -132,6 +132,9 @@ Calibrations
Expand/Collapse Default
+ {% if name.deprecated %}
+ Deprecated
+ {% endif %}
@@ -212,8 +215,8 @@ Calibrations
// Toggle display of Constant functionality
let user_id = '{{ user.id }}'
- let inv_part_type = '{{ part_template.part_type }}';
- if (inv_part_type == 'Instrument') {
+ let ccc_toggle = '{{ part_template.part_type.ccc_toggle }}';
+ if (ccc_toggle == 'True') {
$('#calibrations-section').show();
} else {
$('#calibrations-section').hide();
diff --git a/roundabout/templates/calibrations/events_detail.html b/roundabout/templates/calibrations/events_detail.html
index f77278f02..92c897740 100644
--- a/roundabout/templates/calibrations/events_detail.html
+++ b/roundabout/templates/calibrations/events_detail.html
@@ -22,7 +22,7 @@
- {% for event in coeff_events %}
+ {% for event in inventory_item.calibration_events.all %}
-
.
-->
@@ -68,6 +84,7 @@
Calibration(s)
Name |
Type |
Significant Figures |
+ Deprecated |
{{ part_calname_form.management_form }}
@@ -76,6 +93,7 @@ Calibration(s)
{% if cal.id %}{{ cal.DELETE }}{% endif %} {{ cal.id }} {{ cal.calibration_name }} |
{% if cal.id %}{{ cal.DELETE }}{% endif %} {{ cal.id }} {{ cal.value_set_type }} |
{% if cal.id %}{{ cal.DELETE }}{% endif %} {{ cal.id }} {{ cal.sigfig_override }} {{cal.sigfig_override.help_text}} |
+ {% if cal.id %}{{ cal.DELETE }}{% endif %} {{ cal.id }} {{ cal.deprecated }} |
{% endfor %}
diff --git a/roundabout/templates/configs_constants/config_default_detail.html b/roundabout/templates/configs_constants/config_default_detail.html
index b8f793eed..3ace49706 100644
--- a/roundabout/templates/configs_constants/config_default_detail.html
+++ b/roundabout/templates/configs_constants/config_default_detail.html
@@ -206,9 +206,9 @@
// Toggle display of Constant functionality
let user_id = '{{ user.id }}'
- let inv_part_type = '{{ assembly_part.part.part_type }}';
+ let ccc_toggle = '{{ assembly_part.part.part_type.ccc_toggle }}';
let part_has_configs = '{{ part_has_configs }}';
- if ((inv_part_type == 'Instrument' || inv_part_type == 'Electrical') && part_has_configs == 'True') {
+ if (ccc_toggle == 'True' && part_has_configs == 'True') {
$('#add_configdefault_action').show();
$('#config-tabs-nav').show();
$('#config_default-template-tab').show().css('display', '');
diff --git a/roundabout/templates/configs_constants/config_name_detail.html b/roundabout/templates/configs_constants/config_name_detail.html
index 092eaa651..1feb66b21 100644
--- a/roundabout/templates/configs_constants/config_name_detail.html
+++ b/roundabout/templates/configs_constants/config_name_detail.html
@@ -132,6 +132,9 @@ Configurations / Constants
Expand/Collapse Default
+ {% if name.deprecated %}
+ Deprecated
+ {% endif %}
@@ -212,8 +215,8 @@ Configurations / Constants
// // Toggle display of Constant functionality
let user_id = '{{ user.id }}'
- let inv_part_type = '{{ part_template.part_type }}';
- if (inv_part_type == 'Instrument' || inv_part_type == 'Electrical') {
+ let ccc_toggle = '{{ part_template.part_type.ccc_toggle }}';
+ if (ccc_toggle == 'True') {
$('#configuration_constant-section').show();
} else {
$('#configuration_constant-section').hide();
diff --git a/roundabout/templates/configs_constants/configs_detail.html b/roundabout/templates/configs_constants/configs_detail.html
index 64cc3c1f5..9b7ec896e 100644
--- a/roundabout/templates/configs_constants/configs_detail.html
+++ b/roundabout/templates/configs_constants/configs_detail.html
@@ -215,16 +215,6 @@
let user_id = '{{ user.id }}';
let part_has_configs = '{{ part_has_configs }}';
- // if (inv_part_type == 'Instrument' && deployment != 'None' && deployment.length > 0 && part_has_configs == 'True') {
- // $('#add_conf_action').show();
- // $('#conf-template-tab').show().css('display', '');
- // $('#conf-template').show().css('display', '');
- // } else {
- // $('#add_conf_action').hide();
- // $('#conf-template-tab').hide();
- // $('#conf-template').hide();
- // }
-
$('#conf-template').on('click', 'button[id^=expander]', function(e) {
e.preventDefault();
let url = $(this).attr('data-reviewer-url');
diff --git a/roundabout/templates/configs_constants/const_default_detail.html b/roundabout/templates/configs_constants/const_default_detail.html
index f21def78a..749876d1f 100644
--- a/roundabout/templates/configs_constants/const_default_detail.html
+++ b/roundabout/templates/configs_constants/const_default_detail.html
@@ -208,15 +208,6 @@
let user_id = '{{ user.id }}'
let inv_part_type = '{{ inventory_item.part.part_type }}';
let part_has_consts = '{{ part_has_consts }}';
- // if (inv_part_type == 'Instrument' && part_has_consts == 'True') {
- // $('#add_constdefault_action').show();
- // $('#const_default-template-tab').show().css('display', '');
- // $('#const_default-template').show().css('display', '');
- // } else {
- // $('#add_constdefault_action').hide();
- // $('#const_default-template-tab').hide();
- // $('#const_default-template').hide();
- // }
// Swap Reviewers as Approvers
$('#const_default-template').on('click', 'button[id^=expander]', function(e) {
diff --git a/roundabout/templates/configs_constants/constants_detail.html b/roundabout/templates/configs_constants/constants_detail.html
index 8baca6e41..2e066016c 100644
--- a/roundabout/templates/configs_constants/constants_detail.html
+++ b/roundabout/templates/configs_constants/constants_detail.html
@@ -215,16 +215,6 @@
let user_id = '{{ user.id }}';
let part_has_consts = '{{ part_has_consts }}';
- // if (inv_part_type == 'Instrument' && deployment != 'None' && deployment.length > 0 && part_has_consts == 'True') {
- // $('#add_const_action').show();
- // $('#const-template-tab').show().css('display', '');
- // $('#const-template').show().css('display', '');
- // } else {
- // $('#add_const_action').hide();
- // $('#const-template-tab').hide();
- // $('#const-template').hide();
- // }
-
$('#const-template').on('click', 'button[id^=expander]', function(e) {
e.preventDefault();
let url = $(this).attr('data-reviewer-url');
diff --git a/roundabout/templates/configs_constants/part_confname_form.html b/roundabout/templates/configs_constants/part_confname_form.html
index bd4e8750a..e2b4305f5 100644
--- a/roundabout/templates/configs_constants/part_confname_form.html
+++ b/roundabout/templates/configs_constants/part_confname_form.html
@@ -19,13 +19,28 @@
# If not, see .
-->
{% load static i18n %}
@@ -69,6 +84,7 @@ Configurations/Constants
Name |
Type |
Export with Calibrations |
+ Deprecated |
{{ part_confname_form.management_form }}
@@ -77,7 +93,7 @@ Configurations/Constants
{% if conf.id %}{{ conf.DELETE }}{% endif %} {{ conf.id }} {{ conf.name }} |
{% if conf.id %}{{ conf.DELETE }}{% endif %} {{ conf.id }} {{ conf.config_type }} |
{% if conf.id %}{{ conf.DELETE }}{% endif %} {{ conf.id }} {{ conf.include_with_calibrations }} |
- |
+ {% if conf.id %}{{ conf.DELETE }}{% endif %} {{ conf.id }} {{ conf.deprecated }} |
{% endfor %}
diff --git a/roundabout/templates/field_instances/field_instance_form.html b/roundabout/templates/field_instances/field_instance_form.html
new file mode 100644
index 000000000..840ebfd98
--- /dev/null
+++ b/roundabout/templates/field_instances/field_instance_form.html
@@ -0,0 +1,48 @@
+
+
+{% extends "base.html" %}
+{% load crispy_forms_tags %}
+
+{% block title %}Field Instance - {{ field_instance.name }}{% endblock %}
+
+{% block content %}
+
+
{% if field_instance.name %}Edit {{ field_instance.name }}{% else %}Add Field Instance {% endif %}
+
+
+{% endblock %}
diff --git a/roundabout/templates/field_instances/field_instance_list.html b/roundabout/templates/field_instances/field_instance_list.html
new file mode 100644
index 000000000..a8d3b3930
--- /dev/null
+++ b/roundabout/templates/field_instances/field_instance_list.html
@@ -0,0 +1,72 @@
+
+
+{% extends "base.html" %}
+{% load static i18n %}
+{% block title %}Field Instances{% endblock %}
+
+{% block content %}
+
+
+
+
Field Instances
+
+
All currently registered Field Instances. You need to create a Field Instance on the "Home Base" RDB site
+ before registering a new computer for deployment
+
+
+
+ Name |
+ Start Date |
+ End Date |
+ Users |
+ Cruise |
+ Notes |
+ |
+
+
+
+ {% for field_instance in field_instances %}
+
+ {{ field_instance.name }} |
+ {{ field_instance.start_date }} |
+ {{ field_instance.end_date }} |
+
+
+ {% for user in field_instance.users.all %}
+ - {{ user.name }}
+ {% endfor %}
+
+ |
+ {{ field_instance.cruise }} |
+ {{ field_instance.notes }} |
+
+ Edit
+ Delete
+ |
+
+ {% endfor %}
+
+
+
+{% endblock content %}
diff --git a/roundabout/templates/inventory/ajax_inventory_detail.html b/roundabout/templates/inventory/ajax_inventory_detail.html
index afbe21e6a..3dfa27863 100644
--- a/roundabout/templates/inventory/ajax_inventory_detail.html
+++ b/roundabout/templates/inventory/ajax_inventory_detail.html
@@ -154,7 +154,7 @@
data-node-type="{{ node_type }}">Edit Inventory Details
{% if not user|has_group:"inventory only" %}
- {% if inventory_item.part.coefficient_name_events.exists %}
+ {% if inventory_item.part.coefficient_name_events.exists and inventory_item.part.part_type.ccc_toggle %}
Add Calibration Coefficients
{% endif %}
- {% if not inventory_item.constant_default_events.exists and part_has_consts %}
+ {% if not inventory_item.constant_default_events.exists and part_has_consts and inventory_item.part.part_type.ccc_toggle %}
Add Constant Defaults
{% endif %}
- {% if part_has_consts and inventory_item.build.current_deployment %}
+ {% if part_has_consts and inventory_item.build.current_deployment and inventory_item.part.part_type.ccc_toggle %}
Add Constants
{% endif %}
- {% if part_has_configs and inventory_item.build.current_deployment %}
+ {% if part_has_configs and inventory_item.build.current_deployment and inventory_item.part.part_type.ccc_toggle %}
Part Specs
- {% if inventory_item.calibration_events.exists %}
+ {% if inventory_item.calibration_events.exists and inventory_item.part.part_type.ccc_toggle %}
Calibration Coefficient History
{% endif %}
- {% if inventory_item.constant_default_events.exists %}
+ {% if inventory_item.constant_default_events.exists and inventory_item.part.part_type.ccc_toggle %}
Constant Defaults
{% endif %}
- {% if inventory_item.config_events.exists %}
+ {% if inventory_item.config_events.exists and inventory_item.part.part_type.ccc_toggle %}
Constant History
diff --git a/roundabout/templates/parts/ajax_part_detail.html b/roundabout/templates/parts/ajax_part_detail.html
index ed48e0088..498b5608a 100644
--- a/roundabout/templates/parts/ajax_part_detail.html
+++ b/roundabout/templates/parts/ajax_part_detail.html
@@ -288,16 +288,12 @@ Custom Fields
});
});
- let partType = '{{part_template.part_type}}';
- if (partType == 'Instrument') {
+ let ccc_toggle = '{{part_template.part_type.ccc_toggle}}';
+ if (ccc_toggle == 'True') {
$('#calibrations-section').show();
- } else {
- $('#calibrations-section').hide();
- }
-
- if (partType == 'Instrument' || partType == 'Electrical') {
$('#configuration_constant-section').show();
} else {
+ $('#calibrations-section').hide();
$('#configuration_constant-section').hide();
}
diff --git a/roundabout/templates/parts/ajax_part_form.html b/roundabout/templates/parts/ajax_part_form.html
index a2558e11f..667a1d44b 100644
--- a/roundabout/templates/parts/ajax_part_form.html
+++ b/roundabout/templates/parts/ajax_part_form.html
@@ -214,8 +214,8 @@ Revision Documentation
})
- const part_type = '{{part_template.part_type}}'
- if (part_type == 'Instrument') {
+ const ccc_toggle = '{{part_template.part_type.ccc_toggle}}'
+ if (ccc_toggle == 'True') {
$('#div_id_cal_dec_places').show();
} else {
$('#div_id_cal_dec_places').hide();
@@ -223,13 +223,22 @@ Revision Documentation
// On-change of dropdown selection, obtain part type name, show/hide max-cal attribute
$('#id_part_type').change(function() {
- let part_type = $(this).find('option:selected').text();
- if (part_type == ' Instrument') {
- $('#div_id_cal_dec_places').show();
- } else {
- $('#div_id_cal_dec_places').hide();
- }
- })
+ let part_type_id = $(this).find('option:selected').attr('value');
+ let url = "{% url 'parts:check_ccc_enabled' %}"
+ $.ajax({
+ url: url,
+ data: {
+ "part_type_id": part_type_id,
+ },
+ success: function (data) {
+ if ( data.ccc_toggle ) {
+ $('#div_id_cal_dec_places').show();
+ } else {
+ $('#div_id_cal_dec_places').hide();
+ }
+ }
+ });
+ });
});
diff --git a/roundabout/templates/parts/part_type_list.html b/roundabout/templates/parts/part_type_list.html
index 6389efd5a..e35a83aee 100644
--- a/roundabout/templates/parts/part_type_list.html
+++ b/roundabout/templates/parts/part_type_list.html
@@ -36,6 +36,7 @@ Part Types
Name/description |
+ Configs, Constants, and Calibrations Enabled |
|
@@ -47,6 +48,7 @@ Part Types
{% else %}
{{ part_type.name }} |
{% endif %}
+ {{ part_type.ccc_toggle }} |
Edit
Delete
diff --git a/roundabout/userdefinedfields/api/serializers.py b/roundabout/userdefinedfields/api/serializers.py
new file mode 100644
index 000000000..f70d76cf6
--- /dev/null
+++ b/roundabout/userdefinedfields/api/serializers.py
@@ -0,0 +1,45 @@
+"""
+# 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 rest_framework import serializers
+from rest_flex_fields import FlexFieldsModelSerializer
+
+from ..models import *
+from roundabout.parts.api.serializers import PartSerializer
+from roundabout.inventory.api.serializers import InventorySerializer
+
+
+class FieldSerializer(FlexFieldsModelSerializer):
+ class Meta:
+ model = Field
+ fields = '__all__'
+
+
+class FieldValueSerializer(FlexFieldsModelSerializer):
+ class Meta:
+ model = FieldValue
+ fields = '__all__'
+
+ expandable_fields = {
+ 'field': FieldSerializer,
+ 'part': PartSerializer,
+ 'inventory': (InventorySerializer, {'many': True})
+ }
diff --git a/roundabout/assemblies/api/urls.py b/roundabout/userdefinedfields/api/views.py
similarity index 61%
rename from roundabout/assemblies/api/urls.py
rename to roundabout/userdefinedfields/api/views.py
index 149dbd28a..5ac140f3d 100644
--- a/roundabout/assemblies/api/urls.py
+++ b/roundabout/userdefinedfields/api/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
@@ -19,14 +19,20 @@
# If not, see .
"""
-from django.urls import path, include
-from rest_framework.routers import DefaultRouter, SimpleRouter
-from .views import AssemblyViewSet
+from rest_framework import generics, filters, viewsets
+from rest_framework.permissions import IsAuthenticated
-# Create a router and register our viewsets with it.
-router = SimpleRouter()
-router.register(r'assemblies', AssemblyViewSet, 'assemblies' )
+from ..models import *
+from .serializers import FieldSerializer, FieldValueSerializer
-urlpatterns = [
- path('', include(router.urls) ),
-]
+
+class FieldViewSet(viewsets.ModelViewSet):
+ queryset = Field.objects.all()
+ serializer_class = FieldSerializer
+ permission_classes = (IsAuthenticated,)
+
+
+class FieldValueViewSet(viewsets.ModelViewSet):
+ queryset = FieldValue.objects.all()
+ serializer_class = FieldValueSerializer
+ permission_classes = (IsAuthenticated,)
diff --git a/roundabout/userdefinedfields/migrations/0006_auto_20200820_1325.py b/roundabout/userdefinedfields/migrations/0006_auto_20200820_1325.py
new file mode 100644
index 000000000..ecb4ed8d5
--- /dev/null
+++ b/roundabout/userdefinedfields/migrations/0006_auto_20200820_1325.py
@@ -0,0 +1,24 @@
+# Generated by Django 2.2.13 on 2020-08-20 13:25
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('userdefinedfields', '0005_auto_20200517_1814_squashed_0006_auto_20200517_1822'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='field',
+ name='created_at',
+ field=models.DateTimeField(default=django.utils.timezone.now),
+ ),
+ migrations.AddField(
+ model_name='field',
+ name='updated_at',
+ field=models.DateTimeField(auto_now=True),
+ ),
+ ]
diff --git a/roundabout/userdefinedfields/migrations/0007_fieldvalue_updated_at.py b/roundabout/userdefinedfields/migrations/0007_fieldvalue_updated_at.py
new file mode 100644
index 000000000..327ef49e4
--- /dev/null
+++ b/roundabout/userdefinedfields/migrations/0007_fieldvalue_updated_at.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.13 on 2020-08-20 15:19
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('userdefinedfields', '0006_auto_20200820_1325'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='fieldvalue',
+ name='updated_at',
+ field=models.DateTimeField(auto_now=True),
+ ),
+ ]
diff --git a/roundabout/userdefinedfields/models.py b/roundabout/userdefinedfields/models.py
index 48267add3..d62b0b967 100644
--- a/roundabout/userdefinedfields/models.py
+++ b/roundabout/userdefinedfields/models.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 dateutil import parser
from django.db import models
+from django.utils import timezone
from django.contrib.postgres.fields import JSONField
# Create your models here.
@@ -40,6 +41,8 @@ class Field(models.Model):
field_default_value = models.CharField(max_length=255, null=False, blank=True)
choice_field_options = JSONField(null=True, blank=True)
global_for_part_types = models.ManyToManyField('parts.PartType', blank=True, related_name='custom_fields')
+ created_at = models.DateTimeField(default=timezone.now)
+ updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ('field_name',)
@@ -57,6 +60,7 @@ class FieldValue(models.Model):
part = models.ForeignKey('parts.Part', related_name='fieldvalues',
on_delete=models.CASCADE, null=True)
created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
user = models.ForeignKey('users.User', related_name='fieldvalues',
on_delete=models.SET_NULL, null=True, blank=False)
is_current = models.BooleanField(default=False)
diff --git a/roundabout/users/migrations/0002_auto_20191022_1510.py b/roundabout/users/migrations/0002_auto_20191022_1510.py
index ba0519bc6..50bcb53b0 100644
--- a/roundabout/users/migrations/0002_auto_20191022_1510.py
+++ b/roundabout/users/migrations/0002_auto_20191022_1510.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
@@ -26,21 +26,27 @@
from django.apps import apps
from django.contrib.auth import get_user_model
from django.contrib.auth.management import create_permissions
+from django.contrib.auth.hashers import make_password
-User = get_user_model()
-Group = apps.get_model('auth','Group')
env = environ.Env()
def generate_superuser(apps, schema_editor):
+ User = apps.get_model('users', 'User')
+ Group = apps.get_model('auth','Group')
+
DJANGO_SU_NAME = env('DJANGO_SU_NAME')
DJANGO_SU_EMAIL = env('DJANGO_SU_EMAIL')
- DJANGO_SU_PASSWORD = env('DJANGO_SU_PASSWORD')
+ DJANGO_SU_PASSWORD = make_password(env('DJANGO_SU_PASSWORD'))
- superuser = User.objects.create_superuser(
+ superuser = User.objects.create(
username=DJANGO_SU_NAME,
email=DJANGO_SU_EMAIL,
- password=DJANGO_SU_PASSWORD)
+ password=DJANGO_SU_PASSWORD,
+ is_superuser=True,
+ is_staff=True,
+ is_active=True,
+ )
superuser.save()
diff --git a/roundabout/users/migrations/0004_user_is_infield.py b/roundabout/users/migrations/0004_user_is_infield.py
new file mode 100644
index 000000000..dc5358ce8
--- /dev/null
+++ b/roundabout/users/migrations/0004_user_is_infield.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.13 on 2020-07-31 19:57
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0003_auto_20191023_1506'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='user',
+ name='is_infield',
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/roundabout/users/migrations/0005_auto_20200819_1355.py b/roundabout/users/migrations/0005_auto_20200819_1355.py
new file mode 100644
index 000000000..6d70672f5
--- /dev/null
+++ b/roundabout/users/migrations/0005_auto_20200819_1355.py
@@ -0,0 +1,17 @@
+# Generated by Django 2.2.13 on 2020-08-19 13:55
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0004_user_is_infield'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='user',
+ options={'ordering': ['username']},
+ ),
+ ]
diff --git a/roundabout/users/models.py b/roundabout/users/models.py
index ae9caff0a..361aa1f63 100644
--- a/roundabout/users/models.py
+++ b/roundabout/users/models.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
@@ -18,18 +18,20 @@
# along with ooicgsn-roundabout in the COPYING.md file at the project root.
# If not, see .
"""
-
+from django.db import models
from django.contrib.auth.models import AbstractUser
-from django.db.models import CharField
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
class User(AbstractUser):
-
# First Name and Last Name do not cover name patterns
# around the globe.
- name = CharField(_("Name of User"), blank=True, max_length=255)
+ name = models.CharField(_("Name of User"), blank=True, max_length=255)
+ is_infield = models.BooleanField(default=False)
+
+ class Meta:
+ ordering = ['username']
def __str__(self):
return self.username
|