Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
ebb75da
big start... lots broken
tlambert03 Nov 21, 2025
2fc75c3
rename dye name
tlambert03 Nov 21, 2025
34bf862
fix choices
tlambert03 Nov 21, 2025
3652902
fix factory
tlambert03 Nov 21, 2025
9bbc7df
broken... fixing tests
tlambert03 Nov 21, 2025
502a975
big change on Spectrum.owner_X
tlambert03 Nov 21, 2025
23b6b34
fix label save
tlambert03 Nov 21, 2025
0ccac8f
up pnpm
tlambert03 Nov 21, 2025
3cf13ab
more test fixes
tlambert03 Nov 21, 2025
06a228e
update oc_eff stuff
tlambert03 Nov 22, 2025
a0abf20
typing
tlambert03 Nov 22, 2025
4f95c3b
fix graphql
tlambert03 Nov 22, 2025
c5bb4f6
just test update
tlambert03 Nov 22, 2025
e9c343c
remove old
tlambert03 Nov 22, 2025
bbdbf9d
migration and tests working
tlambert03 Nov 22, 2025
f70f57c
add notes
tlambert03 Nov 22, 2025
61458e1
revise notes
tlambert03 Nov 22, 2025
b0bb086
apply suggestions
tlambert03 Nov 22, 2025
3bcff92
move scout
tlambert03 Nov 23, 2025
1ef7a5b
single migration
tlambert03 Nov 23, 2025
0c0e651
conditional scout celery
tlambert03 Nov 23, 2025
e505190
just typing
tlambert03 Nov 23, 2025
95f51bd
just notes and tests
tlambert03 Nov 23, 2025
01f9bea
preserve old state id
tlambert03 Nov 24, 2025
ac6fd3f
cleanup and miminize new model, add owner_name to flourophore, change…
tlambert03 Nov 24, 2025
caeb8dc
undo changes to old migrations
tlambert03 Nov 24, 2025
f17ecfe
remove notes
tlambert03 Nov 24, 2025
be840e5
review notes
tlambert03 Nov 24, 2025
0052ea8
remove unneeded sql
tlambert03 Nov 24, 2025
b7d671d
cleanup
tlambert03 Nov 24, 2025
d89dde2
fix data migration
tlambert03 Nov 25, 2025
aee3486
fix reversion
tlambert03 Nov 25, 2025
02061b1
add comments
tlambert03 Nov 25, 2025
2bfdcad
fix tests
tlambert03 Nov 25, 2025
918bc96
inline wavetohex
tlambert03 Nov 25, 2025
0663bb3
fix dye spectrum submit, add test
tlambert03 Nov 25, 2025
73ea243
slugify dye
tlambert03 Nov 25, 2025
a77e36c
fix exband
tlambert03 Nov 25, 2025
f7c2a98
fix help text for dark state and update timer/other labels
tlambert03 Nov 25, 2025
87344bc
change hints
tlambert03 Nov 25, 2025
6bd7a88
remove comments about measurable fields in migration functions
tlambert03 Nov 25, 2025
df635b8
refactor rebuild_attributes
tlambert03 Nov 25, 2025
8006dc6
update migrations
tlambert03 Nov 25, 2025
d83fea6
add check to ci
tlambert03 Nov 25, 2025
4edb27b
fix test
tlambert03 Nov 25, 2025
e47d6c7
fix typing
tlambert03 Nov 25, 2025
9c3cc57
cleanup
tlambert03 Nov 25, 2025
97fcada
remove files
tlambert03 Nov 25, 2025
24a7675
fix fret stuff
tlambert03 Nov 26, 2025
17c78f0
test scope report
tlambert03 Nov 26, 2025
618c7f6
chang m2m typing
tlambert03 Nov 26, 2025
5d99bf1
remove date measured
tlambert03 Nov 26, 2025
02e3d86
remove is_trusted
tlambert03 Nov 26, 2025
b8723f0
Merge branch 'main' into schema-overhaul
tlambert03 Nov 26, 2025
01eeed4
rename Fluorophore to FluorState
tlambert03 Nov 26, 2025
7ddcbfd
update admin
tlambert03 Nov 26, 2025
ef6e631
fix admin
tlambert03 Nov 26, 2025
50aa2d7
update view
tlambert03 Nov 26, 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
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ jobs:
- name: Run TypeScript type checking
run: pnpm typecheck

check-migrations:
runs-on: ubuntu-latest
steps:
- name: Checkout Code Repository
uses: actions/checkout@v5
- uses: astral-sh/setup-uv@v7
with:
enable-cache: true
- name: Check for new migrations
run: uv run backend/manage.py makemigrations --check --noinput

test:
runs-on: ubuntu-latest
services:
Expand Down
1 change: 0 additions & 1 deletion backend/config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@

THIRD_PARTY_APPS = [
"django_structlog", # Structured logging
"scout_apm.django", # APM monitoring
"crispy_forms", # Form layouts
"crispy_bootstrap5",
"allauth", # registration
Expand Down
1 change: 1 addition & 0 deletions backend/config/settings/production.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ def WHITENOISE_IMMUTABLE_FILE_TEST(path, url):
# Scout APM Configuration
# ------------------------------------------------------------------------------
# SCOUT_MONITOR and SCOUT_KEY are automatically set by the Heroku addon
INSTALLED_APPS += ["scout_apm.django"]
SCOUT_NAME = "FPbase"

# Structlog Configuration for Production
Expand Down
15 changes: 12 additions & 3 deletions backend/favit/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from __future__ import annotations

from typing import TYPE_CHECKING

from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
Expand All @@ -6,19 +10,24 @@

from .managers import FavoriteManager

if TYPE_CHECKING:
from fpbase.users.models import User


class Favorite(models.Model):
user = models.ForeignKey(
user_id: int
user: models.ForeignKey[User] = models.ForeignKey(
getattr(settings, "AUTH_USER_MODEL", "auth.User"),
related_name="favorites",
on_delete=models.CASCADE,
)
target_content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
target_content_type_id: int
target_content_type: models.ForeignKey[ContentType] = models.ForeignKey(ContentType, on_delete=models.CASCADE)
target_object_id = models.PositiveIntegerField()
target = GenericForeignKey("target_content_type", "target_object_id")
timestamp = models.DateTimeField(auto_now_add=True, db_index=True)

objects = FavoriteManager()
objects: FavoriteManager = FavoriteManager()

class Meta:
ordering = ["-timestamp"]
Expand Down
1 change: 1 addition & 0 deletions backend/fpbase/cache_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def _invalidate_optical_config_cache() -> None:
SPECTRUM_OWNER_MODELS = {
"proteins.Camera",
"proteins.Dye",
"proteins.DyeState",
"proteins.Filter",
"proteins.Light",
"proteins.Protein",
Expand Down
3 changes: 2 additions & 1 deletion backend/fpbase/celery.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
app = Celery("fpbase", namespace="CELERY")
app.config_from_object("django.conf:settings", namespace="CELERY")

scout_apm.celery.install(app)
if getattr(settings, "SCOUT_NAME", False):
scout_apm.celery.install(app)

# Load task modules from all registered Django app configs.
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
21 changes: 20 additions & 1 deletion backend/fpbase/users/models.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
from __future__ import annotations

from typing import TYPE_CHECKING

from django.contrib.auth.models import AbstractUser
from django.contrib.auth.signals import user_logged_in
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _

if TYPE_CHECKING:
from favit.models import Favorite
from proteins.models import Microscope, OpticalConfig, ProteinCollection
from references.models import Reference


class User(AbstractUser):
# # AbstractUser Fields
Expand All @@ -19,6 +28,15 @@ class User(AbstractUser):
# around the globe.
name = models.CharField(_("Name of User"), blank=True, max_length=255)

if TYPE_CHECKING:
logins: models.QuerySet[UserLogin]
favorites: models.QuerySet[Favorite]
reference_author: models.QuerySet[Reference]
reference_modifier: models.QuerySet[Reference]
microscopes: models.QuerySet[Microscope]
opticalconfigs: models.QuerySet[OpticalConfig]
proteincollections: models.QuerySet[ProteinCollection]

def __str__(self):
return self.username

Expand All @@ -29,7 +47,8 @@ def get_absolute_url(self):
class UserLogin(models.Model):
"""Represent users' logins, one per record"""

user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="logins")
user_id: int
user: models.ForeignKey[User] = models.ForeignKey(User, on_delete=models.CASCADE, related_name="logins")
timestamp = models.DateTimeField(auto_now_add=True)

def __str__(self) -> str:
Expand Down
2 changes: 1 addition & 1 deletion backend/fpbase/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ def get_context_data(self):
data = super().get_context_data()
data["stats"] = {
"proteins": Protein.objects.count(),
"protspectra": Spectrum.objects.exclude(owner_state=None).count(),
"protspectra": Spectrum.objects.exclude(owner_fluor=None).count(),
}
return data

Expand Down
134 changes: 108 additions & 26 deletions backend/proteins/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@
BleachMeasurement,
Camera,
Dye,
DyeState,
Excerpt,
Filter,
FilterPlacement,
Fluorophore,
FluorState,
Light,
Lineage,
Microscope,
Expand All @@ -41,7 +42,7 @@
class SpectrumOwner:
list_display = ("__str__", "spectra", "created_by", "created")
list_select_related = ("created_by",)
list_filter = ("created", "manufacturer")
list_filter = ("created",)
search_fields = ("name",)

def __init__(self, *args, **kwargs):
Expand All @@ -56,7 +57,7 @@ def _makelink(sp):
return f'<a href="{url}">{sp.get_subtype_display()}{pending}</a>'

links = []
if isinstance(obj, Fluorophore):
if isinstance(obj, FluorState):
[links.append(_makelink(sp)) for sp in obj.spectra.all()]
else:
links.append(_makelink(obj.spectrum))
Expand Down Expand Up @@ -98,10 +99,8 @@ class OSERInline(admin.StackedInline):
]


class StateInline(MultipleSpectraOwner, admin.StackedInline):
# form = StateForm
# formset = StateFormSet
model = State
class FluorStateInline(MultipleSpectraOwner):
model = FluorState
extra = 0
can_delete = True
show_change_link = True
Expand All @@ -113,10 +112,9 @@ class StateInline(MultipleSpectraOwner, admin.StackedInline):
"fields": (
("ex_max", "em_max"),
("ext_coeff", "qy"),
("twop_ex_max", "twop_peakGM", "twop_qy"),
("pka", "maturation"),
("twop_ex_max", "twop_peak_gm", "twop_qy"),
"pka",
"lifetime",
"bleach_links",
"spectra",
)
},
Expand All @@ -131,13 +129,25 @@ class StateInline(MultipleSpectraOwner, admin.StackedInline):
]
readonly_fields = (
"slug",
"bleach_links",
"created",
"created_by",
"modified",
"updated_by",
)


class StateInline(FluorStateInline, admin.StackedInline):
# form = StateForm
# formset = StateFormSet
model = State
fieldsets = [
*FluorStateInline.fieldsets,
(None, {"fields": ("bleach_links",)}),
(None, {"fields": ("maturation",)}),
]

readonly_fields = (*FluorStateInline.readonly_fields, "bleach_links")

@admin.display(description="BleachMeasurements")
def bleach_links(self, obj):
links = []
Expand All @@ -148,6 +158,10 @@ def bleach_links(self, obj):
return mark_safe(", ".join(links))


class DyeStateInline(FluorStateInline, admin.StackedInline):
model = DyeState


class LineageInline(admin.TabularInline):
model = Lineage
autocomplete_fields = ("parent", "root_node")
Expand All @@ -165,9 +179,24 @@ class LightAdmin(SpectrumOwner, admin.ModelAdmin):


@admin.register(Dye)
class DyeAdmin(MultipleSpectraOwner, VersionAdmin):
class DyeAdmin(admin.ModelAdmin):
model = Dye
list_display = ("__str__", "created_by", "created")
ordering = ("-created",)
list_filter = ("created", "manufacturer")
fields = (
"name",
"slug",
"manufacturer",
"default_state",
"primary_reference",
"created",
"modified",
"created_by",
"updated_by",
)
readonly_fields = ("created", "modified")
inlines = (DyeStateInline,)


@admin.register(Filter)
Expand Down Expand Up @@ -198,39 +227,42 @@ class SpectrumAdmin(VersionAdmin):
model = Spectrum
autocomplete_fields = ["reference"]
list_select_related = (
"owner_state__protein",
"owner_fluor",
"owner_filter",
"owner_camera",
"owner_light",
"owner_dye",
"created_by",
)
list_display = ("__str__", "category", "subtype", "owner", "created_by")
list_filter = ("status", "created", "category", "subtype")
readonly_fields = ("owner", "name", "created", "modified", "spectrum_preview")
search_fields = (
"owner_state__protein__name",
"owner_fluor__name",
"owner_filter__name",
"owner_camera__name",
"owner_light__name",
"owner_dye__name",
)

def get_fields(self, request, obj=None):
fields = []
if not obj or not obj.category:
# If no category yet, allow selecting any owner type
own = [
"owner_state",
"owner_fluor",
"owner_filter",
"owner_camera",
"owner_light",
"owner_dye",
]
elif obj.category == Spectrum.PROTEIN:
own = ["owner_state"]
elif obj.category in (Spectrum.PROTEIN, Spectrum.DYE):
# Protein and Dye both use owner_fluor (Fluorophore)
own = ["owner_fluor"]
else:
# Filter, Camera, Light
own = ["owner_" + obj.get_category_display().split(" ")[0].lower()]
fields.extend(own)
# Add clickable link to owner's admin page (for existing objects)
if obj and obj.owner:
fields.append("owner")
self.autocomplete_fields.extend(own)
fields += [
"category",
Expand All @@ -249,11 +281,18 @@ def get_fields(self, request, obj=None):

@admin.display(description="Owner")
def owner(self, obj):
url = reverse(
f"admin:proteins_{obj.owner._meta.model.__name__.lower()}_change",
args=(obj.owner.pk,),
)
link = f'<a href="{url}">{obj.owner}</a>'
owner = obj.owner
# FluorState is a base class - resolve to the actual subclass admin
if isinstance(owner, FluorState):
if owner.entity_type == FluorState.EntityTypes.PROTEIN:
model_name = "state"
else:
model_name = "dyestate"
else:
model_name = owner._meta.model.__name__.lower()

url = reverse(f"admin:proteins_{model_name}_change", args=(owner.pk,))
link = f'<a href="{url}">{owner}</a>'
return mark_safe(link)

@admin.display(description="Spectrum Preview")
Expand Down Expand Up @@ -342,7 +381,7 @@ class StateAdmin(CompareVersionAdmin):
"fields": (
("ex_max", "em_max"),
("ext_coeff", "qy"),
("twop_ex_max", "twop_peakGM", "twop_qy"),
("twop_ex_max", "twop_peak_gm", "twop_qy"),
("pka", "maturation"),
"lifetime",
)
Expand All @@ -356,6 +395,49 @@ def protein_link(self, obj):
return mark_safe(f'<a href="{url}">{obj.protein}</a>')


@admin.register(DyeState)
class DyeStateAdmin(MultipleSpectraOwner, CompareVersionAdmin):
# form = StateForm
model = State
list_select_related = ("dye", "created_by", "updated_by")
search_fields = ("dye__name",)
list_display = (
"__str__",
"dye_link",
"ex_max",
"em_max",
"created_by",
"updated_by",
"modified",
)
list_filter = ("created", "modified")
fieldsets = [
(None, {"fields": (("name", "slug", "is_dark"),)}),
(
None,
{
"fields": (
("ex_max", "em_max"),
("ext_coeff", "qy"),
("twop_ex_max", "twop_peak_gm", "twop_qy"),
("pka", "lifetime"),
)
},
),
(
None,
{
"fields": ("spectra",),
},
),
]

@admin.display(description="Dye")
def dye_link(self, obj):
url = reverse("admin:proteins_dye_change", args=([obj.dye.pk]))
return mark_safe(f'<a href="{url}">{obj.dye}</a>')


class StateTransitionAdmin(VersionAdmin):
model = StateTransition
list_select_related = ("protein", "from_state", "to_state")
Expand Down
Loading
Loading