Skip to content
Open
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
35 changes: 35 additions & 0 deletions ifcbdb/common/auth.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
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
Expand All @@ -12,7 +14,40 @@ def is_admin(user):
# 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
19 changes: 19 additions & 0 deletions ifcbdb/dashboard/migrations/0047_team_default_dataset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2.21 on 2025-08-24 21:36

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


class Migration(migrations.Migration):

dependencies = [
('dashboard', '0046_auto_20250721_2039'),
]

operations = [
migrations.AddField(
model_name='team',
name='default_dataset',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dashboard.dataset'),
),
]
4 changes: 4 additions & 0 deletions ifcbdb/dashboard/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -926,10 +926,14 @@ class AppSettings(models.Model):
class Team(models.Model):
timestamp = models.DateTimeField(auto_now_add=True)
name = models.CharField(max_length=50, blank=False, null=False)
default_dataset = models.ForeignKey(Dataset, null=True, blank=True, on_delete=models.SET_NULL)

users = models.ManyToManyField(User, through='TeamUser', related_name='teams')
datasets = models.ManyToManyField(Dataset, through='TeamDataset', related_name='teams')

def __str__(self):
return self.name

class TeamRole(models.Model):
name = models.CharField(max_length=50, blank=False, null=False)

Expand Down
7 changes: 6 additions & 1 deletion ifcbdb/dashboard/templatetags/nav.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from dashboard.models import Dataset, Instrument, Tag, bin_query, AppSettings, \
DEFAULT_LATITUDE, DEFAULT_LONGITUDE, DEFAULT_ZOOM_LEVEL
from common import auth

register = template.Library()

Expand All @@ -20,6 +21,10 @@ def app_settings():

return mark_safe(settings)

@register.simple_tag(takes_context=False)
def can_access_settings(user):
return auth.can_access_settings(user)

@register.inclusion_tag('dashboard/_dataset_switcher.html')
def dataset_switcher():
datasets = Dataset.objects.all()
Expand All @@ -41,7 +46,7 @@ def dataset_nav():
@register.inclusion_tag("dashboard/_timeline-filters.html", takes_context=True)
def timeline_filters(context):
return {
}
}


@register.inclusion_tag("dashboard/_comments-nav.html", takes_context=True)
Expand Down
19 changes: 17 additions & 2 deletions ifcbdb/secure/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ class DatasetForm(forms.ModelForm):
longitude = forms.FloatField(required=False, widget=forms.TextInput(
attrs={"class": "form-control form-control-sm", "placeholder": "Longitude"}
))
team = forms.ModelChoiceField(queryset=Team.objects.all(), required=False,
widget=forms.Select(attrs={"class": "form-control form-control-sm"}))

class Meta:
model = Dataset
Expand Down Expand Up @@ -62,6 +64,10 @@ def __init__(self, *args, **kwargs):
self.fields["latitude"].initial = instance.location.y
self.fields["longitude"].initial = instance.location.x

team_dataset = TeamDataset.objects.filter(dataset=instance).first()
if team_dataset is not None:
self.fields["team"].initial = team_dataset.team

def save(self, commit=True):
instance = super(DatasetForm, self).save(commit=False)

Expand Down Expand Up @@ -271,12 +277,20 @@ class Meta:


class TeamForm(forms.ModelForm):
assigned_dataset_ids = forms.CharField(required=False, max_length=1000, widget=forms.HiddenInput())
assigned_users_json = forms.CharField(required=False, widget=forms.HiddenInput())

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

# New teams, which can only be created by a superadmin, can use any dataset that is not already associated with
# another team. On edits, the only allowed values are those already assigned to this team
if self.instance.pk:
dataset_choices = Dataset.objects.filter(teamdataset__team=self.instance)
else:
dataset_choices = Dataset.objects.filter(teamdataset__isnull=True)

self.fields["default_dataset"].queryset = dataset_choices

def clean_name(self):
name = self.cleaned_data.get("name")

Expand All @@ -288,8 +302,9 @@ def clean_name(self):

class Meta:
model = Team
fields = ["id", "name", ]
fields = ["id", "name", "default_dataset", ]

widgets = {
"name": forms.TextInput(attrs={"class": "form-control form-control-sm", "placeholder": "Name"}),
"default_dataset": forms.Select(attrs={"class": "form-control form-control-sm"}),
}
125 changes: 83 additions & 42 deletions ifcbdb/secure/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
TeamUser, TeamDataset, TeamRole
from .forms import DatasetForm, InstrumentForm, DirectoryForm, MetadataUploadForm, AppSettingsForm, UserForm, TeamForm
from common import auth
from common.constants import Features
from common.constants import Features, TeamRoles

from django.core.cache import cache
from celery.result import AsyncResult
Expand All @@ -23,12 +23,16 @@

@login_required
def index(request):
# The settings page is restricted to super admins and staff, the latter of which is what will be used to determine
# if the given user has access to something they can manage (based on their associated teams and roles)
if not auth.is_admin(request.user) and not auth.is_staff(request.user):
return redirect(reverse("secure:index"))
if not auth.can_access_settings(request.user):
return redirect("/")

can_manage_teams = auth.can_manage_teams(request.user)
has_settings_to_manage = request.user.is_superuser or can_manage_teams

return render(request, 'secure/index.html')
return render(request, 'secure/index.html', {
"has_settings_to_manage": has_settings_to_manage,
"can_manage_teams": can_manage_teams,
})


@login_required
Expand Down Expand Up @@ -77,7 +81,7 @@ def user_management(request):
@login_required
@waffle_switch('Teams')
def team_management(request):
if not auth.is_admin(request.user):
if not auth.can_manage_teams(request.user):
return redirect(reverse("secure:index"))

return render(request, 'secure/team-management.html', {
Expand All @@ -97,12 +101,22 @@ def dt_datasets(request):

@waffle_switch('Teams')
def dt_teams(request):
if not auth.is_admin(request.user):
return HttpResponseForbidden()
if not auth.can_manage_teams(request.user):
return redirect(reverse("secure:index"))

teams = Team.objects.all() \
.annotate(user_count=Count("users", distinct=True)) \
.annotate(dataset_count=Count("datasets", distinct=True)) #\
.annotate(dataset_count=Count("datasets", distinct=True))

# Limit teams list if not a super user
if not auth.is_admin(request.user):
allowed_team_ids = TeamUser.objects \
.filter(user=request.user) \
.filter(role_id=TeamRoles.CAPTAIN.value) \
.values_list("team_id", flat=True)
print(allowed_team_ids)

teams = teams.filter(id__in=allowed_team_ids)

return JsonResponse({
"data": [
Expand Down Expand Up @@ -146,6 +160,30 @@ def edit_dataset(request, id):
if form.is_valid():
instance = form.save()

existing = TeamDataset.objects.filter(dataset_id=dataset.id).first()
team = form.cleaned_data.get("team")
original_team = existing.team if existing else None
is_team_removed = False

# Save the associated team, if any
if team is None and existing is not None:
is_team_removed = True
existing.delete()

if team is not None and existing is None:
TeamDataset.objects.create(team=team, dataset=instance)

if team is not None and existing is not None and existing.team != team:
is_team_removed = True
existing.team = team
existing.save()

# If a team was removed (or changed to something else) but it was the default dataset for that team,
# clear the value
if is_team_removed and original_team is not None and original_team.default_dataset == instance:
original_team.default_dataset = None
original_team.save()

status = "created" if id == 0 else "updated"
return redirect(reverse("secure:edit-dataset", kwargs={"id": instance.id}) + "?status=" + status)
else:
Expand Down Expand Up @@ -295,16 +333,36 @@ def edit_user(request, id):

@login_required
def edit_team(request, id):
if not auth.is_admin(request.user):
if not auth.can_manage_teams(request.user):
return redirect(reverse("secure:index"))

team = get_object_or_404(Team, pk=id) if int(id) > 0 else Team()
is_new = team.pk is None

# Non-superadmins (essentially team captains) can only manage their own teams
if not auth.is_admin(request.user):
is_team_captain = TeamUser.objects \
.filter(team=team) \
.filter(user=request.user) \
.filter(role_id=TeamRoles.CAPTAIN.value) \
.exists()
if not is_team_captain:
return redirect(reverse("secure:team-management"))

if request.POST:
form = TeamForm(request.POST, instance=team)
if form.is_valid():
instance = form.save(commit=False)
instance.save()
instance = form.save()

# If this is a new team, and a default dataset is selected, make sure to associate it with
# the team. The only allowed values for team should already be datasets not already associated
# with any other team
if is_new and instance.default_dataset is not None:
# Datasets can only be associated with a single dataset right now, even though it's a many-to-many
# relationship that could support more. Because of this, make sure that the dataset selected is
# not already associated with another team
if not TeamDataset.objects.filter(dataset=instance.default_dataset).exists():
TeamDataset.objects.create(team=instance, dataset=instance.default_dataset)

assigned_users_json = form.cleaned_data.get("assigned_users_json")
assigned_users = json.loads(assigned_users_json)
Expand Down Expand Up @@ -332,37 +390,10 @@ def edit_team(request, id):
# Remove any user relationships that have been unassigned
TeamUser.objects.filter(team=instance).exclude(user_id__in=assigned_user_ids).delete()

# Update assigned datasets
assigned_dataset_ids = request.POST.get("assigned_dataset_ids")
assigned_dataset_ids = list(map(int, assigned_dataset_ids.split(","))) if assigned_dataset_ids else []

# Remove any dataset relationships that have been unassigned
TeamDataset.objects.filter(team=instance).exclude(dataset_id__in=assigned_dataset_ids).delete()

# Add any new dataset relationships
existing_ids = list(TeamDataset.objects.filter(team=instance).values_list("dataset_id", flat=True))
ids_to_add = set(assigned_dataset_ids) - set(existing_ids)
for id in ids_to_add:
team_dataset = TeamDataset()
team_dataset.team = instance
team_dataset.dataset_id = id
team_dataset.save()

return redirect(reverse("secure:team-management"))
else:
form = TeamForm(instance=team)

datasets = Dataset.objects.all().order_by("name")

if team.pk:
assigned_dataset_ids = list(TeamDataset.objects.filter(team=team).values_list("dataset_id", flat=True))

assigned_datasets = datasets.filter(id__in=assigned_dataset_ids)
available_datasets = datasets.exclude(id__in=assigned_dataset_ids)
else:
assigned_datasets = []
available_datasets = datasets

team_users = TeamUser.objects \
.filter(team=team) \
.select_related("user") \
Expand All @@ -383,15 +414,25 @@ def edit_team(request, id):

role_options = TeamRole.objects.all()

assigned_team_datasets = TeamDataset.objects \
.select_related("dataset") \
.filter(team=team).order_by("dataset__name") \
.order_by("dataset__name")
assigned_datasets_json = json.dumps([
{
"name": team_dataset.dataset.name
}
for team_dataset in assigned_team_datasets
])

return render(request, "secure/edit-team.html", {
"team": team,
"form": form,
"assigned_datasets": assigned_datasets,
"available_datasets": available_datasets,
"is_admin": auth.is_admin(request.user),
"all_users": all_users,
"assigned_users_json": assigned_users_json,
"role_options": role_options,
"assigned_datasets_json": assigned_datasets_json,
})


Expand Down
3 changes: 2 additions & 1 deletion ifcbdb/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
<!-- Left -->
<nav class="nav flex-column flex-lg-row">
{% dataset_nav %}
{% if user.is_superadmin or user.is_staff %}
{% can_access_settings user as can_access_settings %}
{% if can_access_settings %}
<a class="white-text nav-link" href="/secure/" title="Settings" data-toggle="tooltip" data-placement="bottom"><span class="h5-responsive fa fa-cog"></span> Settings</a>
{% endif %}
<div class="custom-control-inline">
Expand Down
7 changes: 7 additions & 0 deletions ifcbdb/templates/secure/edit-dataset.html
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@
{% if form.title.errors %}<span class="text-danger">{{ form.title.errors.as_text }}</span>{% endif %}
</div>
</div>
<div class="form-row">
<div class="form-group col">
<label for="id_team">Team</label>
{{ form.team }}
{% if form.team.errors %}<span class="text-danger">{{ form.team.errors.as_text }}</span>{% endif %}
</div>
</div>
<div class="form-row">
<div class="form-group col">
<label for="id_doi">DOI (optional)</label>
Expand Down
Loading