From efb90320c50699fee4217799efa6648fd265392d Mon Sep 17 00:00:00 2001 From: kev98 Date: Mon, 1 Dec 2025 13:37:57 +0000 Subject: [PATCH 1/8] Fix template for mri T1 modality --- .../upload/modalities/braintumor-mri-t1.html | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/templates/common/upload/modalities/braintumor-mri-t1.html b/templates/common/upload/modalities/braintumor-mri-t1.html index 5630661..067709c 100644 --- a/templates/common/upload/modalities/braintumor-mri-t1.html +++ b/templates/common/upload/modalities/braintumor-mri-t1.html @@ -1,33 +1,30 @@ -
+
Brain MRI T1 Scan
-
- -
-
- - {{ patient_upload_form.braintumor_mri_t1 }} - {% if patient_upload_form.braintumor_mri_t1.errors %} -
{{ patient_upload_form.braintumor_mri_t1.errors }}
+
+ + + {% if patient_upload_form.cbct.errors %} +
{{ patient_upload_form.cbct.errors }}
{% endif %}
Upload single Brain MRI T1 file: DICOM (.dcm), NIfTI (.nii, .nii.gz), MetaImage (.mha, .mhd), NRRD (.nrrd, .nhdr), or archives (.zip, .tar)
- - - - +
\ No newline at end of file From e1b82556f81c316312726e2c8879e3c79473506b Mon Sep 17 00:00:00 2001 From: kev98 Date: Mon, 1 Dec 2025 13:41:14 +0000 Subject: [PATCH 2/8] Manage user profiles per-project --- brain/admin.py | 8 ++++ brain/migrations/0001_initial.py | 25 ++++++++++++ brain/migrations/__init__.py | 0 brain/models.py | 65 ++++++++++++++++++++++++++++++++ toothfairy/middleware.py | 46 +++++++++++++++++++++- 5 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 brain/admin.py create mode 100644 brain/migrations/0001_initial.py create mode 100644 brain/migrations/__init__.py create mode 100644 brain/models.py diff --git a/brain/admin.py b/brain/admin.py new file mode 100644 index 0000000..fbde6b0 --- /dev/null +++ b/brain/admin.py @@ -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',) diff --git a/brain/migrations/0001_initial.py b/brain/migrations/0001_initial.py new file mode 100644 index 0000000..7e4aa88 --- /dev/null +++ b/brain/migrations/0001_initial.py @@ -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)), + ], + ), + ] diff --git a/brain/migrations/__init__.py b/brain/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/brain/models.py b/brain/models.py new file mode 100644 index 0000000..7c43f63 --- /dev/null +++ b/brain/models.py @@ -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}" diff --git a/toothfairy/middleware.py b/toothfairy/middleware.py index 8c430e1..55794ca 100644 --- a/toothfairy/middleware.py +++ b/toothfairy/middleware.py @@ -85,4 +85,48 @@ def process_request(self, request): project = Project.objects.get(name=url_start) request.session['current_project_id'] = project.id - return None \ No newline at end of file + return None + + +class ActiveProfileMiddleware(MiddlewareMixin): + """ + Middleware that sets `request.user.profile` to the correct profile object + depending on which app namespace the request is for (e.g. 'maxillo' or 'brain'). + This makes template and view code that uses `user.profile` app-agnostic. + + If the appropriate profile object doesn't exist yet, it will be created with + the default role. + """ + + def process_request(self, request): + # Only operate for authenticated users + if not hasattr(request, 'user') or not request.user.is_authenticated: + return None + + # Determine app by first URL segment (fallback to 'maxillo') + path_parts = [p for p in request.path.split('/') if p] + app_key = path_parts[0] if path_parts else 'maxillo' + + try: + if app_key == 'brain': + # Prefer an existing related object, create if missing + if hasattr(request.user, 'brain_profile') and getattr(request.user, 'brain_profile'): + request.user.profile = request.user.brain_profile + else: + # Lazy import to avoid circular imports on module import time + from brain.models import BrainUserProfile + profile, _ = BrainUserProfile.objects.get_or_create(user=request.user) + request.user.profile = profile + else: + # default to maxillo profile + if hasattr(request.user, 'profile') and getattr(request.user, 'profile'): + # already set (possibly by signal or previous logic) + pass + else: + from maxillo.models import UserProfile + profile, _ = UserProfile.objects.get_or_create(user=request.user) + request.user.profile = profile + except Exception: + # In case of any DB errors during auth or when running management commands, + # avoid failing the whole request — leave user.profile unset. + return None From d87809819cfa2fbec5bd56eac55b0dc0fb540dbe Mon Sep 17 00:00:00 2001 From: kev98 Date: Mon, 1 Dec 2025 14:14:06 +0000 Subject: [PATCH 3/8] Update .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index d38a0ab..c10a670 100644 --- a/.gitignore +++ b/.gitignore @@ -183,3 +183,6 @@ GitHub.sublime-settings !.vscode/launch.json !.vscode/extensions.json .history + +# Database +prod_db.sql From 67463ce10e5fe1f66167e542dee923ce44492abb Mon Sep 17 00:00:00 2001 From: kev98 Date: Mon, 1 Dec 2025 14:47:25 +0000 Subject: [PATCH 4/8] Refactor user roles to be project-specific --- common/context_processors.py | 77 +++++++++++++++++++++++++++++++++++ maxillo/context_processors.py | 48 ---------------------- templates/base.html | 4 +- templates/common/landing.html | 4 +- toothfairy/settings.py | 3 +- 5 files changed, 83 insertions(+), 53 deletions(-) create mode 100644 common/context_processors.py delete mode 100644 maxillo/context_processors.py diff --git a/common/context_processors.py b/common/context_processors.py new file mode 100644 index 0000000..5f2c926 --- /dev/null +++ b/common/context_processors.py @@ -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: _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, + } diff --git a/maxillo/context_processors.py b/maxillo/context_processors.py deleted file mode 100644 index 89f69b1..0000000 --- a/maxillo/context_processors.py +++ /dev/null @@ -1,48 +0,0 @@ -from .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 - if user.is_staff or getattr(user.profile, 'is_admin', False) or getattr(user.profile, '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 = [] - return { - 'current_project': project, - 'current_project_name': name, - 'current_project_icon': icon, - 'current_project_description': description, - 'current_project_id': pid, - 'all_projects': all_projects, - } - - diff --git a/templates/base.html b/templates/base.html index f66bbd4..11b6b4b 100644 --- a/templates/base.html +++ b/templates/base.html @@ -120,7 +120,9 @@