Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/chl/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def get(cls, cruise_name: str) -> FileResponse:

try:
Cruise.objects.get(name__iexact=cruise_name)
object_key = f"{cruise_name}{cls.FILE_SUFFIX}"
object_key = f"{cruise_name.lower()}{cls.FILE_SUFFIX}"
with MediaStore(cls.URL, token=cls.TOKEN) as store:
prefix = PrefixStore(store, cls.MEDIASTORE_PREFIX)
try:
Expand Down
14 changes: 9 additions & 5 deletions api/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,25 +153,25 @@ def cruise_track_view(request, cruise_name):
# Non-clickable track points (no labels or popups)
try:
track_points = [
{"lat": row[" Dec_LAT"], "lng": row[" Dec_LON"]}
{"lat": row["dec_lat"], "lng": row["dec_lon"]} # ar
for _, row in underway_data.iterrows()
]
except KeyError:
try:
track_points = [
{"lat": row["Latitude_Deg"], "lng": row["Longitude_Deg"]} # hrs2303
{"lat": row["latitude_deg"], "lng": row["longitude_deg"]} # hrs2303
for _, row in underway_data.iterrows()
]
except KeyError:
try:
track_points = [
{"lat": row["GPS-Furuno-Latitude"], "lng": row["GPS-Furuno-Longitude"]} # en
{"lat": row["gps_furuno_latitude"], "lng": row["gps_furuno_longitude"]} # en
for _, row in underway_data.iterrows()
]
except KeyError:

track_points = [
{"lat": row["Latitude"], "lng": row["Longitude"]} # ae2426
{"lat": row["latitude"], "lng": row["longitude"]} # ae2426
for _, row in underway_data.iterrows()
]

Expand Down Expand Up @@ -286,4 +286,8 @@ def ctd_plot_view(request, cruise_name, cast_number):
plt.close()
buffer.seek(0)

return HttpResponse(buffer.getvalue(), content_type='image/png')
# The title will be verified in cruise_track.js automated test
resp = HttpResponse(buffer.getvalue(), content_type="image/png")
resp["X-Plot-Title"] = f"{cruise_name} Cast {cast_number} - CTD Profile"
return resp

7 changes: 7 additions & 0 deletions api/ctd/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ def update_vessel(request, vessel_name: str, input: UpdateVesselInput):
except ValueError as e:
return {"status": "error", "message": str(e)}

@router.delete('/vessels/delete/{vessel_name}', tags=["Admin"], auth=TokenAuthenticator())
def delete_vessel(request, vessel_name: str):
try:
result = CtdService.delete_vessel(vessel_name)
return result
except ValueError as e:
return {"status": "error", "message": str(e)}

@router.get("/cruises/get/all", response=List[CruiseOutput], tags=["Users"])
def get_cruises(request):
Expand Down
41 changes: 35 additions & 6 deletions api/ctd/services.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import io, os
import csv
from typing import Optional, List, Tuple
from datetime import datetime

Expand Down Expand Up @@ -160,6 +161,16 @@ def create_vessel(cls, input: AddVesselInput) -> VesselOutput:
except Vessel.DoesNotExist:
pass

@classmethod
def delete_vessel(cls, vessel_name: str):
vessel = Vessel.objects.filter(name__iexact=vessel_name)
if not vessel.exists():
raise HttpError(404, f"Vessel with name '{vessel_name}' does not exist.")
try:
vessel.delete()
return {"message": f"Vessel '{vessel_name}' deleted"}
except Exception as e:
raise HttpError(500, f"Failed to delete vessel: {str(e)}")

@classmethod
def update_vessel(cls, vessel_name: str, input: UpdateVesselInput) -> VesselOutput:
Expand Down Expand Up @@ -194,9 +205,27 @@ def serialize_cruise(cruise: Cruise) -> CruiseOutput:
)

@classmethod
def get_cruises(cls) -> list[CruiseOutput]:
def get_cruises(cls) -> HttpResponse:
cruises = Cruise.objects.all()
return [cls.serialize_cruise(cruise) for cruise in cruises]

buffer = io.StringIO()
writer = csv.writer(buffer)

# Write header row
writer.writerow(["name", "vessel", "start_time", "end_time"])

for cruise in cruises:
writer.writerow([
cruise.name,
cruise.vessel.name,
cruise.start_time,
cruise.end_time
])

# Convert to HttpResponse
response = HttpResponse(buffer.getvalue(), content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="cruises.csv"'
return response

@classmethod
def get_cruise(cls, cruise_name: str) -> CruiseOutput:
Expand Down Expand Up @@ -446,7 +475,7 @@ def delete_niskin(cruise_name: str, cast_number: str, niskin_number: int):
cast = Cast.objects.get(cruise=cruise, number__iexact=cast_number)
niskin = Niskin.objects.get(cast=cast, number=niskin_number)
niskin.delete()
return {"status": "success", "message": f"Cast {cast_number} on cruise {cruise_name} deleted."}
return {"status": "success", "message": f"Niskin {niskin_number} on cruise {cruise_name} for cast {cast_number} deleted."}
except Cruise.DoesNotExist:
raise Http404(f"Cruise {cruise_name} not found.")
except Cast.DoesNotExist:
Expand All @@ -462,7 +491,7 @@ def get_bottles(cls, cruise_name: str) -> FileResponse:
FILE_SUFFIX = '_ctd_bottles.csv'
try:
Cruise.objects.get(name__iexact=cruise_name)
object_key = f"{cruise_name}{FILE_SUFFIX}"
object_key = f"{cruise_name.lower()}{FILE_SUFFIX}"
with MediaStore(URL, token=TOKEN) as store:
prefix = PrefixStore(store, MEDIASTORE_PREFIX)
try:
Expand All @@ -485,7 +514,7 @@ def get_bottle_summary(cls, cruise_name: str) -> FileResponse:
FILE_SUFFIX = '_ctd_bottle_summary.csv'
try:
Cruise.objects.get(name__iexact=cruise_name)
object_key = f"{cruise_name}{FILE_SUFFIX}"
object_key = f"{cruise_name.lower()}{FILE_SUFFIX}"
with MediaStore(URL, token=TOKEN) as store:
prefix = PrefixStore(store, MEDIASTORE_PREFIX)
try:
Expand All @@ -508,7 +537,7 @@ def get_metadata(cls, cruise_name: str) -> FileResponse:
FILE_SUFFIX = '_ctd_metadata.csv'
try:
Cruise.objects.get(name__iexact=cruise_name)
object_key = f"{cruise_name}{FILE_SUFFIX}"
object_key = f"{cruise_name.lower()}{FILE_SUFFIX}"
with MediaStore(URL, token=TOKEN) as store:
prefix = PrefixStore(store, MEDIASTORE_PREFIX)
try:
Expand Down
11 changes: 7 additions & 4 deletions api/events/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@
def get_events(request, cruise_name: str):
return EventService.get_events(cruise_name)

@router.get("/instruments/{cruise_name}", response=List[str], tags=["Users"])
def get_instruments(request, cruise_name: str):
return EventService.get_instruments(cruise_name)

@router.post("/filter/{cruise_name}", response=List[EventOutput], tags=["Users"])
@router.post("/filter/{cruise_name}", response=List[EventOutput], tags=["Users"], auth=TokenAuthenticator())
def filter_events(request, cruise_name: str, input: FilterEventInput):
return EventService.filter_events(cruise_name, input)

@router.post("/edit/{cruise_name}/{message_id}", response=EventOutput, tags=["Admin"], auth=TokenAuthenticator())
def edit_events(request, cruise_name: str, message_id: int, input: EditEventInput):
return EventService.edit_events(cruise_name, message_id, input)
@router.post("/edit/{cruise_name}/{r2r_event}", response=EventOutput, tags=["Admin"], auth=TokenAuthenticator())
def edit_events(request, cruise_name: str, r2r_event: str, input: EditEventInput):
return EventService.edit_events(cruise_name, r2r_event, input)


@router.get("/history/{cruise_name}", response=str, tags=["Users"])
Expand Down
2 changes: 1 addition & 1 deletion api/events/management/commands/importevent.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def store_csv_file(self, cruise_name, csv_data):

def handle(self, *args, **options):
cruise_name = options['cruise_name']

if cruise_name is None:
cruises = list(Cruise.objects.values_list('name', flat=True))
else:
Expand Down
56 changes: 44 additions & 12 deletions api/events/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
MESSAGE_ID = 'Message ID'

class EventOutput(BaseModel):
r2r_event: str
message_id: int
instrument: str
action: str
Expand All @@ -43,18 +44,19 @@ class EventOutput(BaseModel):
class FilterEventInput(BaseModel):
instrument: Optional[str] = None
action: Optional[str] = None
station: Optional[str] = None
cast: Optional[str] = None
comment: Optional[str] = None
station: Optional[str] = ""
cast: Optional[str] = ""
comment: Optional[str] = ""

class EditEventInput(BaseModel):
message_id: Optional[int] = None
instrument: Optional[str] = None
action: Optional[str] = None
station: Optional[str] = None
cast: Optional[str] = None
station: Optional[str] = ""
cast: Optional[str] = ""
latitude: Optional[float] = None
longitude: Optional[float] = None
comment: Optional[str] = None
comment: Optional[str] = ""
datetime: Optional[datetime] = None


Expand All @@ -71,6 +73,7 @@ def serialize_event(event: Event) -> EventOutput:
longitude = None

return EventOutput(
r2r_event=event.r2r_event,
message_id=event.message_id,
instrument=event.instrument,
action=event.action,
Expand All @@ -90,6 +93,8 @@ def store_csv_file(self, cruise_name, csv_data):
df = pd.DataFrame(csv_data)
df[DATETIME] = pd.to_datetime(df[DATETIME])
df = df.sort_values(by=DATETIME)
df["Station"] = df["Station"].replace("nan", "")
df["Cast"] = df["Cast"].replace("nan", "")
csv_buffer = io.StringIO()
df.to_csv(csv_buffer, index=False)
csv_binary = csv_buffer.getvalue().encode("utf-8")
Expand Down Expand Up @@ -129,7 +134,7 @@ def get_events(cls, cruise_name: str) -> FileResponse:
try:
cruise = Cruise.objects.get(name__iexact=cruise_name)
if Event.objects.filter(cruise=cruise).exists():
object_key = f"{cruise_name}{FILE_SUFFIX}"
object_key = f"{cruise_name.lower()}{FILE_SUFFIX}"
with MediaStore(URL, token=TOKEN) as store:
prefix = PrefixStore(store, MEDIASTORE_PREFIX)
try:
Expand All @@ -144,7 +149,27 @@ def get_events(cls, cruise_name: str) -> FileResponse:
else:
raise Http404(f"Event data not imported.")
except Cruise.DoesNotExist:
raise Http404(f"Cruise {cruise_name} not found.")
raise Http404(f"Cruise {cruise_name} not found.")

@classmethod
def get_instruments(cls, cruise_name: str) -> List[str]:
URL = os.getenv("URL")
TOKEN = os.getenv("TOKEN")
MEDIASTORE_PREFIX = os.getenv("MEDIASTORE_PREFIX")

try:
cruise = Cruise.objects.get(name__iexact=cruise_name)
if Event.objects.filter(cruise=cruise).exists():
events = Event.objects.filter(cruise=cruise)

# Extract unique instruments, sorted alphabetically
instruments = events.values_list('instrument', flat=True).distinct().order_by('instrument')
return list(instruments)
else:
raise Http404("Event data not imported..")
return []
except Cruise.DoesNotExist:
raise Http404(f"Cruise {cruise_name} not found.")


@classmethod
Expand All @@ -162,23 +187,28 @@ def filter_events(cls, cruise_name: str, input: FilterEventInput) -> List[EventO
events = events.filter(cast__iexact=input.cast)
if input.comment:
events = events.filter(comment__icontains=input.comment)
print(events.query, flush=True)
print([e.id for e in events], flush=True)

return [EventService.serialize_event(event) for event in events]
except Cruise.DoesNotExist:
raise Http404(f"Cruise {cruise_name} not found.")

@classmethod
def edit_events(cls, cruise_name: str, message_id: int, input: EditEventInput) -> EventOutput:
def edit_events(cls, cruise_name: str, r2r_event: str, input: EditEventInput) -> EventOutput:
try:
cruise = Cruise.objects.get(name__iexact=cruise_name)
event = Event.objects.get(cruise=cruise, message_id=message_id)
event = Event.objects.get(cruise=cruise, r2r_event=r2r_event)
if input.message_id:
event.message_id = input.message_id
if input.instrument:
event.instrument = input.instrument
if input.action:
event.action = input.action
if input.station:
event.station = input.station
if input.cast:
event.cast = input.cast
event.cast = input.cast
if input.latitude and input.longitude:
event.geolocation = Point(float(input.longitude), float(input.latitude), srid=4326)
if input.comment:
Expand All @@ -191,6 +221,7 @@ def edit_events(cls, cruise_name: str, message_id: int, input: EditEventInput) -
events = Event.objects.filter(cruise=cruise)
data = [
{
"R2R_Event": e.r2r_event,
MESSAGE_ID: e.message_id,
DATETIME: e.datetime,
"Instrument": e.instrument,
Expand All @@ -210,7 +241,7 @@ def edit_events(cls, cruise_name: str, message_id: int, input: EditEventInput) -
except Cruise.DoesNotExist:
raise Http404(f"Cruise {cruise_name} not found.")
except Event.DoesNotExist:
raise Http404(f"Event {message_id} not found for {cruise_name} .")
raise Http404(f"Event {r2r_event} not found for {cruise_name} .")

@classmethod
def history_events(cls, cruise_name: str) -> str:
Expand All @@ -224,6 +255,7 @@ def history_events(cls, cruise_name: str) -> str:
diff = record.diff_against(record.prev_record)
if diff.changed_fields:
history_data.append({
'r2r_event': event.r2r_event,
'message_id': event.message_id,
'history_date': record.history_date,
'history_user': record.history_user,
Expand Down
2 changes: 1 addition & 1 deletion api/hplc/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def get(cls, cruise_name: str) -> FileResponse:
try:
if cruise_name.lower() != "mvco":
Cruise.objects.get(name__iexact=cruise_name)
object_key = f"{cruise_name}{FILE_SUFFIX}"
object_key = f"{cruise_name.lower()}{FILE_SUFFIX}"
with MediaStore(URL, token=TOKEN) as store:
prefix = PrefixStore(store, MEDIASTORE_PREFIX)
try:
Expand Down
2 changes: 1 addition & 1 deletion api/nut/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def get(cls, cruise_name: str) -> FileResponse:

try:
Cruise.objects.get(name__iexact=cruise_name)
object_key = f"{cruise_name}{cls.FILE_SUFFIX}"
object_key = f"{cruise_name.lower()}{cls.FILE_SUFFIX}"
with MediaStore(cls.URL, token=cls.TOKEN) as store:
prefix = PrefixStore(store, cls.MEDIASTORE_PREFIX)
try:
Expand Down
2 changes: 1 addition & 1 deletion api/stations/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def set_location(request, input: StationLocationInput):
return 204


@router.post('/add_nearest', response=AddNearestStationOutput, tags=["Users"])
@router.post('/add_nearest', response=AddNearestStationOutput, tags=["Users"], auth=TokenAuthenticator())
def add_nearest_station(request, input: AddNearestStationInput):
return StationService.add_nearest_station(
latitude=input.latitude,
Expand Down
6 changes: 5 additions & 1 deletion api/underway/management/commands/importunderwaydata.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pandas as pd
from core.models import Cruise
from core.models import Underway
from core.utils import clean_column_names
from storage.fs import FilesystemStore
from storage.mediastore import MediaStore
from storage.utils import PrefixStore
Expand Down Expand Up @@ -87,6 +88,9 @@ def handle(self, *args, **options):
else:
start_datetime = pd.to_datetime(combined_data[date_column].iloc[0])
end_datetime = pd.to_datetime(combined_data[date_column].iloc[-1])

df_data = clean_column_names(combined_data)

else:
raise ValueError(f"Unsupported cruise type for cruise_name: {cruise_name}")
start_datetime = None if pd.isna(start_datetime) else self.make_aware_if_naive(start_datetime)
Expand All @@ -100,7 +104,7 @@ def handle(self, *args, **options):
)

csv_buffer = StringIO()
combined_data.to_csv(csv_buffer, index=False)
df_data.to_csv(csv_buffer, index=False)
csv_binary = csv_buffer.getvalue().encode('utf-8')
# Use the put method to store the CSV in the vast media store
object_key = f"{cruise_name}{self.FILE_SUFFIX}"
Expand Down
Loading