Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
9274d7c
initial infrastructure for features
mike-kaimika Mar 18, 2025
0a44fb6
add user management screen
mike-kaimika Mar 11, 2025
5c54217
add manager role
mike-kaimika May 11, 2025
1f8f749
add team management pages
mike-kaimika Jun 3, 2025
e328e2f
updates to team edit page
mike-kaimika Jun 4, 2025
f01a839
add list_dataset endpoints
mike-kaimika Jun 4, 2025
94c7e15
adjust manager permissions for partial team editing
mike-kaimika Jun 4, 2025
298b18f
add migrations to add and set user groups
mike-kaimika Jun 6, 2025
ea2237a
tweak team list and sorting when editing a team
mike-kaimika Jun 6, 2025
b471ce1
updates to the user listing screen
mike-kaimika Jun 6, 2025
ab9b7fd
additional updates to per user manager setting
mike-kaimika Jul 10, 2025
323cf1c
use django-waffles to handle feature flags
mike-kaimika Jul 18, 2025
6d9ec1e
remove system-wide groups (to be replaced with per-team roles)
mike-kaimika Jul 18, 2025
61c6d8b
add roles to assigned team users
mike-kaimika Jul 18, 2025
bb3fac2
additional adjustments to user permissions
mike-kaimika Jul 20, 2025
1f2f514
make datasets readonly on edit team screen
mike-kaimika Sep 2, 2025
cbdb782
allow team captains to manage users for a team
mike-kaimika Aug 13, 2025
3734173
add default dataset to teams
mike-kaimika Aug 24, 2025
85510c2
bin path caching, no change to accession logic yet
joefutrelle Aug 14, 2025
db56f51
adjusted accession logic for bin caching
joefutrelle Aug 14, 2025
f938cf2
adding --cache-paths directive
joefutrelle Aug 14, 2025
d89f690
Merge branch 'cache_bin_path_v2' into release/4.5.1-alpha
mike-kaimika Sep 2, 2025
73f6110
Merge branch 'team_enhancements_v2' into release/4.5.1-alpha
mike-kaimika Sep 2, 2025
5f3b54f
merge migrations
mike-kaimika Sep 2, 2025
034bf43
update version
mike-kaimika Sep 2, 2025
68d0b9b
add contact and description fields to datasets
mike-kaimika Sep 12, 2025
d4581ea
add team based routing; improve datasets page
mike-kaimika Sep 16, 2025
c60c77d
add team field to bins
mike-kaimika Sep 17, 2025
ecdde29
add bin management
mike-kaimika Sep 17, 2025
2bc419b
add team description
mike-kaimika Sep 19, 2025
26a186b
hide inactive datasets
mike-kaimika Sep 19, 2025
a2f10b3
add actions to bin management
mike-kaimika Sep 21, 2025
e6d2319
improve permission checks and logic when saving dataset
mike-kaimika Oct 10, 2025
6537bd2
update permission checks for bin management and metadata upload
mike-kaimika Oct 15, 2025
6d695d3
ui/ux improvements to bin maangement
mike-kaimika Oct 19, 2025
1a09025
handle team feature on dataset listing and during accession
mike-kaimika Oct 26, 2025
060b2e7
improve permission checks and form value restrictions
mike-kaimika Oct 28, 2025
d2a003d
Merge branch 'master' into team_enhancements_v3
mike-kaimika Dec 10, 2025
8554370
refactoring; updating team feature checks; fixing share links
mike-kaimika Dec 17, 2025
ad82149
hide teams on edit dataset when not enabled
mike-kaimika Dec 17, 2025
7fe66ad
update start/end date logic for bin search
mike-kaimika Dec 30, 2025
630bd92
fix export and improve action ui
mike-kaimika Dec 30, 2025
6aec030
fix line spacing in dataset descriptions
mike-kaimika Dec 31, 2025
7069b3d
backfill team on bins during sync
mike-kaimika Dec 31, 2025
aeed8d5
improve layout of dataset list w/o teams enabled
mike-kaimika Jan 15, 2026
7f33e57
trim spaces on metadata import columns
mike-kaimika Jan 15, 2026
b878ddd
ensure team name is a slug
mike-kaimika Jan 15, 2026
13a3b2b
improve display of empty dataset and remove 404
mike-kaimika Jan 16, 2026
0737fd1
upping version and date in footer to 5.0 2026/01
joefutrelle Jan 29, 2026
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: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ x-ifcb-common: &ifcb-common
services:
ifcbdb:
<<: *ifcb-common
env_file:
- .env
environment:
- NGINX_HOST=${HOST:-localhost}
- NGINX_HTTP_PORT=${HTTP_PORT:-80}
Expand Down
1 change: 1 addition & 0 deletions dotenv.template
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ DJANGO_SECRET_KEY=changeme
POSTGRES_PASSWORD=changeme

#LOCAL_SETTINGS=./local_settings.py

16 changes: 15 additions & 1 deletion ifcbdb/assets/css/site.css
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,20 @@ table .btn-mdb-color {
text-overflow: ellipsis;
}

#team-nav-menu {
-webkit-columns: 3;
-moz-columns: 3;
columns: 3;
list-style-position: inside;
overflow-x: auto;
}

#team-nav-menu li a{
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

#filter-content {
min-width: 250px;
}
Expand Down Expand Up @@ -384,7 +398,7 @@ a.tstabcollapser:after
#go-to-bid-pid {
height:22px;
margin-top:9px;
width:200px;
width:100px;
border:none;
padding-left:2px;
padding-right:2px;
Expand Down
43 changes: 24 additions & 19 deletions ifcbdb/assets/js/bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,33 +40,38 @@ var _autoCompleteJS;

//************* Common Methods ***********************/

function getPage(page, queryString = "") {
return window.location.pathname.replace(/[^/]+$/, page)
+ (queryString ? "?" + queryString : "");
}

// Generates a relative link to the current bin/dataset
function createLink() {
// Bin Mode
if (_route != "timeline")
return createBinModeLink();

// Timeline Mode
return "/timeline?" +
buildFilterOptionsQueryString(true) +
(_bin != "" ? "&bin=" + _bin : "");
const queryString = buildFilterOptionsQueryString(true) + (_bin != "" ? "&bin=" + _bin : "");

return getPage("timeline", queryString);
}

function createListLink(start, end) {
if (!isFilteringUsed())
return "javascript:;;";

return "/list?" + getGroupingParameters(_bin) +
"&start_date=" + start +
"&end_date=" + end;
const queryString = getGroupingParameters(_bin) + "&start_date=" + start + "&end_date=" + end;

return getPage("list", queryString);
}

function createBinModeLink(bin) {
if (bin == "" || typeof bin == "undefined") {
bin = _bin;
}

return "/bin?" + getGroupingParameters(bin);
return getPage("bin", getGroupingParameters(bin));
}

function getGroupingParameters(bin) {
Expand Down Expand Up @@ -104,15 +109,15 @@ function createBinLink(bin) {
return createBinModeLink(bin);
}

return "/timeline?" + getGroupingParameters(bin);
return getPage("timeline", getGroupingParameters(bin));
}

function createImageLink(imageId) {
if (_route == "bin") {
return "/image?image=" + imageId + "&bin=" + _bin;
return getPage("image", "image=" + imageId + "&bin=" + _bin);
}

var url = "/image?image=" + imageId;
var url = getPage("image", "image=" + imageId);
var parameters = getGroupingParameters(_bin);

return url + (parameters != "" ? "&" + parameters : "");
Expand Down Expand Up @@ -171,7 +176,7 @@ function updateBinStats(data) {
}

$("#stat-instrument").html(data["instrument"]);
$("#stat-instrument-link").attr('href','/timeline?instrument='+data["instrument"]+'&bin='+_bin);
$("#stat-instrument-link").attr('href', getPage("timeline", 'instrument='+data["instrument"]+'&bin='+_bin));
$("#stat-num-triggers").html(data["num_triggers"]);
$("#stat-num-images").html(data["num_images"]);
$("#stat-trigger-freq").html(data["trigger_freq"]);
Expand Down Expand Up @@ -226,12 +231,11 @@ function updateBinDatasets(data) {
$("#dataset-links").empty();

for (var i = 0; i < data.datasets.length; i++) {
// <a href="#" class="d-block">asdasd</a>
$("#dataset-links").append(
$("<a class='d-block' />")
.attr("href", "/timeline?bin=" + _bin + "&dataset=" + data.datasets[i])
.attr("href", getPage("timeline", "bin=" + _bin + "&dataset=" + data.datasets[i]))
.text(data.datasets[i])
)
);
}
}

Expand Down Expand Up @@ -367,8 +371,10 @@ function displayTags(tags) {
for (var i = 0; i < tags.length; i++) {
var tag = tags[i];
var li = $("<span class='badge badge-pill badge-light mx-1'>");
var link = "timeline?tags=" + tag + "&" +
buildFilterOptionsQueryString(false, _dataset, _instrument, null, _cruise, _sampleType);

const queryString = "tags=" + tag + "&" + buildFilterOptionsQueryString(
false, _dataset, _instrument, null, _cruise, _sampleType);
var link = getPage("timeline", queryString);

var span = li.html("<a href='"+link+"'>"+tag+"</a>");
var icon = $("<i class='fas fa-times pl-1'></i>");
Expand Down Expand Up @@ -1029,12 +1035,11 @@ function initEvents() {
$("#share-button").click(function(e) {
e.preventDefault();

var link = $("#share-link");
var base = link.data("scheme") + "://" + link.data("host");
const rootUrl = window.location.protocol + "//" + window.location.host;

$("#share-modal").modal();
$("#share-modal .modal-title").text($("#share-modal .modal-title").data("default-text"));
$("#share-link").val(base + createLink()).select();
$("#share-link").val(rootUrl + createLink()).select();
});

// Copy the share link to the clipboard
Expand Down
43 changes: 38 additions & 5 deletions ifcbdb/assets/js/site.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ const MAX_SELECTABLE_IMAGES = 25;

let _binFilterMode = "timeline";

function getPage(page, queryString) {
return window.location.pathname.replace(/[^/]+$/, page)
+ (queryString ? "?" + queryString : "");
}

function initDashboard(appSettings) {
defaultLat = appSettings.default_latitude;
defaultLng = appSettings.default_longitude;
Expand All @@ -36,7 +41,7 @@ function initDashboard(appSettings) {
});

$("#dataset-switcher").change(function () {
location.href = "/timeline?dataset=" + $(this).val();
location.href = getPage("timeline") + "dataset=" + $(this).val();
});

$("#go-to-bin").click(function () {
Expand Down Expand Up @@ -511,7 +516,7 @@ function goToBin(pid) {
return;
}

location.href = "/bin?bin=" + pid.trim();
location.href = getPage("bin", "bin=" + pid.trim());
});
}

Expand Down Expand Up @@ -579,13 +584,41 @@ function isFilteringUsed() {
return true;
}

// Formats a list of Django validation errors into an <ul />
function formatValidationErrorsAsList(errors) {
const items = [];

for (const [field, messages] of Object.entries(errors)) {

// Capitalize and format field name (e.g., "email_address" -> "Email Address")
const fieldName = field.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());

messages.forEach(message => {
const prefix = field === "__all__" ? "" : `<strong>${fieldName}:</strong> `;

items.push(`<li>${prefix}${message}</li>`);
});
}

return "<ul class='error-list mb-0'>" + items.join() + "</ul>";
}

$(function () {
$('#dataset-popover').popover({
$('#datasets-popover').popover({
container: 'body',
title: 'Select Dataset',
title: 'Dataset',
html: true,
placement: 'bottom',
sanitize: false,
template: '<div class="popover" style="max-width:60%;" role="tooltip"><div class="arrow"></div><h3 class="popover-header"></h3><div class="popover-body" style="max-height:50vh; overflow-y:auto;"></div></div>'
})
});

$('#teams-popover').popover({
container: 'body',
title: 'Teams',
html: true,
placement: 'bottom',
sanitize: false,
template: '<div class="popover" style="max-width:60%;" role="tooltip"><div class="arrow"></div><h3 class="popover-header"></h3><div class="popover-body" style="max-height:50vh; overflow-y:auto;"></div></div>'
});
})
112 changes: 112 additions & 0 deletions ifcbdb/common/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
from django.contrib.auth.models import User, Group
from dashboard.models import Dataset, Team, TeamDataset, TeamUser
from .constants import TeamRoles

# At the moment, this is simply a wrapper around the superadmin flag. In the future, this, and possibly other
# methods, will be used to determine access rules based on which teams a user is associated with and what
# their assigned role is for that team
def is_admin(user):
if not user.is_authenticated:
return False

return user.is_superuser

# This one is also just a wrapper around the staff flag. This is likely what will be used to determine if a user
# has access to things "quickly" without having to check through associated teams and roles on those records
def is_staff(user):
if not user.is_authenticated:
return False

if not user.is_staff:
return False

return user.is_staff

def can_manage_teams(user):
if not user.is_authenticated:
return False

if user.is_superuser or user.is_staff:
return True

# Team captains have limited access to the admin to manage their own teams
return has_team_roles(user, [TeamRoles.CAPTAIN, ])

def can_manage_datasets(user):
if not user.is_authenticated:
return False

if user.is_superuser or user.is_staff:
return True

# Team captains have limited access to the admin to manage their own teams
return has_team_roles(user, [TeamRoles.CAPTAIN, ])

def can_manage_metadata(user):
if not user.is_authenticated:
return False

if user.is_superuser or user.is_staff:
return True

# Team captains have limited access to the admin to manage their own teams
return has_team_roles(user, [TeamRoles.CAPTAIN, ])

def can_manage_bins(user):
if not user.is_authenticated:
return False

if user.is_superuser or user.is_staff:
return True

# Team captains have limited access to the admin to manage their own teams
return has_team_roles(user, [TeamRoles.CAPTAIN, ])

def can_access_settings(user):
if not user.is_authenticated:
return False

if user.is_superuser or user.is_staff:
return True

return has_team_roles(user, [TeamRoles.CAPTAIN, ])

def has_team_roles(user, roles):
role_values = [role.value for role in roles]

return TeamUser.objects \
.filter(user=user) \
.filter(role_id__in=role_values) \
.exists()

def get_manageable_teams(user):
if not user.is_authenticated:
return []

if user.is_superuser or user.is_staff:
return Team.objects.all()

return Team.objects \
.filter(teamuser__user=user) \
.filter(teamuser__role_id__in=[TeamRoles.CAPTAIN.value, ]) \
.distinct()

def get_manageable_datasets(user, exclude_inactive=True):
if not user.is_authenticated:
return []

datasets = Dataset.objects.all()

if exclude_inactive:
datasets = datasets.exclude(is_active=False)

if user.is_superuser or user.is_staff:
return datasets

teams = get_manageable_teams(user)

dataset_ids = TeamDataset.objects \
.filter(team__in=teams) \
.values_list("dataset_id", flat=True)

return datasets.filter(id__in=dataset_ids)
20 changes: 20 additions & 0 deletions ifcbdb/common/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from enum import Enum

class TeamRoles(Enum):
CAPTAIN = 1
MANAGER = 2
USER = 3

# Values for this enum should map to their environment variable names
class Features(Enum):
TEAMS = "Teams"


class BinManagementActions(Enum):
SKIP_BINS = "skip-bins"
UNSKIP_BINS = "unskip-bins"
ASSIGN_DATASET = "assign-dataset"
UNASSIGN_DATASET = "unassign-dataset"

# Metadata column names
BIN_ID_COLUMNS = ['id','pid','lid','bin','bin_id','sample','sample_id','filename']
Loading