Skip to content
Draft
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,6 @@ GitHub.sublime-settings
!.vscode/launch.json
!.vscode/extensions.json
.history

# Database
prod_db.sql
8 changes: 8 additions & 0 deletions brain/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.contrib import admin
from .models import BrainUserProfile

@admin.register(BrainUserProfile)
class BrainUserProfileAdmin(admin.ModelAdmin):
list_display = ('user', 'role')
search_fields = ('user__username', 'user__email')
list_filter = ('role',)
25 changes: 25 additions & 0 deletions brain/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 5.2.4 on 2025-12-01 10:36

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


class Migration(migrations.Migration):

initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='BrainUserProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('role', models.CharField(choices=[('standard', 'Standard User'), ('annotator', 'Annotator'), ('project_manager', 'Project Manager'), ('admin', 'Administrator'), ('student_dev', 'Student Developer')], default='standard', max_length=20)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='brain_profile', to=settings.AUTH_USER_MODEL)),
],
),
]
17 changes: 17 additions & 0 deletions brain/migrations/0002_alter_brainuserprofile_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.2.4 on 2025-12-02 13:44

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('brain', '0001_initial'),
]

operations = [
migrations.AlterModelOptions(
name='brainuserprofile',
options={'verbose_name': 'User profile', 'verbose_name_plural': 'User profiles'},
),
]
Empty file added brain/migrations/__init__.py
Empty file.
65 changes: 65 additions & 0 deletions brain/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from django.db import models
from django.contrib.auth.models import User

class BrainUserProfile(models.Model):
ROLE_CHOICES = [
('standard', 'Standard User'),
('annotator', 'Annotator'),
('project_manager', 'Project Manager'),
('admin', 'Administrator'),
('student_dev', 'Student Developer'),
]

user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='brain_profile')
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='standard')

class Meta:
verbose_name = "User profile"
verbose_name_plural = "User profiles"

def __str__(self):
return f"{self.user.username} - {self.get_role_display()}"

def is_annotator(self):
return self.role in ['annotator', 'project_manager', 'admin']

def is_project_manager(self):
return self.role == 'project_manager'

def is_admin(self):
return self.role == 'admin'

def is_student_developer(self):
return self.role == 'student_dev'

def can_upload_scans(self):
"""Check if user can upload scans"""
return self.role in ['annotator', 'project_manager', 'admin', 'student_dev']

def can_see_debug_scans(self):
"""Check if user can see debug scans"""
return self.role in ['admin', 'student_dev']

def can_see_public_private_scans(self):
"""Check if user can see public/private scans"""
return self.role in ['annotator', 'project_manager', 'admin', 'standard']

def can_modify_scan_settings(self):
"""Check if user can modify scan settings (visibility, dataset, etc.)"""
return self.role in ['annotator', 'project_manager', 'admin']

def can_delete_scans(self):
"""Check if user can delete scans"""
return self.role in ['admin'] # Only admins can delete non-debug scans

def can_delete_debug_scans(self):
"""Check if user can delete debug scans"""
return self.role in ['admin', 'student_dev']

def can_view_other_profiles(self):
"""Check if user can view other users' profiles"""
return self.role in ['project_manager', 'admin']


def __str__(self):
return f"Brain profile of {self.user.username}"
77 changes: 77 additions & 0 deletions common/context_processors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from common.models import Project, ProjectAccess


def current_project(request):
project = None
icon = ''
name = ''
description = ''
all_projects = []
pid = request.session.get('current_project_id')
if pid:
try:
project = Project.objects.get(id=pid, is_active=True)
name = getattr(project, 'name', '') or ''
icon = getattr(project, 'icon', '') or ''
description = getattr(project, 'description', '') or ''
except Project.DoesNotExist:
pass

# Expose projects based on user access for navbar switching
user = getattr(request, 'user', None)
try:
if user and user.is_authenticated:
# Admins and student developers can see all projects
# Use getattr checks to avoid attribute errors if profile is missing
if user.is_staff or getattr(getattr(user, 'profile', None), 'is_admin', False) or getattr(getattr(user, 'profile', None), 'is_student_developer', False):
all_projects = Project.objects.filter(is_active=True).order_by('name')
else:
# Regular users only see projects they have access to
accessible_project_ids = ProjectAccess.objects.filter(
user=user,
can_view=True
).values_list('project_id', flat=True)
all_projects = Project.objects.filter(
is_active=True,
id__in=accessible_project_ids
).order_by('name')
except Exception:
# Avoid breaking templates if profile or db access fails in edge cases
all_projects = []

# Determine project-specific role display for the current user
current_project_slug = ''
current_project_role_display = None
current_project_profile = None
if project and user and user.is_authenticated:
current_project_slug = getattr(project, 'slug', '') or ''
# Try convention: <slug>_profile, then fallback to 'profile'
try:
if current_project_slug:
attr_name = f"{current_project_slug}_profile"
if hasattr(user, attr_name):
current_project_profile = getattr(user, attr_name)
current_project_role_display = getattr(current_project_profile, 'get_role_display', None)
if callable(current_project_role_display):
current_project_role_display = current_project_role_display()
# fallback
if not current_project_role_display and hasattr(user, 'profile'):
current_project_profile = user.profile
current_project_role_display = getattr(current_project_profile, 'get_role_display', None)
if callable(current_project_role_display):
current_project_role_display = current_project_role_display()
except Exception:
# Ignore profile lookup errors
current_project_role_display = None

return {
'current_project': project,
'current_project_name': name,
'current_project_icon': icon,
'current_project_description': description,
'current_project_id': pid,
'all_projects': all_projects,
'current_project_slug': current_project_slug,
'current_project_role_display': current_project_role_display,
'current_project_profile': current_project_profile,
}
19 changes: 19 additions & 0 deletions common/migrations/0011_alter_invitation_project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 5.2.4 on 2025-12-02 16:08

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


class Migration(migrations.Migration):

dependencies = [
('common', '0010_add_project_manager_role'),
]

operations = [
migrations.AlterField(
model_name='invitation',
name='project',
field=models.ForeignKey(blank=True, help_text='Project the user will have access to', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invitations', to='common.project'),
),
]
27 changes: 27 additions & 0 deletions common/migrations/0012_alter_invitation_project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 5.2.4 on 2025-12-02 16:11

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


class Migration(migrations.Migration):

dependencies = [
('common', '0011_alter_invitation_project'),
]

operations = [
migrations.AlterField(
model_name='invitation',
name='project',
field=models.ForeignKey(
on_delete=models.SET_NULL,
to='common.project',
null=True,
blank=True,
related_name='invitations',
help_text='Project the user will have access to',
),
preserve_default=False,
),
]
31 changes: 31 additions & 0 deletions common/migrations/0013_alter_invitation_project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Generated by Django 5.2.4 on 2025-12-02 16:11

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


def set_default_project(apps, schema_editor):
Invitation = apps.get_model('common', 'Invitation')
Project = apps.get_model('common', 'Project')

default_project = Project.objects.get(slug='maxillo')

# Assign default project to all invitations that have no project yet
Invitation.objects.filter(project__isnull=True).update(project=default_project)

def unset_default_project(apps, schema_editor):
# Reverse function: in case of migration rollback
Invitation = apps.get_model('common', 'Invitation')
# You can either set project back to NULL:
Invitation.objects.filter().update(project=None)
# or leave it empty if you don't care about rolling back

class Migration(migrations.Migration):

dependencies = [
('common', '0012_alter_invitation_project'),
]

operations = [
migrations.RunPython(set_default_project, unset_default_project),
]
19 changes: 19 additions & 0 deletions common/migrations/0014_alter_invitation_project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 5.2.4 on 2025-12-03 10:56

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


class Migration(migrations.Migration):

dependencies = [
('common', '0013_alter_invitation_project'),
]

operations = [
migrations.AlterField(
model_name='invitation',
name='project',
field=models.ForeignKey(help_text='Project the user will have access to', on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to='common.project'),
),
]
3 changes: 2 additions & 1 deletion common/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ class Invitation(models.Model):
code = models.CharField(max_length=64, unique=True)
email = models.EmailField(blank=True, null=True)
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='standard')
project = models.ForeignKey(Project, on_delete=models.SET_NULL, null=True, blank=True, related_name='invitations', help_text='Optional: Project the user will have access to')
#project = models.ForeignKey(Project, on_delete=models.SET_NULL, null=True, blank=True, related_name='invitations', help_text='Project the user will have access to')
project = models.ForeignKey(Project, on_delete=models.CASCADE, null=False, blank=False, related_name='invitations', help_text='Project the user will have access to')
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
created_at = models.DateTimeField(auto_now_add=True)
expires_at = models.DateTimeField()
Expand Down
2 changes: 2 additions & 0 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,7 @@ done

mkdir -p /app/logs

python manage.py makemigrations
python manage.py migrate
# python manage.py collectstatic --noinput
python manage.py runserver 0.0.0.0:8000
6 changes: 3 additions & 3 deletions maxillo/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.contrib import admin
from django.contrib.auth.models import User
from .models import UserProfile, Dataset, Patient, Classification, VoiceCaption
from .models import MaxilloUserProfile, Dataset, Patient, Classification, VoiceCaption
from common.models import Project, Modality, ProjectAccess, Job, FileRegistry, Invitation
from .models import Tag, Folder
from common.models import Project, Modality, ProjectAccess
Expand All @@ -25,8 +25,8 @@ def has_delete_permission(self, request, obj=None):
return super().has_delete_permission(request, obj)


@admin.register(UserProfile)
class UserProfileAdmin(ReadOnlyAdminMixin, admin.ModelAdmin):
@admin.register(MaxilloUserProfile)
class MaxilloUserProfileAdmin(ReadOnlyAdminMixin, admin.ModelAdmin):
list_display = ['user', 'role']
list_filter = ['role']
search_fields = ['user__username', 'user__email']
Expand Down
1 change: 1 addition & 0 deletions maxillo/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class MaxilloConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "maxillo"
label = "scans"
verbose_name = "Maxillo"

def ready(self):
import maxillo.signals
48 changes: 0 additions & 48 deletions maxillo/context_processors.py

This file was deleted.

Loading