Skip to content
Merged
Show file tree
Hide file tree
Changes from 34 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
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 # noqa F401


class Favorite(models.Model):
user = models.ForeignKey(
user_id: int
user = models.ForeignKey["User"](
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](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](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
32 changes: 20 additions & 12 deletions backend/proteins/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
BleachMeasurement,
Camera,
Dye,
DyeState,
Excerpt,
Filter,
FilterPlacement,
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 Down Expand Up @@ -113,7 +114,7 @@ class StateInline(MultipleSpectraOwner, admin.StackedInline):
"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",
"bleach_links",
Expand Down Expand Up @@ -165,9 +166,16 @@ class LightAdmin(SpectrumOwner, admin.ModelAdmin):


@admin.register(Dye)
class DyeAdmin(MultipleSpectraOwner, VersionAdmin):
class DyeAdmin(admin.ModelAdmin):
model = Dye
ordering = ("-created",)
list_filter = ("created", "manufacturer")


@admin.register(DyeState)
class DyeStateAdmin(MultipleSpectraOwner, VersionAdmin):
model = DyeState
ordering = ("-created",)


@admin.register(Filter)
Expand Down Expand Up @@ -198,37 +206,37 @@ 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)
self.autocomplete_fields.extend(own)
Expand Down Expand Up @@ -342,7 +350,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 Down
16 changes: 10 additions & 6 deletions backend/proteins/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from rest_framework import serializers

from ..models import Protein, Spectrum, State, StateTransition
from ._tweaks import ModelSerializer
from proteins.api._tweaks import ModelSerializer
from proteins.models import Fluorophore, Protein, Spectrum, State, StateTransition


class SpectrumSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -29,12 +29,16 @@ class Meta:
)

def get_protein_name(self, obj):
if obj.owner_state:
return obj.owner_state.protein.name
# Check if owner_fluor is a State (has protein attribute)
if obj.owner_fluor and obj.owner_fluor.entity_type == Fluorophore.EntityTypes.PROTEIN:
return obj.owner_fluor.protein.name
return None

def get_protein_slug(self, obj):
if obj.owner_state:
return obj.owner_state.protein.slug
# Check if owner_fluor is a State (has protein attribute)
if obj.owner_fluor and obj.owner_fluor.entity_type == Fluorophore.EntityTypes.PROTEIN:
return obj.owner_fluor.protein.slug
return None


class StateTransitionSerializer(serializers.ModelSerializer):
Expand Down
4 changes: 2 additions & 2 deletions backend/proteins/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class SpectrumList(ListAPIView):


class SpectrumDetail(RetrieveAPIView):
queryset = pm.Spectrum.objects.prefetch_related("owner_state")
queryset = pm.Spectrum.objects.prefetch_related("owner_fluor")
permission_classes = (AllowAny,)
serializer_class = SpectrumSerializer

Expand Down Expand Up @@ -122,7 +122,7 @@ def dispatch(self, *args, **kwargs):

class BasicProteinListAPIView(ProteinListAPIView):
queryset = (
pm.Protein.visible.filter(switch_type=pm.Protein.BASIC)
pm.Protein.visible.filter(switch_type=pm.Protein.SwitchingChoices.BASIC)
.select_related("default_state")
.annotate(rate=Max(F("default_state__bleach_measurements__rate")))
)
Expand Down
36 changes: 21 additions & 15 deletions backend/proteins/factories.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# pyright: reportPrivateImportUsage=false
import random
from typing import TypeVar, cast
from typing import TYPE_CHECKING, TypeVar, cast

import factory
import factory.builder
Expand All @@ -9,10 +9,12 @@
from django.utils.text import slugify

from fpseq import FPSeq
from proteins.models import Camera, Filter, FilterPlacement, Light, Microscope, OpticalConfig, Protein, Spectrum, State
from proteins.util.helpers import wave_to_hex
from references.factories import ReferenceFactory

from .models import Camera, Filter, FilterPlacement, Light, Microscope, OpticalConfig, Protein, Spectrum, State
if TYPE_CHECKING:
from proteins.models import Fluorophore

T = TypeVar("T")

Expand Down Expand Up @@ -120,14 +122,14 @@ def _mock_edge_filter(edge, subtype, min_wave=300, max_wave=900, transmission=0.
def _build_spectral_data(resolver: factory.builder.Resolver):
subtype = getattr(resolver, "subtype", None)

if (owner_state := getattr(resolver, "owner_state", None)) is not None:
owner_state = cast("State", owner_state)
if (owner_fluor := getattr(resolver, "owner_fluor", None)) is not None:
owner_fluor = cast("Fluorophore", owner_fluor)
if subtype == "ex":
return _mock_spectrum(owner_state.ex_max, type="ex")
return _mock_spectrum(owner_fluor.ex_max, type="ex")
elif subtype == "em":
return _mock_spectrum(owner_state.em_max, type="em")
elif subtype == "2p" and owner_state.twop_ex_max:
return _mock_spectrum(owner_state.twop_ex_max, type="ex", min_wave=600, max_wave=1100)
return _mock_spectrum(owner_fluor.em_max, type="em")
elif subtype == "2p" and getattr(owner_fluor, "twop_ex_max", None):
return _mock_spectrum(owner_fluor.twop_ex_max, type="ex", min_wave=600, max_wave=1100)

if (owner_filter := getattr(resolver, "owner_filter", None)) is not None:
owner_filter = cast("Filter", owner_filter)
Expand Down Expand Up @@ -192,27 +194,31 @@ class Meta:

ex_spectrum = factory.RelatedFactory(
"proteins.factories.SpectrumFactory",
factory_related_name="owner_state",
factory_related_name="owner_fluor",
subtype="ex",
category="p",
)
em_spectrum = factory.RelatedFactory(
"proteins.factories.SpectrumFactory",
factory_related_name="owner_state",
factory_related_name="owner_fluor",
subtype="em",
category="p",
)
twop_spectrum = factory.RelatedFactory(
"proteins.factories.SpectrumFactory",
factory_related_name="owner_state",
factory_related_name="owner_fluor",
subtype="2p",
category="p",
)


class DyeFactory(FluorophoreFactory):
class DyeFactory(factory.django.DjangoModelFactory):
class Meta:
model = "proteins.Dye"
django_get_or_create = ("name", "slug")

name = factory.Sequence(lambda n: f"TestDye{n}")
slug = factory.LazyAttribute(lambda o: slugify(o.name))


class ProteinFactory(factory.django.DjangoModelFactory[Protein]):
Expand All @@ -225,7 +231,7 @@ class Meta:
slug = factory.LazyAttribute(lambda o: slugify(o.name))
seq = factory.LazyFunction(_protein_seq)
seq_validated = factory.Faker("boolean", chance_of_getting_true=75)
agg = factory.fuzzy.FuzzyChoice(Protein.AGG_CHOICES, getter=lambda c: c[0])
agg = factory.fuzzy.FuzzyChoice(Protein.AggChoices, getter=lambda c: c[0])
pdb = factory.LazyFunction(lambda: random.choices(REAL_PDBS, k=random.randint(0, 2)))
parent_organism = factory.SubFactory(OrganismFactory)
primary_reference = factory.SubFactory("references.factories.ReferenceFactory")
Expand Down Expand Up @@ -352,7 +358,7 @@ def create_egfp() -> Protein:
"DHYQQNTPIGDGPVLLPDNHYLSTQSALSKDPNEKRDHMVLLEFVTAAGITLGMDELYK"
),
seq_validated=True,
agg=Protein.MONOMER,
agg=Protein.AggChoices.MONOMER,
pdb=["4EUL", "2Y0G"],
parent_organism=OrganismFactory(scientific_name="Aequorea victoria", id=6100, division="hydrozoans"),
primary_reference=ReferenceFactory(doi="10.1016/0378-1119(95)00685-0"),
Expand All @@ -367,7 +373,7 @@ def create_egfp() -> Protein:
default_state__maturation=25,
default_state__lifetime=2.8,
default_state__twop_ex_max=927,
default_state__twop_peakGM=39.64,
default_state__twop_peak_gm=39.64,
)

return egfp
6 changes: 3 additions & 3 deletions backend/proteins/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
class SpectrumFilter(filters.FilterSet):
class Meta:
model = Spectrum
fields = ("category", "subtype", "id", "owner_state")
fields = ("category", "subtype", "id", "owner_fluor")


class StateFilter(filters.FilterSet):
Expand Down Expand Up @@ -107,8 +107,8 @@ class ProteinFilter(filters.FilterSet):
name__icontains = django_filters.CharFilter(
field_name="name", method="name_or_alias_icontains", lookup_expr="icontains"
)
switch_type__ne = django_filters.ChoiceFilter(choices=Protein.SWITCHING_CHOICES, method="switch_type__notequal")
cofactor__ne = django_filters.ChoiceFilter(choices=Protein.COFACTOR_CHOICES, method="cofactor__notequal")
switch_type__ne = django_filters.ChoiceFilter(choices=Protein.SwitchingChoices, method="switch_type__notequal")
cofactor__ne = django_filters.ChoiceFilter(choices=Protein.CofactorChoices, method="cofactor__notequal")
parent_organism__ne = django_filters.ModelChoiceFilter(
queryset=Organism.objects.all(), method="parent_organism__notequal"
)
Expand Down
Loading
Loading