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
7 changes: 4 additions & 3 deletions .github/workflows/publish-image-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ on:
push:
branches:
- "main"
- "remove-mediastore-client"
- "test-bucketstore"

env:
IMAGE_NAME: nes-lter-api-2
Expand All @@ -20,12 +20,13 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Running Automated tests
run: |

# Build and run development version
# Use in memory store for CI/tests; no network
mv dotenv.template .env
sed -i 's|^VAST_DATA_DIR=.*|VAST_DATA_DIR=./tests|' .env
sed -i 's|^USE_DICTSTORE=FALSE$|USE_DICTSTORE=TRUE|' .env
Expand All @@ -43,7 +44,7 @@ jobs:

# Create admin user and capture token
docker compose -f docker-compose-testing.yml exec -T api \
python manage.py shell -c "from django.contrib.auth import get_user_model;from rest_framework.authtoken.models import Token;from pathlib import Path;User = get_user_model();u, _ = User.objects.get_or_create(username='admin');u.set_password('admin');u.is_superuser = u.is_staff = True;u.save();token, _ = Token.objects.get_or_create(user=u);Path('/data/token.txt').write_text(token.key); print(token.key)"
python manage.py shell -c "from django.contrib.auth import get_user_model;from rest_framework.authtoken.models import Token;from pathlib import Path;User = get_user_model();u, _ = User.objects.get_or_create(username='admin');u.set_password('admin');u.is_superuser = u.is_staff = True;u.save();token, _ = Token.objects.get_or_create(user=u);Path('/data/token.txt').write_text(token.key)"


# Import the test cruise and CTD data
Expand Down
18 changes: 18 additions & 0 deletions api/core/migrations/0011_cruise_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 6.0.5 on 2026-05-12 13:33

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('core', '0010_remove_event_unique_cruise_message_id_and_more'),
]

operations = [
migrations.AddField(
model_name='cruise',
name='type',
field=models.CharField(choices=[('NESLTER', 'Neslter'), ('JP Student', 'Jp Student'), ('MAB Pioneer', 'MAB Pioneer')], default='NESLTER', max_length=32),
),
]
18 changes: 18 additions & 0 deletions api/core/migrations/0012_alter_cruise_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 6.0.5 on 2026-05-18 18:38

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('core', '0011_cruise_type'),
]

operations = [
migrations.AlterField(
model_name='cruise',
name='type',
field=models.CharField(choices=[('NESLTER', 'Neslter'), ('JP Student', 'Jp Student'), ('OOI Pioneer', 'Ooi Pioneer'), ('Opportunistic', 'Opportunistic')], default='NESLTER', max_length=32),
),
]
11 changes: 11 additions & 0 deletions api/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,19 @@ def __str__(self):


class Cruise(models.Model):
class CruiseType(models.TextChoices):
NESLTER = "NESLTER"
JP_STUDENT = "JP Student"
OOI_PIONEER = "OOI Pioneer"
OPPORTUNISTIC = "Opportunistic"

name = models.CharField(max_length=100, unique=True) # e.g. "EN627"
vessel = models.ForeignKey(Vessel, on_delete=models.CASCADE)
type = models.CharField(
max_length=32,
choices=CruiseType.choices,
default=CruiseType.NESLTER
)
start_time = models.DateTimeField(null=True, blank=True)
end_time = models.DateTimeField(null=True, blank=True)

Expand Down
3 changes: 2 additions & 1 deletion api/core/templates/cruise_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@
<body>
<div class="wrap">
<h1><span class="emoji">{{ emoji }}</span>{{ label }}</h1>
<a href="{% url 'cruises_by_type' %}">⬅️ Back to Cruises by Ship</a><br>
<a href="{% url 'cruises_by_ship' %}">⬅️ Back to Cruises by Ship</a><br>
<a href="{% url 'cruises_by_year' %}">⬅️ Back to Cruises by Year</a><br>
<a href="{% url 'cruises_by_season' %}">⬅️ Back to Cruises by Season</a><br>
<a href="{% url 'cruises_by_type' %}">⬅️ Back to Cruises by Type</a><br>
<a href="{% url 'landing' %}">⬅️ Back to landing</a>

<ul>
Expand Down
69 changes: 69 additions & 0 deletions api/core/templates/cruises_by_ship.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Cruises by Ship – NES-LTER API 2</title>
<style>
body {
font-family: system-ui, sans-serif;
margin: 2rem;
line-height: 1.6;
}
.wrap {
max-width: 780px;
margin: 0 auto;
}
h1 {
color: dodgerblue;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
margin-top: 1rem;
}
.card {
border: 1px solid #e5e5e5;
border-radius: 10px;
padding: 10px;
text-align: center;
text-decoration: none;
color: inherit;
background: white;
transition: transform .06s ease, box-shadow .06s ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
.emoji-icon {
font-size: 1.8rem;
display: block;
}
.label {
font-weight: 600;
margin-top: 4px;
}
.count {
font-size: 0.9rem;
color: #555;
}
</style>
</head>
<body>
<div class="wrap">
<h1>🌊 Cruises by Ship 🌊</h1>

<div class="grid">
{% for c in cruise_ships %}
<a class="card" href="{% url 'cruise_prefix' c.prefix %}" aria-label="{{ c.label }}">
<span class="emoji-icon">{{ c.emoji }}</span>
<div class="label">{{ c.label }}</div>
<div class="count">{{ c.count }}</div>
</a>
{% endfor %}
</div>
</div>
</body>
</html>
4 changes: 2 additions & 2 deletions api/core/templates/cruises_by_type.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@
</head>
<body>
<div class="wrap">
<h1>🌊 Cruises by Ship 🌊</h1>
<h1>🌊 Cruises by Type 🌊</h1>

<div class="grid">
{% for c in cruise_types %}
<a class="card" href="{% url 'cruise_prefix' c.prefix %}" aria-label="{{ c.label }}">
<a class="card" href="{% url 'cruises_for_type' c.prefix %}" aria-label="{{ c.label }}">
<span class="emoji-icon">{{ c.emoji }}</span>
<div class="label">{{ c.label }}</div>
<div class="count">{{ c.count }}</div>
Expand Down
3 changes: 2 additions & 1 deletion api/core/templates/landing.html
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,10 @@ <h2>📅 Cruises by Year 📅</h2>
<h3><label for="cruise-nav">Display Cruises:</label></h3>
<select id="cruise-nav" class="nav-select">
<option value="" selected disabled>Select…</option>
<option value="/cruises/type/">by Ship</option>
<option value="/cruises/ship/">by Ship</option>
<option value="/cruises/year/">by Year</option>
<option value="/cruises/season/">by Season</option>
<option value="/cruises/type/">by Type</option>
</select>
</div>

Expand Down
8 changes: 7 additions & 1 deletion api/core/templates/upload.html
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ <h1>📂 Drag and Drop Your Files</h1>
paramName: "file",
maxFilesize: 100, // MB
dataType: 'json',
headers: {
"X-CSRFToken": document.querySelector('[name=csrfmiddlewaretoken]').value
},
init: function () {
// Get CSRF token once
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
Expand Down Expand Up @@ -166,7 +169,10 @@ <h1>📂 Drag and Drop Your Files</h1>

fetch("{% url 'file-upload' %}", {
method: "POST",
body: retryFormData
body: retryFormData,
headers: {
"X-CSRFToken": csrfToken
}
})
.then(res => res.json())
.then(data => {
Expand Down
8 changes: 5 additions & 3 deletions api/core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
from chl.api import router as chl_router

from .views import file_upload_view, cruise_track_view, ctd_plot_view, \
landing, cruise_list, cruises_by_type, cruises_by_year, \
landing, cruise_list, cruises_by_ship, cruises_by_year, \
cruises_for_year, cruises_by_season, cruises_for_season, \
download_bathymetry, readme_page
cruises_by_type, cruises_for_type, download_bathymetry, readme_page

api = NinjaAPI(
title="NES-LTER API 2",
Expand All @@ -39,11 +39,13 @@
path('cruise/<str:cruise_name>/track/', cruise_track_view, name='cruise_track'),
path('cruise/<str:cruise_name>/cast/<str:cast_number>/ctd_plot/', ctd_plot_view, name='ctd_plot'),
path("", landing, name="landing"), # landing page view
path("cruises/type/", cruises_by_type, name="cruises_by_type"),
path("cruises/ship/", cruises_by_ship, name="cruises_by_ship"),
path("cruises/year/", cruises_by_year, name="cruises_by_year"),
path("cruises/year/<int:year>/", cruises_for_year, name="cruises_for_year"),
path("cruises/season/", cruises_by_season, name="cruises_by_season"),
path("cruises/season/<str:season>/", cruises_for_season, name="cruises_for_season"),
path("cruises/type/", cruises_by_type, name="cruises_by_type"),
path("cruises/type/<str:cruise_type>/", cruises_for_type, name="cruises_for_type"),
path("cruises/<str:prefix>/", cruise_list, name="cruise_prefix"),
path("download/bathymetry/", download_bathymetry, name="download_bathymetry"),
path("readmes/", readme_page, name="readmes"),
Expand Down
54 changes: 44 additions & 10 deletions api/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def clean_float(val):
except Exception:
return None

PREFIXES = [
SHIP_PREFIXES = [
("ar", "AR Cruises", "🚢"), # Ship
("en", "EN Cruises", "🛳️"), # Passenger ship
("hrs", "HRS Cruises", "🛥️"), # Motor boat
Expand Down Expand Up @@ -167,19 +167,19 @@ def landing(request):
"cruise_years": cruise_years,
})

def cruises_by_type(request):
cruise_types = []
for prefix, label, emoji in PREFIXES:
def cruises_by_ship(request):
cruise_ships = []
for prefix, label, emoji in SHIP_PREFIXES:
count = Cruise.objects.filter(name__istartswith=prefix).count()
cruise_types.append({
cruise_ships.append({
"prefix": prefix,
"label": label,
"emoji": emoji,
"count": count,
})

return render(request, "cruises_by_type.html", {
"cruise_types": cruise_types,
return render(request, "cruises_by_ship.html", {
"cruise_ships": cruise_ships,
})

def cruises_by_year(request):
Expand Down Expand Up @@ -264,12 +264,46 @@ def cruises_for_season(request, season: str):
"cruises": qs,
})

TYPE_PREFIXES = [
(Cruise.CruiseType.NESLTER, "NESLTER", "🚢"),
(Cruise.CruiseType.JP_STUDENT, "JP Student", "🛳️"),
(Cruise.CruiseType.OOI_PIONEER, "OOI Pioneer", "🛥️"),
(Cruise.CruiseType.OPPORTUNISTIC, "Opportunistic", "⛵"),
]

def cruises_by_type(request):
cruise_types = []
for prefix, label, emoji in TYPE_PREFIXES:
count = Cruise.objects.filter(type=prefix).count()
cruise_types.append({
"prefix": prefix,
"label": label,
"emoji": emoji,
"count": count,
})

return render(request, "cruises_by_type.html", {
"cruise_types": cruise_types,
})

def cruises_for_type(request, cruise_type: str):
cruises = Cruise.objects.filter(type=cruise_type).order_by("start_time", "name")

label = next((lbl for p, lbl, _ in TYPE_PREFIXES if p == cruise_type), cruise_type)
emoji = next((em for p, _, em in TYPE_PREFIXES if p == cruise_type), "🚢")

return render(request, "cruise_list.html", {
"label": label,
"emoji": emoji,
"cruises": cruises,
})

def cruise_list(request, prefix: str):
prefix = prefix.lower()
# Filter cruises by prefix
qs = Cruise.objects.filter(name__istartswith=prefix)

# sort cruises by number and suffix letter for By Ship pages
# sort cruises by number and suffix letter
cruises = sorted(
qs,
key=lambda c: (
Expand All @@ -278,8 +312,8 @@ def cruise_list(request, prefix: str):
)
)

label = next((lbl for p, lbl, _ in PREFIXES if p == prefix), prefix.upper())
emoji = next((em for p, _, em in PREFIXES if p == prefix), "🚢")
label = next((lbl for p, lbl, _ in SHIP_PREFIXES if p == prefix), prefix.upper())
emoji = next((em for p, _, em in SHIP_PREFIXES if p == prefix), "🚢")

return render(request, "cruise_list.html", {
"prefix": prefix,
Expand Down
Loading