Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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