Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 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
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

53 changes: 53 additions & 0 deletions ifcbdb/common/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from django.contrib.auth.models import User, Group
from dashboard.models import 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
is_team_captain = TeamUser.objects \
.filter(user=user) \
.filter(role_id=TeamRoles.CAPTAIN.value) \
.exists()
if is_team_captain:
return True

return False

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

if user.is_superuser or user.is_staff:
return True

if can_manage_teams(user):
return True

return False
11 changes: 11 additions & 0 deletions ifcbdb/common/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
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"

31 changes: 21 additions & 10 deletions ifcbdb/dashboard/accession.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,21 @@ def scan(self):
continue # skip and continue searching
directory = ifcb.DataDirectory(dd.path)
for b in directory:
yield b
yield (b, dd)
def sync_one(self, pid):
bin = None
dd_found = None
for dd in self.dataset.directories.filter(kind=DataDirectory.RAW).order_by('priority'):
if not os.path.exists(dd.path):
continue # skip and continue searching
directory = ifcb.DataDirectory(dd.path)
try:
bin = directory[pid]
dd_found = dd
except KeyError:
continue
if bin is not None:
break
if bin is None:
return 'bin {} not found'.format(pid)
# create instrument if necessary
Expand All @@ -86,11 +90,12 @@ def sync_one(self, pid):
'timestamp': timestamp,
'sample_time': timestamp,
'instrument': instrument,
'path': os.path.splitext(bin.fileset.adc_path)[0], # path without extension
'data_directory': dd_found,
'skip': True, # in case accession is interrupted
})
if not created and not self.dataset in b.datasets:
self.dataset.bins.add(b)
return
if not created:
return
b2s, error = self.add_bin(bin, b)
if error is not None:
# there was an error. if we created a bin, delete it
Expand All @@ -115,13 +120,14 @@ def sync(self, progress_callback=do_nothing, log_callback=do_nothing):
start_time = self.start_time()
errors = {}
while True:
bins = list(islice(scanner, self.batch_size))
if not bins:
bin_dds = list(islice(scanner, self.batch_size))
if not bin_dds:
break
total_bins += len(bins)
total_bins += len(bin_dds)
# create instrument(s)
instruments = {} # keyed by instrument number
for bin in bins:
for bin_dd in bin_dds:
bin, dd = bin_dd
i = bin.pid.instrument
if not i in instruments:
version = bin.pid.schema_version
Expand All @@ -132,7 +138,8 @@ def sync(self, progress_callback=do_nothing, log_callback=do_nothing):
# create bins
then = time.time()
bins2save = []
for bin in bins:
for bin_dd in bin_dds:
bin, dd = bin_dd
pid = bin.lid
most_recent_bin_id = pid
log_callback('{} found'.format(pid))
Expand All @@ -144,10 +151,11 @@ def sync(self, progress_callback=do_nothing, log_callback=do_nothing):
'timestamp': timestamp,
'sample_time': timestamp,
'instrument': instrument,
'path': os.path.splitext(bin.fileset.adc_path)[0], # path without extension
'data_directory': dd,
'skip': True, # in case accession is interrupted
})
if not created:
self.dataset.bins.add(b)
continue
b2s, error = self.add_bin(bin, b)
if error is not None:
Expand Down Expand Up @@ -200,6 +208,9 @@ def add_bin(self, bin, b): # IFCB bin, Bin instance
except Exception as e:
b.qc_bad = True
return b, 'ml_analyzed: {}'.format(str(e))
# paths
if b.path is None:
b.path, _ = os.path.splitext(bin.fileset.adc_path)
# metadata
try:
headers = bin.hdr_attributes
Expand Down
5 changes: 5 additions & 0 deletions ifcbdb/dashboard/management/commands/bintool.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def add_arguments(self, parser):
parser.add_argument('--sample-type', type=str, help='Sample type')
parser.add_argument('--remove-dataset', type=str, help='Dataset name to remove filtered bins from')
parser.add_argument('--add-dataset', type=str, help='Dataset name to add filtered bins to')
parser.add_argument('--cache-paths', action='store_true', help='Cache paths for filtered bins')

def handle(self, *args, **options):
dataset_name = options['dataset']
Expand Down Expand Up @@ -58,5 +59,9 @@ def handle(self, *args, **options):
except Dataset.DoesNotExist:
self.stderr.write(f"Dataset '{add_dataset_name}' does not exist.")

if options['cache_paths']:
for b in bins:
b._get_bin() # this will cache the path if it isn't already

for bin_id in bin_ids:
self.stdout.write(bin_id)
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Generated by Django 4.2.20 on 2025-06-04 02:42

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('dashboard', '0038_remove_datadirectory_unique_path_and_more'),
]

operations = [
migrations.CreateModel(
name='Team',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(auto_now_add=True)),
('name', models.CharField(max_length=50)),
],
),
migrations.CreateModel(
name='TeamUser',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dashboard.team')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('user', 'team')},
},
),
migrations.CreateModel(
name='TeamDataset',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('dataset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dashboard.dataset')),
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dashboard.team')),
],
options={
'unique_together': {('dataset', 'team')},
},
),
migrations.AddField(
model_name='team',
name='datasets',
field=models.ManyToManyField(related_name='teams', through='dashboard.TeamDataset', to='dashboard.dataset'),
),
migrations.AddField(
model_name='team',
name='users',
field=models.ManyToManyField(related_name='teams', through='dashboard.TeamUser', to=settings.AUTH_USER_MODEL),
),
]
18 changes: 18 additions & 0 deletions ifcbdb/dashboard/migrations/0040_teamuser_is_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.21 on 2025-07-04 16:21

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('dashboard', '0039_team_teamuser_teamdataset_team_datasets_team_users'),
]

operations = [
migrations.AddField(
model_name='teamuser',
name='is_manager',
field=models.BooleanField(default=False),
),
]
35 changes: 35 additions & 0 deletions ifcbdb/dashboard/migrations/0041_auto_20250718_0247.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 4.2.21 on 2025-07-18 02:47

from django.db import migrations
from common.constants import Features


def apply_migration(apps, schema_editor):
Switch = apps.get_model('waffle', 'Switch')
Switch.objects.update_or_create(
name=Features.TEAMS.value,
defaults={
'active': False,
'note': 'Enables the teams feature'
}
)

def revert_migration(apps, schema_editor):
try:
Switch = apps.get_model('waffle', 'Switch')
Switch.objects.filter(name=Features.TEAMS.value).delete()
except LookupError:
# Waffle isn't installed
pass


class Migration(migrations.Migration):

dependencies = [
('dashboard', '0040_teamuser_is_manager'),
('waffle', '0001_initial'),
]

operations = [
migrations.RunPython(apply_migration, revert_migration),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 4.2.21 on 2025-07-18 03:45

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('dashboard', '0041_auto_20250718_0247'),
]

operations = [
migrations.CreateModel(
name='TeamRole',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
],
),
migrations.RemoveField(
model_name='teamuser',
name='is_manager',
),
]
31 changes: 31 additions & 0 deletions ifcbdb/dashboard/migrations/0043_auto_20250718_0346.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Generated by Django 4.2.21 on 2025-07-18 03:46

from django.db import migrations
from common.constants import TeamRoles


def apply_migration(apps, schema_editor):
TeamRole = apps.get_model('dashboard', 'TeamRole')
TeamRole.objects.bulk_create([
TeamRole(id=TeamRoles.CAPTAIN.value, name="Captain"),
TeamRole(id=TeamRoles.MANAGER.value, name="Manager"),
TeamRole(id=TeamRoles.USER.value, name="User"),
])

def revert_migration(apps, schema_editor):
TeamRole = apps.get_model('dashboard', 'TeamRole')
TeamRole.objects.filter(id__in=[
TeamRoles.CAPTAIN.value,
TeamRoles.MANAGER.value,
TeamRoles.USER.value
]).delete()

class Migration(migrations.Migration):

dependencies = [
('dashboard', '0042_teamrole_remove_teamuser_is_manager'),
]

operations = [
migrations.RunPython(apply_migration, revert_migration),
]
19 changes: 19 additions & 0 deletions ifcbdb/dashboard/migrations/0044_teamuser_role.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2.21 on 2025-07-18 03:54

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('dashboard', '0043_auto_20250718_0346'),
]

operations = [
migrations.AddField(
model_name='teamuser',
name='role',
field=models.ForeignKey(default=3, on_delete=django.db.models.deletion.CASCADE, to='dashboard.teamrole'),
),
]
Loading