From ebb75da0d277a653e8eeffbf1c0dd99fbef102cf Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Thu, 20 Nov 2025 20:02:34 -0500 Subject: [PATCH 01/57] big start... lots broken --- backend/favit/models.py | 2 +- backend/proteins/api/views.py | 2 +- backend/proteins/filters.py | 4 +- backend/proteins/models/__init__.py | 4 +- backend/proteins/models/_old_protein.py | 637 ++++++++++++++++++ .../models/{state.py => _old_state.py} | 1 + backend/proteins/models/_sequence_field.py | 30 + backend/proteins/models/dye.py | 193 ++++++ backend/proteins/models/efficiency.py | 5 +- backend/proteins/models/fluorescence_data.py | 101 +++ .../models/fluorescence_measurement.py | 40 ++ backend/proteins/models/fluorophore.py | 185 +++++ backend/proteins/models/lineage.py | 2 +- backend/proteins/models/protein.py | 302 ++++----- backend/proteins/models/spectrum.py | 4 +- backend/proteins/util/efficiency.py | 3 +- backend/proteins/views/protein.py | 4 +- pyproject.toml | 5 +- uv.lock | 5 +- 19 files changed, 1328 insertions(+), 201 deletions(-) create mode 100644 backend/proteins/models/_old_protein.py rename backend/proteins/models/{state.py => _old_state.py} (99%) create mode 100644 backend/proteins/models/_sequence_field.py create mode 100644 backend/proteins/models/dye.py create mode 100644 backend/proteins/models/fluorescence_data.py create mode 100644 backend/proteins/models/fluorescence_measurement.py create mode 100644 backend/proteins/models/fluorophore.py diff --git a/backend/favit/models.py b/backend/favit/models.py index bda700dcc..c0113c3a4 100644 --- a/backend/favit/models.py +++ b/backend/favit/models.py @@ -18,7 +18,7 @@ class Favorite(models.Model): 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"] diff --git a/backend/proteins/api/views.py b/backend/proteins/api/views.py index 0ac31fcb6..11b019d92 100644 --- a/backend/proteins/api/views.py +++ b/backend/proteins/api/views.py @@ -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"))) ) diff --git a/backend/proteins/filters.py b/backend/proteins/filters.py index a6c15189f..dfd7baf7b 100644 --- a/backend/proteins/filters.py +++ b/backend/proteins/filters.py @@ -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" ) diff --git a/backend/proteins/models/__init__.py b/backend/proteins/models/__init__.py index 57d092156..192a39917 100644 --- a/backend/proteins/models/__init__.py +++ b/backend/proteins/models/__init__.py @@ -1,15 +1,17 @@ from .bleach import BleachMeasurement from .collection import ProteinCollection +from .dye import Dye as Dye from .efficiency import OcFluorEff from .excerpt import Excerpt +from .fluorophore import Fluorophore from .lineage import Lineage from .microscope import FilterPlacement, Microscope, OpticalConfig from .organism import Organism from .oser import OSERMeasurement from .protein import Protein +from .protein import State as State from .snapgene import SnapGenePlasmid from .spectrum import Camera, Filter, Light, Spectrum -from .state import Dye, Fluorophore, State from .transition import StateTransition __all__ = [ diff --git a/backend/proteins/models/_old_protein.py b/backend/proteins/models/_old_protein.py new file mode 100644 index 000000000..1d8f732ce --- /dev/null +++ b/backend/proteins/models/_old_protein.py @@ -0,0 +1,637 @@ +import contextlib +import datetime +import io +import json +import os +import re +import sys +from random import choices +from subprocess import PIPE, run +from typing import TYPE_CHECKING + +from django.contrib.auth import get_user_model +from django.contrib.postgres.fields import ArrayField +from django.contrib.postgres.search import TrigramSimilarity +from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.db import models +from django.db.models import Count, F, Func, Q, Value +from django.urls import reverse +from django.utils.text import slugify +from model_utils import Choices +from model_utils.managers import QueryManager +from model_utils.models import StatusModel, TimeStampedModel +from reversion.models import Version + +from favit.models import Favorite +from fpseq import FPSeq +from references.models import Reference + +from .. import util +from ..util.helpers import get_base_name, get_color_group, mless, spectra_fig +from ..validators import protein_sequence_validator, validate_uniprot +from .collection import ProteinCollection +from .mixins import Authorable +from .spectrum import Spectrum + +if TYPE_CHECKING: + from proteins.models import SnapGenePlasmid, State # noqa: F401 + +User = get_user_model() + + +def prot_uuid(k=5, opts="ABCDEFGHJKLMNOPQRSTUVWXYZ123456789"): + i = "".join(choices(opts, k=k)) + try: + Protein.objects.get(uuid=i) + except Protein.DoesNotExist: + return i + else: + return prot_uuid(k, opts) + + +def findname(name): + queries = [ + {"name__iexact": name}, + {"aliases__icontains": name}, + # {'name__icontains': name}, + {"name__iexact": re.sub(r" \((Before|Planar|wild).*", "", name)}, + {"aliases__icontains": re.sub(r" \((Before|Planar|wild).*", "", name)}, + # {'name__icontains': re.sub(r' \((Before|Planar).*', '', name)}, + {"name__iexact": name.strip("1")}, + # {'name__icontains': name.strip('1')}, + ] + for query in queries: + with contextlib.suppress(Exception): + return Protein.objects.get(**query) + return None + + +class ProteinQuerySet(models.QuerySet): + def fasta(self): + seqs = list(self.exclude(seq__isnull=True).values("uuid", "name", "seq")) + for s in seqs: + s["name"] = s["name"].replace("\u03b1", "-alpha").replace("β", "-beta") + return io.StringIO("\n".join([">{uuid} {name}\n{seq}".format(**s) for s in seqs])) + + def to_tree(self, output="clw"): + fasta = self.fasta() + binary = "bin/muscle_" + ("osx" if sys.platform == "darwin" else "nix") + cmd = [binary] + # faster + cmd += ["-maxiters", "2", "-diags", "-quiet", f"-{output}"] + # make tree + cmd += ["-cluster", "neighborjoining", "-tree2", "tree.phy"] + result = run(cmd, input=fasta.read(), stdout=PIPE, encoding="ascii") + with open("tree.phy") as handle: + newick = handle.read().replace("\n", "") + os.remove("tree.phy") + return result.stdout, newick + + +class ProteinManager(models.Manager): + def deep_get(self, name): + slug = slugify(name) + with contextlib.suppress(Protein.DoesNotExist): + return self.get(slug=slug) + aliases_lower = Func(Func(F("aliases"), function="unnest"), function="LOWER") + remove_space = Func(aliases_lower, Value(" "), Value("-"), function="replace") + final = Func(remove_space, Value("."), Value(""), function="replace") + D = dict(Protein.objects.annotate(aka=final).values_list("aka", "id")) + if slug in D: + return self.get(id=D[slug]) + else: + raise Protein.DoesNotExist("Protein matching query does not exist.") + + def get_queryset(self): + return ProteinQuerySet(self.model, using=self._db) + + def with_counts(self): + from django.db import connection + + with connection.cursor() as cursor: + cursor.execute( + """ + SELECT p.id, p.name, p.slug, COUNT(*) + FROM proteins_protein p, proteins_state s + WHERE p.id = s.protein_id + GROUP BY p.id, p.name, p.slug + ORDER BY COUNT(*) DESC""" + ) + result_list = [] + for row in cursor.fetchall(): + p = self.model(id=row[0], name=row[1], slug=row[2]) + p.num_states = row[3] + result_list.append(p) + return result_list + + def annotated(self): + return self.get_queryset().annotate(Count("states"), Count("transitions")) + + def with_spectra(self, twoponly=False): + qs = self.get_queryset().filter(states__spectra__isnull=False).distinct() + if not twoponly: + # hacky way to remove 2p only spectra + qs = qs.annotate(stypes=Count("states__spectra__subtype")).filter(stypes__gt=1) + return qs + + def find_similar(self, name, similarity=0.2): + return ( + self.get_queryset() + .annotate(similarity=TrigramSimilarity("name", name)) + .filter(similarity__gt=similarity) + .order_by("-similarity") + ) + + +class SequenceField(models.CharField): + def __init__(self, *args, **kwargs): + kwargs["max_length"] = 1024 + kwargs["validators"] = [protein_sequence_validator] + super().__init__(*args, **kwargs) + + def deconstruct(self): + name, path, args, kwargs = super().deconstruct() + del kwargs["max_length"] + del kwargs["validators"] + return name, path, args, kwargs + + def from_db_value(self, value, expression, connection): + # Skip validation for database values - they're already validated + return FPSeq(value, validate=False) if value else None + + def to_python(self, value): + if isinstance(value, FPSeq): + return value + # New values should still be validated + return FPSeq(value, validate=True) if value else None + + def get_prep_value(self, value): + return str(value) if value else None + + +# this is a hack to allow for reversions of proteins to work with Null chromophores +# this makes sure that a None value is converted to an empty string +class _NonNullChar(models.CharField): + def to_python(self, value): + return "" if value is None else super().to_python(value) + + +class Protein(Authorable, StatusModel, TimeStampedModel): + """Protein class to store individual proteins, each with a unique AA sequence and name""" + + STATUS = Choices("pending", "approved", "hidden") + + MONOMER = "m" + DIMER = "d" + TANDEM_DIMER = "td" + WEAK_DIMER = "wd" + TETRAMER = "t" + AGG_CHOICES = ( + (MONOMER, "Monomer"), + (DIMER, "Dimer"), + (TANDEM_DIMER, "Tandem dimer"), + (WEAK_DIMER, "Weak dimer"), + (TETRAMER, "Tetramer"), + ) + + BASIC = "b" + PHOTOACTIVATABLE = "pa" + PHOTOSWITCHABLE = "ps" + PHOTOCONVERTIBLE = "pc" + MULTIPHOTOCHROMIC = "mp" + TIMER = "t" + OTHER = "o" + SWITCHING_CHOICES = ( + (BASIC, "Basic"), + (PHOTOACTIVATABLE, "Photoactivatable"), + (PHOTOSWITCHABLE, "Photoswitchable"), + (PHOTOCONVERTIBLE, "Photoconvertible"), + (MULTIPHOTOCHROMIC, "Multi-photochromic"), # both convertible and switchable + (OTHER, "Multistate"), + (TIMER, "Timer"), + ) + + BILIRUBIN = "br" + BILIVERDIN = "bv" + FLAVIN = "fl" + PHYCOCYANOBILIN = "pc" + RIBITYL_LUMAZINE = "rl" + COFACTOR_CHOICES = ( + (BILIRUBIN, "Bilirubin"), + (BILIVERDIN, "Biliverdin"), + (FLAVIN, "Flavin"), + (PHYCOCYANOBILIN, "Phycocyanobilin"), + (RIBITYL_LUMAZINE, "ribityl-lumazine"), + ) + + # Attributes + # uuid = models.UUIDField(default=uuid_lib.uuid4, editable=False, unique=True) # for API + uuid = models.CharField( + max_length=5, + default=prot_uuid, + editable=False, + unique=True, + db_index=True, + verbose_name="FPbase ID", + ) + name = models.CharField(max_length=128, help_text="Name of the fluorescent protein", db_index=True) + slug = models.SlugField(max_length=64, unique=True, help_text="URL slug for the protein") # for generating urls + base_name = models.CharField(max_length=128) # easily searchable "family" name + aliases = ArrayField(models.CharField(max_length=200), blank=True, null=True) + chromophore = _NonNullChar(max_length=5, blank=True, default="") + seq_validated = models.BooleanField(default=False, help_text="Sequence has been validated by a moderator") + # seq must be nullable because of uniqueness contraints + seq = SequenceField( + unique=True, + blank=True, + null=True, + verbose_name="Sequence", + help_text="Amino acid sequence (IPG ID is preferred)", + ) + seq_comment = models.CharField( + max_length=512, + blank=True, + help_text="if necessary, comment on source of sequence", + ) + + pdb = ArrayField( + models.CharField(max_length=4), + blank=True, + null=True, + verbose_name="Protein DataBank IDs", + ) + genbank = models.CharField( + max_length=12, + null=True, + blank=True, + unique=True, + verbose_name="Genbank Accession", + help_text="NCBI Genbank Accession", + ) + uniprot = models.CharField( + max_length=10, + null=True, + blank=True, + unique=True, + verbose_name="UniProtKB Accession", + validators=[validate_uniprot], + ) + ipg_id = models.CharField( + max_length=12, + null=True, + blank=True, + unique=True, + verbose_name="IPG ID", + help_text="Identical Protein Group ID at Pubmed", + ) # identical protein group uid + mw = models.FloatField(null=True, blank=True, help_text="Molecular Weight") # molecular weight + agg = models.CharField( + max_length=2, + choices=AGG_CHOICES, + blank=True, + verbose_name="Oligomerization", + help_text="Oligomerization tendency", + ) + oser = models.FloatField(null=True, blank=True, help_text="OSER score") # molecular weight + switch_type = models.CharField( + max_length=2, + choices=SWITCHING_CHOICES, + blank=True, + default="b", + verbose_name="Switching Type", + help_text="Photoswitching type (basic if none)", + ) + blurb = models.TextField(max_length=512, blank=True, help_text="Brief descriptive blurb") + cofactor = models.CharField( + max_length=2, + choices=COFACTOR_CHOICES, + blank=True, + help_text="Required for fluorescence", + ) + + # Relations + parent_organism = models.ForeignKey( + "Organism", + related_name="proteins", + verbose_name="Parental organism", + on_delete=models.SET_NULL, + blank=True, + null=True, + help_text="Organism from which the protein was engineered", + ) + primary_reference = models.ForeignKey( + Reference, + related_name="primary_proteins", + verbose_name="Primary Reference", + blank=True, + null=True, + on_delete=models.SET_NULL, + help_text="Preferably the publication that introduced the protein", + ) # usually, the original paper that published the protein + references = models.ManyToManyField( + Reference, related_name="proteins", blank=True + ) # all papers that reference the protein + # FRET_partner = models.ManyToManyField('self', symmetrical=False, through='FRETpair', blank=True) + + default_state_id: int | None + default_state = models.ForeignKey["State | None"]( + "State", + related_name="default_for", + blank=True, + null=True, + on_delete=models.SET_NULL, + ) + + if TYPE_CHECKING: + snapgene_plasmids = models.ManyToManyField["SnapGenePlasmid", "Protein"] + else: + snapgene_plasmids = models.ManyToManyField( + "SnapGenePlasmid", + related_name="proteins", + blank=True, + help_text="Associated SnapGene plasmids", + ) + + # __original_ipg_id = None + + # managers + objects = ProteinManager() + visible = QueryManager(~Q(status="hidden")) + + def mutations_from_root(self): + try: + root = self.lineage.get_root() + if root.protein.seq and self.seq: + return root.protein.seq.mutations_to(self.seq) + except ObjectDoesNotExist: + return None + + @property + def mless(self): + return mless(self.name) + + @property + def description(self): + return util.long_blurb(self) + + @property + def _base_name(self): + '''return core name of protein, stripping prefixes like "m" or "Tag"''' + return get_base_name(self.name) + + @property + def versions(self): + return Version.objects.get_for_object(self) + + def last_approved_version(self): + if self.status == "approved": + return self + try: + return ( + Version.objects.get_for_object(self).filter(serialized_data__contains='"status": "approved"').first() + ) + except Exception: + return None + + @property + def additional_references(self): + return self.references.exclude(id=self.primary_reference_id).order_by("-year") + + @property + def em_css(self): + if self.states.count() > 1: + from collections import OrderedDict + + stops = OrderedDict({st.emhex: "" for st in self.states.all()}) + bgs = [] + stepsize = int(100 / (len(stops) + 1)) + sub = 0 + for i, _hex in enumerate(stops): + if _hex == "#000": + sub = 18 + bgs.append(f"{_hex} {(i + 1) * stepsize - sub}%") + return f"linear-gradient(90deg, {', '.join(bgs)})" + elif self.default_state: + return self.default_state.emhex + else: + return "repeating-linear-gradient(-45deg,#333,#333 8px,#444 8px,#444 16px);" + + @property + def em_svg(self): + if self.states.count() <= 1: + return self.default_state.emhex if self.default_state else "?" + stops = [st.emhex for st in self.states.all()] + stepsize = int(100 / (len(stops) + 1)) + svgdef = "linear:" + for i, color in enumerate(stops): + perc = (i + 1.0) * stepsize + if color == "#000": + perc *= 0.2 + svgdef += f'' + return svgdef + + @property + def color(self): + try: + return get_color_group(self.default_state.ex_max, self.default_state.em_max)[0] # pyright: ignore + except Exception: + return "" + + # Methods + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse("proteins:protein-detail", args=[self.slug]) + + def has_default(self): + return bool(self.default_state) + + def mutations_to(self, other, **kwargs): + if isinstance(other, Protein): + other = other.seq + return self.seq.mutations_to(other, **kwargs) if self.seq and other else None + + def mutations_from(self, other, **kwargs): + if isinstance(other, Protein): + other = other.seq + return other.seq.mutations_to(self.seq, **kwargs) if (self.seq and other) else None + + def has_spectra(self): + return any(state.has_spectra() for state in self.states.all()) + + def has_bleach_measurements(self): + return self.states.filter(bleach_measurements__isnull=False).exists() + + def d3_spectra(self): + spectra = [] + for state in self.states.all(): + spectra.extend(state.d3_dicts()) + return json.dumps(spectra) + + def spectra_img(self, fmt="svg", output=None, **kwargs): + spectra = list(Spectrum.objects.filter(owner_state__protein=self).exclude(subtype="2p")) + title = self.name if kwargs.pop("title", False) else None + if kwargs.get("twitter", False): + title = self.name + info = "" + if self.default_state: + info += f"Ex/Em λ: {self.default_state.ex_max}/{self.default_state.em_max}" + info += f"\nEC: {self.default_state.ext_coeff} QY: {self.default_state.qy}" + return spectra_fig(spectra, fmt, output, title=title, info=info, **kwargs) + + def set_default_state(self) -> bool: + # FIXME: should allow control of default states in form + # if only 1 state, make it the default state + if not self.default_state or self.default_state.is_dark: + if self.states.count() == 1 and not self.states.first().is_dark: + self.default_state = self.states.first() + # otherwise use farthest red non-dark state + elif self.states.count() > 1: + self.default_state = self.states.exclude(is_dark=True).order_by("-em_max").first() + return True + return False + + def clean(self): + errors = {} + # Don't allow basic switch_types to have more than one state. + # if self.switch_type == 'b' and self.states.count() > 1: + # errors.update({'switch_type': 'Basic (non photoconvertible) proteins ' + # 'cannot have more than one state.'}) + if self.pdb: + self.pdb = list(set(self.pdb)) + for item in self.pdb: + if Protein.objects.exclude(id=self.id).filter(pdb__contains=[item]).exists(): + p = Protein.objects.filter(pdb__contains=[item]).first() + errors["pdb"] = f"PDB ID {item} is already in use by protein {p.name}" + + if errors: + raise ValidationError(errors) + + def save(self, *args, **kwargs): + self.slug = slugify(self.name) + self.base_name = self._base_name + super().save(*args, **kwargs) + if self.set_default_state(): + super().save() + # self.__original_ipg_id = self.ipg_id + + # Meta + class Meta: + ordering = ["name"] + indexes = [ + models.Index(fields=["status"], name="protein_status_idx"), + ] + + def history(self, ignoreKeys=()): + from proteins.util.history import get_history + + return get_history(self, ignoreKeys) + + # ################################## + # for algolia index + + def is_visible(self): + return self.status != "hidden" + + def img_url(self): + if self.has_spectra(): + return ( + "https://www.fpbase.org" + + reverse("proteins:spectra-img", args=[self.slug]) + + ".png?xlabels=0&xlim=400,800" + ) + else: + return None + + def tags(self): + tags = [self.get_switch_type_display(), self.get_agg_display(), self.color] + return [i for i in tags if i] + + def date_published(self, norm=False): + d = self.primary_reference.date if self.primary_reference else None + if norm: + return (d.year - 1992) / (datetime.datetime.now(datetime.UTC).year - 1992) if d else 0 + return datetime.datetime.combine(d, datetime.datetime.min.time()) if d else None + + def n_faves(self, norm=False): + nf = Favorite.objects.for_model(Protein).filter(target_object_id=self.id).count() + if norm: + from collections import Counter + + mx = Counter(Favorite.objects.for_model(Protein).values_list("target_object_id", flat=True)).most_common(1) + mx = mx[0][1] if mx else 1 + return nf / mx + return nf + + def n_cols(self): + return ProteinCollection.objects.filter(proteins=self.id).count() + + def ga_views(self, period="month", norm=False): + from proteins.extrest.ga import cached_ga_popular + + try: + hits = cached_ga_popular()[period] + return next( + ( + rating / max(list(zip(*hits))[2]) if norm else rating + for slug, _name, rating in hits + if slug == self.slug + ), + 0, + ) + except Exception: + return 0 + + def switchType(self): + return self.get_switch_type_display() + + def _agg(self): + return self.get_agg_display() + + def url(self): + return self.get_absolute_url() + + def ex(self): + if not self.states.exists(): + return None + ex = [s.ex_max for s in self.states.all()] + return ex[0] if len(ex) == 1 else ex + + def em(self): + if not self.states.exists(): + return None + em = [s.em_max for s in self.states.all()] + return em[0] if len(em) == 1 else em + + def pka(self): + if not self.states.exists(): + return None + n = [s.pka for s in self.states.all()] + return n[0] if len(n) == 1 else n + + def ec(self): + if not self.states.exists(): + return None + n = [s.ext_coeff for s in self.states.all()] + return n[0] if len(n) == 1 else n + + def qy(self): + if not self.states.exists(): + return None + n = [s.qy for s in self.states.all()] + return n[0] if len(n) == 1 else n + + def rank(self): + # max rank is 1 + return ( + 0.5 * self.date_published(norm=True) + 0.6 * self.ga_views(norm=True) + 1.0 * self.n_faves(norm=True) + ) / 2.5 + + def local_brightness(self): + if self.states.exists(): + return max(s.local_brightness for s in self.states.all()) + + def first_author(self): + if self.primary_reference and self.primary_reference.first_author: + return self.primary_reference.first_author.family diff --git a/backend/proteins/models/state.py b/backend/proteins/models/_old_state.py similarity index 99% rename from backend/proteins/models/state.py rename to backend/proteins/models/_old_state.py index 35d30ccd5..9b143c5f5 100644 --- a/backend/proteins/models/state.py +++ b/backend/proteins/models/_old_state.py @@ -22,6 +22,7 @@ class Fluorophore(SpectrumOwner): validators=[MinValueValidator(300), MaxValueValidator(1000)], db_index=True, ) + twop_ex_max = models.PositiveSmallIntegerField( blank=True, null=True, diff --git a/backend/proteins/models/_sequence_field.py b/backend/proteins/models/_sequence_field.py new file mode 100644 index 000000000..eb6a4076b --- /dev/null +++ b/backend/proteins/models/_sequence_field.py @@ -0,0 +1,30 @@ +from django.db import models + +from fpseq import FPSeq +from proteins.validators import protein_sequence_validator + + +class SequenceField(models.CharField): + def __init__(self, *args, **kwargs): + kwargs["max_length"] = 1024 + kwargs["validators"] = [protein_sequence_validator] + super().__init__(*args, **kwargs) + + def deconstruct(self): + name, path, args, kwargs = super().deconstruct() + del kwargs["max_length"] + del kwargs["validators"] + return name, path, args, kwargs + + def from_db_value(self, value, expression, connection): + # Skip validation for database values - they're already validated + return FPSeq(value, validate=False) if value else None + + def to_python(self, value): + if isinstance(value, FPSeq): + return value + # New values should still be validated + return FPSeq(value, validate=True) if value else None + + def get_prep_value(self, value): + return str(value) if value else None diff --git a/backend/proteins/models/dye.py b/backend/proteins/models/dye.py new file mode 100644 index 000000000..ffbd59ee2 --- /dev/null +++ b/backend/proteins/models/dye.py @@ -0,0 +1,193 @@ +from typing import TYPE_CHECKING + +from django.contrib.postgres.fields import ArrayField +from django.db import models +from model_utils.models import TimeStampedModel + +from proteins.models.fluorophore import Fluorophore +from proteins.models.mixins import Authorable, Product + +if TYPE_CHECKING: + from references.models import Reference # noqa: F401 + + +class Dye(Authorable, TimeStampedModel, Product): # TODO: rename to SmallMolecule + """ + Represents a distinct organic fluorophore or chromophore. + This is the 'Parent' entity for reactive derivatives. + """ + + # --- Identification --- + common_name = models.CharField(max_length=255, db_index=True) + slug = models.SlugField(unique=True) + + # Synonyms allow users to find "FITC" when searching "Fluorescein" + synonyms = ArrayField(models.CharField(max_length=255), blank=True, default=list) + + # --- Structural Status (The "Regret-Proof" Field) --- + # Allows entry of proprietary dyes without forcing a fake structure. + STRUCTURE_STATUS_CHOICES = [ + ("DEFINED", "Defined Structure"), + ("PROPRIETARY", "Proprietary / Unknown Structure"), + ] + structural_status = models.CharField(max_length=20, choices=STRUCTURE_STATUS_CHOICES, default="DEFINED") + + # --- Chemical Graph Data (Nullable for Proprietary Dyes) --- + # We prioritize MolBlock for rendering, InChIKey for de-duplication. + canonical_smiles = models.TextField(blank=True) + inchi = models.TextField(blank=True) + inchikey = models.CharField(max_length=27, blank=True, db_index=True) + molblock = models.TextField(blank=True, help_text="V3000 Molfile for precise rendering") + + # --- Hierarchy & Ontology --- + # Handles FITC (Parent) vs 5-FITC (Child) relationship + parent_mixture = models.ForeignKey( + "self", on_delete=models.SET_NULL, null=True, blank=True, related_name="isomers" + ) + + # Automated classification (e.g., "Rhodamine", "Cyanine", "BODIPY") + # Populated via ClassyFire API or manual curation + chemical_class = models.CharField(max_length=100, blank=True, db_index=True) + + # --- Intrinsic Physics --- + # Critical for fluorogenic dyes (JF dyes, SiR-tubulin) + # Describes the Lactone-Zwitterion equilibrium constant. + equilibrium_constant_klz = models.FloatField( + null=True, + blank=True, + help_text="Equilibrium constant between non-fluorescent lactone and fluorescent zwitterion.", + ) + + class Meta: + # Enforce uniqueness only on defined structures to allow multiple proprietary entries + constraints = [ + models.UniqueConstraint( + fields=["inchikey"], + name="unique_defined_molecule", + condition=models.Q(structural_status="DEFINED"), + ) + ] + + def get_primary_spectrum(self): + """Returns the 'Reference' DyeState (e.g. Protein-bound) for display cards.""" + return self.states.filter(is_reference=True).first() + + +# Instead of storing spectral data directly in the SmallMolecule, we link it here. +# This allows us to store "Alexa 488 in PBS" and "Alexa 488 in Ethanol" as valid, +# separate datasets. +class DyeState(Fluorophore): + """ + Represents a SmallMolecule in a specific environmental context. + This holds the actual spectral data. + """ + + dye_id: int + dye = models.ForeignKey["Dye"](Dye, on_delete=models.CASCADE, related_name="states") + + # --- Context --- + name = models.CharField(max_length=255, help_text="e.g., 'Bound to DNA' or 'In Methanol'") + solvent = models.CharField(max_length=100, default="PBS") + ph = models.FloatField(default=7.4) + + # --- Environmental Categorization --- + # Helps the UI decide which spectrum to show for a specific query. + ENVIRONMENT_CHOICES = [] + environment = models.CharField(max_length=20, choices=ENVIRONMENT_CHOICES, default="FREE") + + # --- Logic --- + is_reference = models.BooleanField( + default=False, help_text="If True, this is the default state shown on the dye summary card." + ) + + def save(self, *args, **kwargs): + self.entity_type = "dye" + super().save(*args, **kwargs) + + +# This section handles the commercial reality of purchasing dyes. +# +# It separates "Chemist Tools" (Reactive Dyes) from "Biologist Tools" (Antibody Conjugates). + + +class ReactiveDerivative(models.Model): + """A sold product derived from the SmallMolecule. + + e.g., 'Janelia Fluor 549 NHS Ester' or 'JF549-HaloTag Ligand' + These are the products users buy to perform conjugation + (e.g., NHS esters, Maleimides, HaloTag Ligands). + """ + + core_dye_id: int + core_dye = models.ForeignKey["Dye"]( + Dye, + on_delete=models.CASCADE, + related_name="derivatives", + ) + + # --- Chemistry --- + REACTIVE_GROUP_CHOICES = [ + ("NHS_ESTER", "NHS Ester"), + ("HALO_TAG", "HaloTag Ligand"), + ("SNAP_TAG", "SNAP-Tag Ligand"), + ("CLIP_TAG", "CLIP-Tag Ligand"), + ("MALEIMIDE", "Maleimide"), + ("AZIDE", "Azide"), + ("ALKYNE", "Alkyne"), + ("BIOTIN", "Biotin"), + ("OTHER", "Other"), + ] + reactive_group = models.CharField(max_length=10, choices=REACTIVE_GROUP_CHOICES) + + # Specific structure of the linker/handle (distinct from core dye) + full_smiles = models.TextField(blank=True, help_text="Structure of the complete reactive molecule") + molecular_weight = models.FloatField() + + # --- Vendor Info --- + vendor = models.CharField(max_length=100) + catalog_number = models.CharField(max_length=100) + + def __str__(self) -> str: + return f"{self.core_dye.common_name} - {self.reactive_group} ({self.vendor} {self.catalog_number})" + + +# Architectural Decision: We do not create a new DyeState or SmallMolecule for every +# antibody conjugate. Instead, we use a relational link. +# +# When a user views "Alexa 488 Anti-CD4", the system pulls: +# 1. Biology from the Antibody model. +# 2. Physics from the SmallMolecule model (specifically, the DyeState where +# environment='PROTEIN_BOUND'). + + +# class AntibodyConjugate(models.Model): +# """ +# A virtual entity representing a commercial antibody-dye pairing. +# Does NOT store spectral data; inherits it from the Fluorophore. +# """ + +# # The "Ingredients" +# fluorophore_id: int +# fluorophore = models.ForeignKey["Dye"](SmallMolecule, on_delete=models.PROTECT) +# antibody_id: int +# antibody = models.ForeignKey["Antibody"]("Antibody", on_delete=models.PROTECT) # Define Antibody model elsewhere + +# # Product Details +# vendor = models.CharField(max_length=100) +# catalog_number = models.CharField(max_length=100) + +# # Heterogeneity +# f_p_ratio = models.FloatField( +# null=True, blank=True, help_text="Average Fluorophore/Protein ratio (Degree of Labeling)" +# ) + +# def get_display_spectrum(self): +# """ +# Logic to fetch the correct spectrum. +# Prioritizes a specific 'Protein Bound' state if available. +# """ +# protein_state = self.fluorophore.states.filter(environment="PROTEIN_BOUND").first() +# if protein_state: +# return protein_state +# # Fallback to reference state if no specific protein-bound data exists +# return self.fluorophore.get_primary_spectrum() diff --git a/backend/proteins/models/efficiency.py b/backend/proteins/models/efficiency.py index a9677184f..2d96010c6 100644 --- a/backend/proteins/models/efficiency.py +++ b/backend/proteins/models/efficiency.py @@ -6,8 +6,9 @@ from django.db.models import F, Max, OuterRef, Q, Subquery from model_utils.models import TimeStampedModel -from ..util.efficiency import oc_efficiency_report -from .state import Dye, State +from proteins.models.dye import Dye as Dye +from proteins.models.protein import State as State +from proteins.util.efficiency import oc_efficiency_report class OcFluorEffQuerySet(models.QuerySet): diff --git a/backend/proteins/models/fluorescence_data.py b/backend/proteins/models/fluorescence_data.py new file mode 100644 index 000000000..5891516ff --- /dev/null +++ b/backend/proteins/models/fluorescence_data.py @@ -0,0 +1,101 @@ +from django.core.validators import MaxValueValidator, MinValueValidator +from django.db import models +from model_utils.models import TimeStampedModel + +from proteins.models.mixins import Authorable +from proteins.util.helpers import wave_to_hex + + +class AbstractFluorescenceData(Authorable, TimeStampedModel, models.Model): + """Defines the physics schema. + Used by both the Measurement (Input) and Fluorophore (Output/Cache). + """ + + ex_max = models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[MinValueValidator(300), MaxValueValidator(900)], + db_index=True, + help_text="Excitation maximum (nm)", + ) + em_max = models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[MinValueValidator(300), MaxValueValidator(1000)], + db_index=True, + help_text="Emission maximum (nm)", + ) + emhex = models.CharField(max_length=7, blank=True) + exhex = models.CharField(max_length=7, blank=True) + + # core properties + ext_coeff = models.IntegerField( + blank=True, + null=True, + verbose_name="Extinction Coefficient (M-1 cm-1)", + validators=[MinValueValidator(0), MaxValueValidator(300000)], + ) + qy = models.FloatField( + null=True, + blank=True, + verbose_name="Quantum Yield", + validators=[MinValueValidator(0), MaxValueValidator(1)], + ) + brightness = models.FloatField(null=True, blank=True, editable=False) + lifetime = models.FloatField( + null=True, + blank=True, + help_text="Lifetime (ns)", + validators=[MinValueValidator(0), MaxValueValidator(20)], + ) + pka = models.FloatField( + null=True, + blank=True, + verbose_name="pKa", + validators=[MinValueValidator(2), MaxValueValidator(12)], + ) + + # two photon properties + twop_ex_max = models.PositiveSmallIntegerField( + blank=True, + null=True, + verbose_name="Peak 2P excitation", + validators=[MinValueValidator(700), MaxValueValidator(1600)], + db_index=True, + ) + twop_peakGM = models.FloatField( + null=True, + blank=True, + verbose_name="Peak 2P cross-section of S0->S1 (GM)", + validators=[MinValueValidator(0), MaxValueValidator(200)], + ) + twop_qy = models.FloatField( + null=True, + blank=True, + verbose_name="2P Quantum Yield", + validators=[MinValueValidator(0), MaxValueValidator(1)], + ) + + # extra + + is_dark = models.BooleanField( + default=False, + verbose_name="Dark State", + help_text="This state does not fluorescence", + ) + + class Meta: + abstract = True + + def save(self, *args, **kwargs): + if self.qy and self.ext_coeff: + self.brightness = float(round(self.ext_coeff * self.qy / 1000, 2)) + + self.emhex = "#000" if self.is_dark else wave_to_hex(self.em_max) + self.exhex = wave_to_hex(self.ex_max) + super().save(*args, **kwargs) + + @classmethod + def get_measurable_fields(cls): + """Helper to return list of field names: ['ex_max', 'qy', ...]""" + return [f.name for f in cls._meta.fields] diff --git a/backend/proteins/models/fluorescence_measurement.py b/backend/proteins/models/fluorescence_measurement.py new file mode 100644 index 000000000..341515796 --- /dev/null +++ b/backend/proteins/models/fluorescence_measurement.py @@ -0,0 +1,40 @@ +from typing import TYPE_CHECKING + +from django.db import models + +from proteins.models.fluorescence_data import AbstractFluorescenceData + +if TYPE_CHECKING: + from references.models import Reference # noqa: F401 + + from .fluorophore import Fluorophore # noqa: F401 + + +# The "evidence" +class FluorescenceMeasurement(AbstractFluorescenceData): + """Raw data points from a specific reference.""" + + fluorophore_id: int + fluorophore = models.ForeignKey["Fluorophore"]( + "Fluorophore", related_name="measurements", on_delete=models.CASCADE + ) + reference_id: int + reference = models.ForeignKey["Reference"]("Reference", on_delete=models.CASCADE) + + # Metadata specific to the act of measuring + date_measured = models.DateField(null=True, blank=True) + conditions = models.TextField(blank=True, help_text="pH, solvent, temp, etc.") + + # Curator Override + is_trusted = models.BooleanField(default=False, help_text="If True, this measurement overrides others.") + + def save(self, *args, **kwargs) -> None: + super().save(*args, **kwargs) + # Always keep the parent cache in sync + self.fluorophore.rebuild_attributes() + + def delete(self, *args, **kwargs) -> None: + f = self.fluorophore + super().delete(*args, **kwargs) + # Always keep the parent cache in sync + f.rebuild_attributes() diff --git a/backend/proteins/models/fluorophore.py b/backend/proteins/models/fluorophore.py new file mode 100644 index 000000000..58c4dbe38 --- /dev/null +++ b/backend/proteins/models/fluorophore.py @@ -0,0 +1,185 @@ +from typing import TYPE_CHECKING, Literal + +from django.db import models + +from proteins.models.fluorescence_data import AbstractFluorescenceData + +if TYPE_CHECKING: + from django.db.models.manager import RelatedManager + + from proteins.models import Spectrum + + +# The Canonical Parent (The Summary) +class Fluorophore(AbstractFluorescenceData): + """The database table for 'Things That Glow'. + + Polymorphic Fluorophore Parent. + + While fluorophores support multiple measurements of fluorescence data, + `Fluorophore` also inherits `AbstractFluorescenceData`, and the values accessible + on this instance serve as the canonical (cached, "published", "composited") + fluorescence properties for this entity. + + Contains the 'Accepted/Cached' values for generic querying. + Acts as the materialized view of the 'best' measurements. + """ + + class EntityTypes(models.TextChoices): + PROTEIN = ("protein", "Protein") + DYE = ("dye", "Dye") + + # Identity (Hoisted for performance) + label = models.CharField(max_length=255, db_index=True) + slug = models.SlugField(unique=True) + entity_type = models.CharField(max_length=10, choices=EntityTypes, db_index=True) + + # Lineage Tracking + # Maps field names to Measurement IDs. e.g., {'ex_max': 102, 'qy': 105} + source_map = models.JSONField(default=dict, blank=True) + + if TYPE_CHECKING: + spectra = RelatedManager["Spectrum"]() + + class Meta: + indexes = [ + models.Index(fields=["ex_max"]), + models.Index(fields=["em_max"]), + ] + + def __str__(self): + return self.label + + def rebuild_attributes(self): + """The Compositing Engine. + + Aggregates all measurements to determine the current canonical values. + """ + # 1. Fetch all measurements, sorted by priority: + # Curator Trusted > Primary Reference > Most Recent Date + measurements = self.measurements.select_related("reference").order_by( + "-is_trusted", + # Assuming you have a helper to check if ref is primary for the owner + # This part requires custom logic depending on if self is Protein or Dye + # '-is_primary_ref', + "-date_measured", + ) + + measurable_fields = AbstractFluorescenceData.get_measurable_fields() + new_values = {} + new_source_map = {} + + # 2. Waterfall Logic: Find the first non-null value for each field + for field in measurable_fields: + found_val = None + found_source_id = None + + for m in measurements: + val = getattr(m, field) + if val is not None: + found_val = val + found_source_id = m.id + break + + new_values[field] = found_val + if found_source_id: + new_source_map[field] = found_source_id + + # 3. Update Cache + for key, val in new_values.items(): + setattr(self, key, val) + + self.source_map = new_source_map + + # 4. Refresh Label (Hoisting) + if hasattr(self, "protein_state"): + ps = self.protein_state + self.label = f"{ps.protein.name} ({ps.name})" + elif hasattr(self, "dye_state"): + ds = self.dye_state + self.label = f"{ds.dye.name} ({ds.name})" + + self.save() + + @property + def fluor_name(self) -> str: + if hasattr(self, "protein"): + return self.protein.name + return self.name + + @property + def abs_spectrum(self) -> "Spectrum | None": + spect = [f for f in self.spectra.all() if f.subtype == "ab"] + if len(spect) > 1: + raise AssertionError(f"multiple ex spectra found for {self}") + if len(spect): + return spect[0] + return None + + @property + def ex_spectrum(self) -> "Spectrum | None": + spect = [f for f in self.spectra.all() if f.subtype == "ex"] + if len(spect) > 1: + raise AssertionError(f"multiple ex spectra found for {self}") + if len(spect): + return spect[0] + return self.abs_spectrum + + @property + def em_spectrum(self) -> "Spectrum | None": + spect = [f for f in self.spectra.all() if f.subtype == "em"] + if len(spect) > 1: + raise AssertionError(f"multiple em spectra found for {self}") + if len(spect): + return spect[0] + return self.abs_spectrum + + @property + def twop_spectrum(self) -> "Spectrum | None": + spect = [f for f in self.spectra.all() if f.subtype == "2p"] + if len(spect) > 1: + raise AssertionError("multiple 2p spectra found") + if len(spect): + return spect[0] + return None + + @property + def bright_rel_egfp(self) -> float | None: + if self.brightness: + return self.brightness / 0.336 + return None + + @property + def stokes(self) -> float | None: + try: + return self.em_max - self.ex_max + except TypeError: + return None + + def has_spectra(self) -> bool: + if any([self.ex_spectrum, self.em_spectrum]): + return True + return False + + def ex_band(self, height=0.7) -> tuple[float, float] | Literal[False]: + return self.ex_spectrum.width(height) + + def em_band(self, height=0.7) -> tuple[float, float] | Literal[False]: + return self.em_spectrum.width(height) + + def within_ex_band(self, value, height=0.7) -> bool: + if band := self.ex_band(height): + minRange, maxRange = band + if minRange < value < maxRange: + return True + return False + + def within_em_band(self, value, height=0.7) -> bool: + if band := self.em_band(height): + minRange, maxRange = band + if minRange < value < maxRange: + return True + return False + + def d3_dicts(self) -> list[dict]: + return [spect.d3dict() for spect in self.spectra.all()] diff --git a/backend/proteins/models/lineage.py b/backend/proteins/models/lineage.py index de8472676..efbf0da2a 100644 --- a/backend/proteins/models/lineage.py +++ b/backend/proteins/models/lineage.py @@ -35,7 +35,7 @@ def get_prep_value(self, value): class Lineage(MPTTModel, TimeStampedModel, Authorable): - protein = models.OneToOneField("Protein", on_delete=models.CASCADE, related_name="lineage") + protein = models.OneToOneField["Protein"]("Protein", on_delete=models.CASCADE, related_name="lineage") parent = TreeForeignKey("self", on_delete=models.CASCADE, null=True, blank=True, related_name="children") reference = models.ForeignKey( Reference, diff --git a/backend/proteins/models/protein.py b/backend/proteins/models/protein.py index 1d8f732ce..acc62dedb 100644 --- a/backend/proteins/models/protein.py +++ b/backend/proteins/models/protein.py @@ -1,20 +1,21 @@ -import contextlib import datetime import io import json import os -import re import sys -from random import choices +from collections import Counter +from collections.abc import Sequence +from random import choice from subprocess import PIPE, run -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast -from django.contrib.auth import get_user_model +from django.contrib.contenttypes.fields import GenericRelation from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.search import TrigramSimilarity from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models -from django.db.models import Count, F, Func, Q, Value +from django.db.models import Count, Q from django.urls import reverse from django.utils.text import slugify from model_utils import Choices @@ -23,24 +24,32 @@ from reversion.models import Version from favit.models import Favorite -from fpseq import FPSeq +from proteins import util +from proteins.models._sequence_field import SequenceField +from proteins.models.collection import ProteinCollection +from proteins.models.fluorophore import Fluorophore +from proteins.models.mixins import Authorable +from proteins.models.spectrum import Spectrum +from proteins.util.helpers import get_base_name, get_color_group, mless, spectra_fig +from proteins.validators import validate_uniprot from references.models import Reference -from .. import util -from ..util.helpers import get_base_name, get_color_group, mless, spectra_fig -from ..validators import protein_sequence_validator, validate_uniprot -from .collection import ProteinCollection -from .mixins import Authorable -from .spectrum import Spectrum - if TYPE_CHECKING: - from proteins.models import SnapGenePlasmid, State # noqa: F401 + from django.db.models.manager import RelatedManager + from reversion.models import VersionQuerySet + + from proteins.models import Lineage, Organism, SnapGenePlasmid # noqa: F401 -User = get_user_model() +# this is a hack to allow for reversions of proteins to work with Null chromophores +# this makes sure that a None value is converted to an empty string +class _NonNullChar(models.CharField): + def to_python(self, value): + return "" if value is None else super().to_python(value) -def prot_uuid(k=5, opts="ABCDEFGHJKLMNOPQRSTUVWXYZ123456789"): - i = "".join(choices(opts, k=k)) + +def prot_uuid(k: int = 5, opts: Sequence[str] = "ABCDEFGHJKLMNOPQRSTUVWXYZ123456789") -> str: + i = "".join(choice(opts, k=k)) try: Protein.objects.get(uuid=i) except Protein.DoesNotExist: @@ -49,24 +58,7 @@ def prot_uuid(k=5, opts="ABCDEFGHJKLMNOPQRSTUVWXYZ123456789"): return prot_uuid(k, opts) -def findname(name): - queries = [ - {"name__iexact": name}, - {"aliases__icontains": name}, - # {'name__icontains': name}, - {"name__iexact": re.sub(r" \((Before|Planar|wild).*", "", name)}, - {"aliases__icontains": re.sub(r" \((Before|Planar|wild).*", "", name)}, - # {'name__icontains': re.sub(r' \((Before|Planar).*', '', name)}, - {"name__iexact": name.strip("1")}, - # {'name__icontains': name.strip('1')}, - ] - for query in queries: - with contextlib.suppress(Exception): - return Protein.objects.get(**query) - return None - - -class ProteinQuerySet(models.QuerySet): +class _ProteinQuerySet(models.QuerySet): def fasta(self): seqs = list(self.exclude(seq__isnull=True).values("uuid", "name", "seq")) for s in seqs: @@ -88,44 +80,9 @@ def to_tree(self, output="clw"): return result.stdout, newick -class ProteinManager(models.Manager): - def deep_get(self, name): - slug = slugify(name) - with contextlib.suppress(Protein.DoesNotExist): - return self.get(slug=slug) - aliases_lower = Func(Func(F("aliases"), function="unnest"), function="LOWER") - remove_space = Func(aliases_lower, Value(" "), Value("-"), function="replace") - final = Func(remove_space, Value("."), Value(""), function="replace") - D = dict(Protein.objects.annotate(aka=final).values_list("aka", "id")) - if slug in D: - return self.get(id=D[slug]) - else: - raise Protein.DoesNotExist("Protein matching query does not exist.") - +class _ProteinManager(models.Manager): def get_queryset(self): - return ProteinQuerySet(self.model, using=self._db) - - def with_counts(self): - from django.db import connection - - with connection.cursor() as cursor: - cursor.execute( - """ - SELECT p.id, p.name, p.slug, COUNT(*) - FROM proteins_protein p, proteins_state s - WHERE p.id = s.protein_id - GROUP BY p.id, p.name, p.slug - ORDER BY COUNT(*) DESC""" - ) - result_list = [] - for row in cursor.fetchall(): - p = self.model(id=row[0], name=row[1], slug=row[2]) - p.num_states = row[3] - result_list.append(p) - return result_list - - def annotated(self): - return self.get_queryset().annotate(Count("states"), Count("transitions")) + return _ProteinQuerySet(self.model, using=self._db) def with_spectra(self, twoponly=False): qs = self.get_queryset().filter(states__spectra__isnull=False).distinct() @@ -143,89 +100,34 @@ def find_similar(self, name, similarity=0.2): ) -class SequenceField(models.CharField): - def __init__(self, *args, **kwargs): - kwargs["max_length"] = 1024 - kwargs["validators"] = [protein_sequence_validator] - super().__init__(*args, **kwargs) - - def deconstruct(self): - name, path, args, kwargs = super().deconstruct() - del kwargs["max_length"] - del kwargs["validators"] - return name, path, args, kwargs - - def from_db_value(self, value, expression, connection): - # Skip validation for database values - they're already validated - return FPSeq(value, validate=False) if value else None - - def to_python(self, value): - if isinstance(value, FPSeq): - return value - # New values should still be validated - return FPSeq(value, validate=True) if value else None - - def get_prep_value(self, value): - return str(value) if value else None - - -# this is a hack to allow for reversions of proteins to work with Null chromophores -# this makes sure that a None value is converted to an empty string -class _NonNullChar(models.CharField): - def to_python(self, value): - return "" if value is None else super().to_python(value) - - class Protein(Authorable, StatusModel, TimeStampedModel): """Protein class to store individual proteins, each with a unique AA sequence and name""" STATUS = Choices("pending", "approved", "hidden") - MONOMER = "m" - DIMER = "d" - TANDEM_DIMER = "td" - WEAK_DIMER = "wd" - TETRAMER = "t" - AGG_CHOICES = ( - (MONOMER, "Monomer"), - (DIMER, "Dimer"), - (TANDEM_DIMER, "Tandem dimer"), - (WEAK_DIMER, "Weak dimer"), - (TETRAMER, "Tetramer"), - ) - - BASIC = "b" - PHOTOACTIVATABLE = "pa" - PHOTOSWITCHABLE = "ps" - PHOTOCONVERTIBLE = "pc" - MULTIPHOTOCHROMIC = "mp" - TIMER = "t" - OTHER = "o" - SWITCHING_CHOICES = ( - (BASIC, "Basic"), - (PHOTOACTIVATABLE, "Photoactivatable"), - (PHOTOSWITCHABLE, "Photoswitchable"), - (PHOTOCONVERTIBLE, "Photoconvertible"), - (MULTIPHOTOCHROMIC, "Multi-photochromic"), # both convertible and switchable - (OTHER, "Multistate"), - (TIMER, "Timer"), - ) + class AggChoices(models.TextChoices): + MONOMER = ("m", "Monomer") + DIMER = ("d", "Dimer") + TANDEM_DIMER = ("td", "Tandem dimer") + WEAK_DIMER = ("wd", "Weak dimer") + TETRAMER = ("t", "Tetramer") + + class SwitchingChoices(models.TextChoices): + BASIC = ("b", "Basic") + PHOTOACTIVATABLE = ("pa", "Photoactivatable") + PHOTOSWITCHABLE = ("ps", "Photoswitchable") + PHOTOCONVERTIBLE = ("pc", "Photoconvertible") + MULTIPHOTOCHROMIC = ("mp", "Multi-photochromic") + TIMER = ("t", "Multistate") + OTHER = ("o", "Timer") + + class CofactorChoices(models.TextChoices): + BILIRUBIN = ("br", "Bilirubin") + BILIVERDIN = ("bv", "Biliverdin") + FLAVIN = ("fl", "Flavin") + PHYCOCYANOBILIN = ("pc", "Phycocyanobilin") + RIBITYL_LUMAZINE = ("rl", "ribityl-lumazine") - BILIRUBIN = "br" - BILIVERDIN = "bv" - FLAVIN = "fl" - PHYCOCYANOBILIN = "pc" - RIBITYL_LUMAZINE = "rl" - COFACTOR_CHOICES = ( - (BILIRUBIN, "Bilirubin"), - (BILIVERDIN, "Biliverdin"), - (FLAVIN, "Flavin"), - (PHYCOCYANOBILIN, "Phycocyanobilin"), - (RIBITYL_LUMAZINE, "ribityl-lumazine"), - ) - - # Attributes - # uuid = models.UUIDField(default=uuid_lib.uuid4, editable=False, unique=True) # for API uuid = models.CharField( max_length=5, default=prot_uuid, @@ -283,11 +185,11 @@ class Protein(Authorable, StatusModel, TimeStampedModel): unique=True, verbose_name="IPG ID", help_text="Identical Protein Group ID at Pubmed", - ) # identical protein group uid + ) mw = models.FloatField(null=True, blank=True, help_text="Molecular Weight") # molecular weight agg = models.CharField( max_length=2, - choices=AGG_CHOICES, + choices=AggChoices, blank=True, verbose_name="Oligomerization", help_text="Oligomerization tendency", @@ -295,22 +197,23 @@ class Protein(Authorable, StatusModel, TimeStampedModel): oser = models.FloatField(null=True, blank=True, help_text="OSER score") # molecular weight switch_type = models.CharField( max_length=2, - choices=SWITCHING_CHOICES, + choices=SwitchingChoices, blank=True, - default="b", + default=SwitchingChoices.BASIC, verbose_name="Switching Type", help_text="Photoswitching type (basic if none)", ) blurb = models.TextField(max_length=512, blank=True, help_text="Brief descriptive blurb") cofactor = models.CharField( max_length=2, - choices=COFACTOR_CHOICES, + choices=CofactorChoices, blank=True, help_text="Required for fluorescence", ) # Relations - parent_organism = models.ForeignKey( + parent_organism_id: int | None + parent_organism = models.ForeignKey["Organism"]( "Organism", related_name="proteins", verbose_name="Parental organism", @@ -319,7 +222,9 @@ class Protein(Authorable, StatusModel, TimeStampedModel): null=True, help_text="Organism from which the protein was engineered", ) - primary_reference = models.ForeignKey( + + primary_reference_id: int | None + primary_reference = models.ForeignKey["Reference"]( Reference, related_name="primary_proteins", verbose_name="Primary Reference", @@ -327,11 +232,8 @@ class Protein(Authorable, StatusModel, TimeStampedModel): null=True, on_delete=models.SET_NULL, help_text="Preferably the publication that introduced the protein", - ) # usually, the original paper that published the protein - references = models.ManyToManyField( - Reference, related_name="proteins", blank=True - ) # all papers that reference the protein - # FRET_partner = models.ManyToManyField('self', symmetrical=False, through='FRETpair', blank=True) + ) + references = models.ManyToManyField(Reference, related_name="proteins", blank=True) default_state_id: int | None default_state = models.ForeignKey["State | None"]( @@ -343,6 +245,8 @@ class Protein(Authorable, StatusModel, TimeStampedModel): ) if TYPE_CHECKING: + states = RelatedManager["State"]() + lineage = RelatedManager["Lineage"]() snapgene_plasmids = models.ManyToManyField["SnapGenePlasmid", "Protein"] else: snapgene_plasmids = models.ManyToManyField( @@ -352,15 +256,13 @@ class Protein(Authorable, StatusModel, TimeStampedModel): help_text="Associated SnapGene plasmids", ) - # __original_ipg_id = None - # managers - objects = ProteinManager() + objects = _ProteinManager() visible = QueryManager(~Q(status="hidden")) def mutations_from_root(self): try: - root = self.lineage.get_root() + root = cast("Lineage", self.lineage.get_root()) if root.protein.seq and self.seq: return root.protein.seq.mutations_to(self.seq) except ObjectDoesNotExist: @@ -381,14 +283,16 @@ def _base_name(self): @property def versions(self): - return Version.objects.get_for_object(self) + version_objects = cast("VersionQuerySet", Version.objects) + return version_objects.get_for_object(self) def last_approved_version(self): if self.status == "approved": return self try: + version_objects = cast("VersionQuerySet", Version.objects) return ( - Version.objects.get_for_object(self).filter(serialized_data__contains='"status": "approved"').first() + version_objects.get_for_object(self).filter(serialized_data__contains='"status": "approved"').first() ) except Exception: return None @@ -400,9 +304,7 @@ def additional_references(self): @property def em_css(self): if self.states.count() > 1: - from collections import OrderedDict - - stops = OrderedDict({st.emhex: "" for st in self.states.all()}) + stops = {st.emhex: "" for st in self.states.all()} bgs = [] stepsize = int(100 / (len(stops) + 1)) sub = 0 @@ -494,10 +396,6 @@ def set_default_state(self) -> bool: def clean(self): errors = {} - # Don't allow basic switch_types to have more than one state. - # if self.switch_type == 'b' and self.states.count() > 1: - # errors.update({'switch_type': 'Basic (non photoconvertible) proteins ' - # 'cannot have more than one state.'}) if self.pdb: self.pdb = list(set(self.pdb)) for item in self.pdb: @@ -514,9 +412,7 @@ def save(self, *args, **kwargs): super().save(*args, **kwargs) if self.set_default_state(): super().save() - # self.__original_ipg_id = self.ipg_id - # Meta class Meta: ordering = ["name"] indexes = [ @@ -545,7 +441,7 @@ def img_url(self): return None def tags(self): - tags = [self.get_switch_type_display(), self.get_agg_display(), self.color] + tags = [self.switchType(), self._agg(), self.color] return [i for i in tags if i] def date_published(self, norm=False): @@ -557,8 +453,6 @@ def date_published(self, norm=False): def n_faves(self, norm=False): nf = Favorite.objects.for_model(Protein).filter(target_object_id=self.id).count() if norm: - from collections import Counter - mx = Counter(Favorite.objects.for_model(Protein).values_list("target_object_id", flat=True)).most_common(1) mx = mx[0][1] if mx else 1 return nf / mx @@ -584,10 +478,10 @@ def ga_views(self, period="month", norm=False): return 0 def switchType(self): - return self.get_switch_type_display() + return self.SwitchingChoices(self.switch_type).label def _agg(self): - return self.get_agg_display() + return self.AggChoices(self.agg).label def url(self): return self.get_absolute_url() @@ -622,11 +516,12 @@ def qy(self): n = [s.qy for s in self.states.all()] return n[0] if len(n) == 1 else n - def rank(self): + def rank(self) -> float: # max rank is 1 - return ( - 0.5 * self.date_published(norm=True) + 0.6 * self.ga_views(norm=True) + 1.0 * self.n_faves(norm=True) - ) / 2.5 + pub_date = self.date_published(norm=True) + ga_views = self.ga_views(norm=True) + n_faves = self.n_faves(norm=True) + return (0.5 * pub_date + 0.6 * ga_views + 1.0 * n_faves) / 2.5 def local_brightness(self): if self.states.exists(): @@ -635,3 +530,38 @@ def local_brightness(self): def first_author(self): if self.primary_reference and self.primary_reference.first_author: return self.primary_reference.first_author.family + + +class State(Fluorophore): # TODO: rename to ProteinState + DEFAULT_NAME = "default" + + name = models.CharField(max_length=64, default=DEFAULT_NAME) # required + protein_id: int + protein = models.ForeignKey["Protein"]( + Protein, + related_name="states", + help_text="The protein to which this state belongs", + on_delete=models.CASCADE, + ) + maturation = models.FloatField( + null=True, + blank=True, + help_text="Maturation time (min)", # maturation half-life in min + validators=[MinValueValidator(0), MaxValueValidator(1600)], + ) + oc_eff = GenericRelation("OcFluorEff", related_query_name="state") + + if TYPE_CHECKING: + transitions = models.ManyToManyField["State", "State"] + else: + transitions = models.ManyToManyField( + "State", + related_name="transition_state", + verbose_name="State Transitions", + blank=True, + through="StateTransition", + ) + + def save(self, *args, **kwargs) -> None: + self.entity_type = self.EntityTypes.PROTEIN + super().save(*args, **kwargs) diff --git a/backend/proteins/models/spectrum.py b/backend/proteins/models/spectrum.py index a98746103..fda7fa2fe 100644 --- a/backend/proteins/models/spectrum.py +++ b/backend/proteins/models/spectrum.py @@ -2,7 +2,7 @@ import json import logging from functools import cached_property -from typing import Any +from typing import Any, Literal import django.forms import numpy as np @@ -535,7 +535,7 @@ def avg(self, waverange): d = self.waverange(waverange) return np.mean([i[1] for i in d]) - def width(self, height=0.5): + def width(self, height=0.5) -> tuple[float, float] | Literal[False]: try: upindex = next(x[0] for x in enumerate(self.y) if x[1] > height) downindex = len(self.y) - next(x[0] for x in enumerate(reversed(self.y)) if x[1] > height) diff --git a/backend/proteins/util/efficiency.py b/backend/proteins/util/efficiency.py index 8c9733c66..9c1833615 100644 --- a/backend/proteins/util/efficiency.py +++ b/backend/proteins/util/efficiency.py @@ -1,6 +1,7 @@ import numpy as np -from ..models.state import Dye, State +from proteins.models.dye import Dye as Dye +from proteins.models.protein import State as State def spectral_product(arrlist): diff --git a/backend/proteins/views/protein.py b/backend/proteins/views/protein.py index c288d5ca1..ce959f001 100644 --- a/backend/proteins/views/protein.py +++ b/backend/proteins/views/protein.py @@ -66,7 +66,7 @@ def check_switch_type(object, request): suggested = suggested_switch_type(object) if suggested and object.switch_type != suggested: - disp = dict(Protein.SWITCHING_CHOICES).get(suggested).lower() + disp = dict(Protein.SwitchingChoices).get(suggested).lower() actual = object.get_switch_type_display().lower() msg = ( "Warning: " @@ -648,7 +648,7 @@ def problems_inconsistencies(request): for prot in p: suggestion = suggested_switch_type(prot) if (prot.switch_type or suggestion) and (prot.switch_type != suggestion): - bad_switch.append((prot, dict(Protein.SWITCHING_CHOICES).get(suggestion))) + bad_switch.append((prot, dict(Protein.SwitchingChoices).get(suggestion))) return render( request, diff --git a/pyproject.toml b/pyproject.toml index 9f5c56209..3048b0950 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ dependencies = [ 'django-autocomplete-light>=3.11.0', 'django-avatar>=8.0.1', 'django-cors-headers>=4.2.0', - 'django-crispy-forms==1.13.0', # NOTE: there's an issue with 1.14 and bootstrap4 forms not having form-control class + 'django-crispy-forms==1.13.0', # NOTE: there's an issue with 1.14 and bootstrap4 forms not having form-control class 'django-environ>=0.10.0', 'django-filter>=23.3', 'django-model-utils>=5.0.0', @@ -175,3 +175,6 @@ reportCallIssue = false reportArgumentType = false reportIncompatibleMethodOverride = false reportOperatorIssue = false + +[tool.uv] +override-dependencies = ["django-stubs ; sys_platform == 'never'"] diff --git a/uv.lock b/uv.lock index 15b68d9a6..af84ec6bd 100644 --- a/uv.lock +++ b/uv.lock @@ -6,6 +6,9 @@ resolution-markers = [ "python_full_version < '3.14'", ] +[manifest] +overrides = [{ name = "django-stubs", marker = "sys_platform == 'never'" }] + [[package]] name = "algoliasearch" version = "3.0.0" @@ -997,7 +1000,7 @@ name = "djangorestframework-stubs" version = "3.16.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django-stubs" }, + { name = "django-stubs", marker = "sys_platform == 'never'" }, { name = "requests" }, { name = "types-pyyaml" }, { name = "types-requests" }, From 2fc75c32f0d67655d6b668acb5aa719311b17329 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Thu, 20 Nov 2025 20:06:12 -0500 Subject: [PATCH 02/57] rename dye name --- backend/proteins/models/dye.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/proteins/models/dye.py b/backend/proteins/models/dye.py index ffbd59ee2..edb243bb8 100644 --- a/backend/proteins/models/dye.py +++ b/backend/proteins/models/dye.py @@ -18,7 +18,7 @@ class Dye(Authorable, TimeStampedModel, Product): # TODO: rename to SmallMolecu """ # --- Identification --- - common_name = models.CharField(max_length=255, db_index=True) + name = models.CharField(max_length=255, db_index=True) slug = models.SlugField(unique=True) # Synonyms allow users to find "FITC" when searching "Fluorescein" From 34bf86265b053bbff9786a67febb8bfeede9a7ff Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Thu, 20 Nov 2025 20:08:26 -0500 Subject: [PATCH 03/57] fix choices --- backend/proteins/models/protein.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/proteins/models/protein.py b/backend/proteins/models/protein.py index acc62dedb..30512c759 100644 --- a/backend/proteins/models/protein.py +++ b/backend/proteins/models/protein.py @@ -5,7 +5,7 @@ import sys from collections import Counter from collections.abc import Sequence -from random import choice +from random import choices from subprocess import PIPE, run from typing import TYPE_CHECKING, cast @@ -49,7 +49,7 @@ def to_python(self, value): def prot_uuid(k: int = 5, opts: Sequence[str] = "ABCDEFGHJKLMNOPQRSTUVWXYZ123456789") -> str: - i = "".join(choice(opts, k=k)) + i = "".join(choices(opts, k=k)) try: Protein.objects.get(uuid=i) except Protein.DoesNotExist: From 365290237773d5eedf7f963ba8633bf54ecab7a5 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Thu, 20 Nov 2025 20:21:43 -0500 Subject: [PATCH 04/57] fix factory --- backend/proteins/factories.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/proteins/factories.py b/backend/proteins/factories.py index eef3d8ecc..c46ee0c02 100644 --- a/backend/proteins/factories.py +++ b/backend/proteins/factories.py @@ -225,7 +225,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") From 9bbc7df1a676d3d27fa2c3350f086cdfeb096fce Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Thu, 20 Nov 2025 20:40:32 -0500 Subject: [PATCH 05/57] broken... fixing tests --- .pre-commit-config.yaml | 2 +- backend/proteins/factories.py | 13 +- backend/proteins/migrations/0001_initial.py | 595 ++++++++++--- .../migrations/0002_auto_20180312_0156.py | 37 - .../proteins/migrations/0003_protein_oser.py | 20 - .../migrations/0005_auto_20180328_1614.py | 67 -- ...8_1614_squashed_0010_auto_20180501_1547.py | 287 ------- .../migrations/0006_auto_20180401_2009.py | 152 ---- .../migrations/0007_auto_20180403_0140.py | 41 - .../migrations/0007_auto_20180513_2346.py | 56 -- .../migrations/0008_auto_20180430_0308.py | 45 - .../migrations/0009_auto_20180430_0335.py | 21 - .../migrations/0009_auto_20180525_1640.py | 18 - .../migrations/0010_auto_20180501_1547.py | 45 - .../migrations/0010_auto_20180525_1840.py | 45 - .../migrations/0011_auto_20180525_2349.py | 23 - .../migrations/0012_auto_20180708_1811.py | 26 - .../migrations/0013_auto_20180718_1717.py | 208 ----- .../migrations/0014_auto_20180718_2340.py | 24 - .../migrations/0015_auto_20180720_1921.py | 33 - .../migrations/0017_auto_20180722_1626.py | 30 - .../migrations/0019_auto_20180723_2200.py | 18 - .../migrations/0020_auto_20180729_0234.py | 36 - .../migrations/0022_osermeasurement.py | 43 - .../migrations/0023_spectrum_reference.py | 20 - .../migrations/0025_auto_20181011_1715.py | 24 - .../0026_bleachmeasurement_cell_type.py | 18 - .../migrations/0028_auto_20181012_2011.py | 54 -- .../migrations/0029_auto_20181014_1241.py | 22 - backend/proteins/migrations/0030_lineage.py | 37 - .../migrations/0033_auto_20181107_2119.py | 26 - .../migrations/0036_lineage_root_node.py | 19 - .../migrations/0037_auto_20181205_2035.py | 18 - .../migrations/0038_auto_20181205_2044.py | 18 - .../migrations/0040_auto_20181210_0345.py | 18 - .../migrations/0041_auto_20181216_1743.py | 29 - .../proteins/migrations/0045_dye_is_dark.py | 18 - .../migrations/0046_auto_20190121_1341.py | 24 - .../migrations/0047_auto_20190319_1525.py | 38 - .../migrations/0049_auto_20190323_1947.py | 19 - ...trum_spectrum_state_status_idx_and_more.py | 24 - ...apgeneplasmid_protein_snapgene_plasmids.py | 35 - .../proteins/migrations_old/0001_initial.py | 799 ++++++++++++++++++ .../migrations_old/0002_auto_20180312_0156.py | 59 ++ .../migrations_old/0003_protein_oser.py | 17 + .../0004_auto_20180315_1323.py | 9 +- .../migrations_old/0005_auto_20180328_1614.py | 124 +++ ...8_1614_squashed_0010_auto_20180501_1547.py | 649 ++++++++++++++ .../migrations_old/0006_auto_20180401_2009.py | 374 ++++++++ .../0006_auto_20180512_0058.py | 7 +- .../migrations_old/0007_auto_20180403_0140.py | 70 ++ .../migrations_old/0007_auto_20180513_2346.py | 103 +++ .../migrations_old/0008_auto_20180430_0308.py | 65 ++ .../0008_auto_20180515_1659.py | 19 +- .../migrations_old/0009_auto_20180430_0335.py | 22 + .../migrations_old/0009_auto_20180525_1640.py | 35 + .../migrations_old/0010_auto_20180501_1547.py | 61 ++ .../migrations_old/0010_auto_20180525_1840.py | 56 ++ .../migrations_old/0011_auto_20180525_2349.py | 45 + .../migrations_old/0012_auto_20180708_1811.py | 37 + .../migrations_old/0013_auto_20180718_1717.py | 348 ++++++++ .../migrations_old/0014_auto_20180718_2340.py | 35 + .../migrations_old/0015_auto_20180720_1921.py | 38 + .../0016_auto_20180722_1314.py | 31 +- .../migrations_old/0017_auto_20180722_1626.py | 39 + .../0018_protein_cofactor.py | 9 +- .../migrations_old/0019_auto_20180723_2200.py | 22 + .../migrations_old/0020_auto_20180729_0234.py | 58 ++ .../0021_auto_20180804_0203.py | 31 +- .../migrations_old/0022_osermeasurement.py | 159 ++++ .../migrations_old/0023_spectrum_reference.py | 25 + .../0024_auto_20181011_1659.py | 15 +- .../migrations_old/0025_auto_20181011_1715.py | 41 + .../0026_bleachmeasurement_cell_type.py | 17 + .../0027_auto_20181011_1754.py | 9 +- .../migrations_old/0028_auto_20181012_2011.py | 100 +++ .../migrations_old/0029_auto_20181014_1241.py | 23 + .../proteins/migrations_old/0030_lineage.py | 69 ++ .../0031_auto_20181103_1531.py | 9 +- .../0032_auto_20181107_2015.py | 18 +- .../migrations_old/0033_auto_20181107_2119.py | 37 + .../0034_lineage_rootmut.py | 7 +- .../0035_auto_20181110_0103.py | 9 +- .../migrations_old/0036_lineage_root_node.py | 24 + .../migrations_old/0037_auto_20181205_2035.py | 22 + .../migrations_old/0038_auto_20181205_2044.py | 22 + .../0039_auto_20181206_0009.py | 19 +- .../migrations_old/0040_auto_20181210_0345.py | 22 + .../migrations_old/0041_auto_20181216_1743.py | 36 + .../0042_auto_20181216_1744.py | 9 +- .../0043_remove_excerpt_protein.py | 7 +- .../0044_auto_20181218_1310.py | 9 +- .../migrations_old/0045_dye_is_dark.py | 19 + .../migrations_old/0046_auto_20190121_1341.py | 44 + .../migrations_old/0047_auto_20190319_1525.py | 91 ++ .../0048_change_protein_uuid.py | 23 +- .../migrations_old/0049_auto_20190323_1947.py | 26 + .../0050_auto_20190714_1318.py | 5 +- ...r_bleachmeasurement_created_by_and_more.py | 70 +- .../0052_alter_protein_chromophore.py | 0 .../0053_alter_protein_chromophore.py | 1 + ...microscope_cfg_calc_efficiency_and_more.py | 0 ...spectrum_status_spectrum_status_changed.py | 3 +- ...trum_spectrum_state_status_idx_and_more.py | 23 + .../0057_add_status_index.py | 9 +- ...apgeneplasmid_protein_snapgene_plasmids.py | 39 + 106 files changed, 4516 insertions(+), 2053 deletions(-) delete mode 100644 backend/proteins/migrations/0002_auto_20180312_0156.py delete mode 100644 backend/proteins/migrations/0003_protein_oser.py delete mode 100644 backend/proteins/migrations/0005_auto_20180328_1614.py delete mode 100644 backend/proteins/migrations/0005_auto_20180328_1614_squashed_0010_auto_20180501_1547.py delete mode 100644 backend/proteins/migrations/0006_auto_20180401_2009.py delete mode 100644 backend/proteins/migrations/0007_auto_20180403_0140.py delete mode 100644 backend/proteins/migrations/0007_auto_20180513_2346.py delete mode 100644 backend/proteins/migrations/0008_auto_20180430_0308.py delete mode 100644 backend/proteins/migrations/0009_auto_20180430_0335.py delete mode 100644 backend/proteins/migrations/0009_auto_20180525_1640.py delete mode 100644 backend/proteins/migrations/0010_auto_20180501_1547.py delete mode 100644 backend/proteins/migrations/0010_auto_20180525_1840.py delete mode 100644 backend/proteins/migrations/0011_auto_20180525_2349.py delete mode 100644 backend/proteins/migrations/0012_auto_20180708_1811.py delete mode 100644 backend/proteins/migrations/0013_auto_20180718_1717.py delete mode 100644 backend/proteins/migrations/0014_auto_20180718_2340.py delete mode 100644 backend/proteins/migrations/0015_auto_20180720_1921.py delete mode 100644 backend/proteins/migrations/0017_auto_20180722_1626.py delete mode 100644 backend/proteins/migrations/0019_auto_20180723_2200.py delete mode 100644 backend/proteins/migrations/0020_auto_20180729_0234.py delete mode 100644 backend/proteins/migrations/0022_osermeasurement.py delete mode 100644 backend/proteins/migrations/0023_spectrum_reference.py delete mode 100644 backend/proteins/migrations/0025_auto_20181011_1715.py delete mode 100644 backend/proteins/migrations/0026_bleachmeasurement_cell_type.py delete mode 100644 backend/proteins/migrations/0028_auto_20181012_2011.py delete mode 100644 backend/proteins/migrations/0029_auto_20181014_1241.py delete mode 100644 backend/proteins/migrations/0030_lineage.py delete mode 100644 backend/proteins/migrations/0033_auto_20181107_2119.py delete mode 100644 backend/proteins/migrations/0036_lineage_root_node.py delete mode 100644 backend/proteins/migrations/0037_auto_20181205_2035.py delete mode 100644 backend/proteins/migrations/0038_auto_20181205_2044.py delete mode 100644 backend/proteins/migrations/0040_auto_20181210_0345.py delete mode 100644 backend/proteins/migrations/0041_auto_20181216_1743.py delete mode 100644 backend/proteins/migrations/0045_dye_is_dark.py delete mode 100644 backend/proteins/migrations/0046_auto_20190121_1341.py delete mode 100644 backend/proteins/migrations/0047_auto_20190319_1525.py delete mode 100644 backend/proteins/migrations/0049_auto_20190323_1947.py delete mode 100644 backend/proteins/migrations/0056_spectrum_spectrum_state_status_idx_and_more.py delete mode 100644 backend/proteins/migrations/0058_snapgeneplasmid_protein_snapgene_plasmids.py create mode 100644 backend/proteins/migrations_old/0001_initial.py create mode 100644 backend/proteins/migrations_old/0002_auto_20180312_0156.py create mode 100644 backend/proteins/migrations_old/0003_protein_oser.py rename backend/proteins/{migrations => migrations_old}/0004_auto_20180315_1323.py (53%) create mode 100644 backend/proteins/migrations_old/0005_auto_20180328_1614.py create mode 100644 backend/proteins/migrations_old/0005_auto_20180328_1614_squashed_0010_auto_20180501_1547.py create mode 100644 backend/proteins/migrations_old/0006_auto_20180401_2009.py rename backend/proteins/{migrations => migrations_old}/0006_auto_20180512_0058.py (57%) create mode 100644 backend/proteins/migrations_old/0007_auto_20180403_0140.py create mode 100644 backend/proteins/migrations_old/0007_auto_20180513_2346.py create mode 100644 backend/proteins/migrations_old/0008_auto_20180430_0308.py rename backend/proteins/{migrations => migrations_old}/0008_auto_20180515_1659.py (66%) create mode 100644 backend/proteins/migrations_old/0009_auto_20180430_0335.py create mode 100644 backend/proteins/migrations_old/0009_auto_20180525_1640.py create mode 100644 backend/proteins/migrations_old/0010_auto_20180501_1547.py create mode 100644 backend/proteins/migrations_old/0010_auto_20180525_1840.py create mode 100644 backend/proteins/migrations_old/0011_auto_20180525_2349.py create mode 100644 backend/proteins/migrations_old/0012_auto_20180708_1811.py create mode 100644 backend/proteins/migrations_old/0013_auto_20180718_1717.py create mode 100644 backend/proteins/migrations_old/0014_auto_20180718_2340.py create mode 100644 backend/proteins/migrations_old/0015_auto_20180720_1921.py rename backend/proteins/{migrations => migrations_old}/0016_auto_20180722_1314.py (51%) create mode 100644 backend/proteins/migrations_old/0017_auto_20180722_1626.py rename backend/proteins/{migrations => migrations_old}/0018_protein_cofactor.py (51%) create mode 100644 backend/proteins/migrations_old/0019_auto_20180723_2200.py create mode 100644 backend/proteins/migrations_old/0020_auto_20180729_0234.py rename backend/proteins/{migrations => migrations_old}/0021_auto_20180804_0203.py (52%) create mode 100644 backend/proteins/migrations_old/0022_osermeasurement.py create mode 100644 backend/proteins/migrations_old/0023_spectrum_reference.py rename backend/proteins/{migrations => migrations_old}/0024_auto_20181011_1659.py (50%) create mode 100644 backend/proteins/migrations_old/0025_auto_20181011_1715.py create mode 100644 backend/proteins/migrations_old/0026_bleachmeasurement_cell_type.py rename backend/proteins/{migrations => migrations_old}/0027_auto_20181011_1754.py (52%) create mode 100644 backend/proteins/migrations_old/0028_auto_20181012_2011.py create mode 100644 backend/proteins/migrations_old/0029_auto_20181014_1241.py create mode 100644 backend/proteins/migrations_old/0030_lineage.py rename backend/proteins/{migrations => migrations_old}/0031_auto_20181103_1531.py (52%) rename backend/proteins/{migrations => migrations_old}/0032_auto_20181107_2015.py (52%) create mode 100644 backend/proteins/migrations_old/0033_auto_20181107_2119.py rename backend/proteins/{migrations => migrations_old}/0034_lineage_rootmut.py (72%) rename backend/proteins/{migrations => migrations_old}/0035_auto_20181110_0103.py (65%) create mode 100644 backend/proteins/migrations_old/0036_lineage_root_node.py create mode 100644 backend/proteins/migrations_old/0037_auto_20181205_2035.py create mode 100644 backend/proteins/migrations_old/0038_auto_20181205_2044.py rename backend/proteins/{migrations => migrations_old}/0039_auto_20181206_0009.py (68%) create mode 100644 backend/proteins/migrations_old/0040_auto_20181210_0345.py create mode 100644 backend/proteins/migrations_old/0041_auto_20181216_1743.py rename backend/proteins/{migrations => migrations_old}/0042_auto_20181216_1744.py (64%) rename backend/proteins/{migrations => migrations_old}/0043_remove_excerpt_protein.py (66%) rename backend/proteins/{migrations => migrations_old}/0044_auto_20181218_1310.py (65%) create mode 100644 backend/proteins/migrations_old/0045_dye_is_dark.py create mode 100644 backend/proteins/migrations_old/0046_auto_20190121_1341.py create mode 100644 backend/proteins/migrations_old/0047_auto_20190319_1525.py rename backend/proteins/{migrations => migrations_old}/0048_change_protein_uuid.py (80%) create mode 100644 backend/proteins/migrations_old/0049_auto_20190323_1947.py rename backend/proteins/{migrations => migrations_old}/0050_auto_20190714_1318.py (93%) rename backend/proteins/{migrations => migrations_old}/0051_alter_bleachmeasurement_created_by_and_more.py (85%) rename backend/proteins/{migrations => migrations_old}/0052_alter_protein_chromophore.py (100%) rename backend/proteins/{migrations => migrations_old}/0053_alter_protein_chromophore.py (99%) rename backend/proteins/{migrations => migrations_old}/0054_microscope_cfg_calc_efficiency_and_more.py (100%) rename backend/proteins/{migrations => migrations_old}/0055_spectrum_status_spectrum_status_changed.py (99%) create mode 100644 backend/proteins/migrations_old/0056_spectrum_spectrum_state_status_idx_and_more.py rename backend/proteins/{migrations => migrations_old}/0057_add_status_index.py (57%) create mode 100644 backend/proteins/migrations_old/0058_snapgeneplasmid_protein_snapgene_plasmids.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5501c63ab..04aded80f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ ci: skip: [] submodules: false -exclude: "^docs/|/migrations/" +exclude: "^docs/|/migrations/|/migrations_old/" default_stages: [pre-commit] repos: diff --git a/backend/proteins/factories.py b/backend/proteins/factories.py index c46ee0c02..9187a7efe 100644 --- a/backend/proteins/factories.py +++ b/backend/proteins/factories.py @@ -210,9 +210,20 @@ class Meta: ) -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)) + structural_status = "DEFINED" + canonical_smiles = "" + inchi = "" + # Generate a fake but unique InChIKey (format: XXXXXXXXXXXXXX-YYYYYYYYYY-Z, exactly 27 chars) + inchikey = factory.Sequence(lambda n: f"TEST{n:06d}ABCD-EFGHIJKLMN-O") + molblock = "" + chemical_class = "" class ProteinFactory(factory.django.DjangoModelFactory[Protein]): diff --git a/backend/proteins/migrations/0001_initial.py b/backend/proteins/migrations/0001_initial.py index e6f3f2d1c..11d20f86a 100644 --- a/backend/proteins/migrations/0001_initial.py +++ b/backend/proteins/migrations/0001_initial.py @@ -1,18 +1,20 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.9 on 2018-02-13 01:08 -from __future__ import unicode_literals +# Generated by Django 5.2.8 on 2025-11-21 01:23 -from django.conf import settings import django.contrib.postgres.fields import django.core.validators -from django.db import migrations, models -from django.contrib.postgres.operations import TrigramExtension import django.db.models.deletion import django.utils.timezone import model_utils.fields -import proteins.fields -import proteins.validators -import uuid +import mptt.fields +import proteins.models._sequence_field +import proteins.models.lineage +import proteins.models.mixins +import proteins.models.protein +import proteins.models.spectrum +import proteins.util.helpers +from django.conf import settings +from django.contrib.postgres.operations import TrigramExtension +from django.db import migrations, models class Migration(migrations.Migration): @@ -20,237 +22,578 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('references', '0008_alter_reference_year'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('references', '0001_initial'), ] operations = [ TrigramExtension(), + migrations.CreateModel( + name='Fluorophore', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('ex_max', models.PositiveSmallIntegerField(blank=True, db_index=True, help_text='Excitation maximum (nm)', null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(900)])), + ('em_max', models.PositiveSmallIntegerField(blank=True, db_index=True, help_text='Emission maximum (nm)', null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1000)])), + ('emhex', models.CharField(blank=True, max_length=7)), + ('exhex', models.CharField(blank=True, max_length=7)), + ('ext_coeff', models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(300000)], verbose_name='Extinction Coefficient (M-1 cm-1)')), + ('qy', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='Quantum Yield')), + ('brightness', models.FloatField(blank=True, editable=False, null=True)), + ('lifetime', models.FloatField(blank=True, help_text='Lifetime (ns)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(20)])), + ('pka', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(2), django.core.validators.MaxValueValidator(12)], verbose_name='pKa')), + ('twop_ex_max', models.PositiveSmallIntegerField(blank=True, db_index=True, null=True, validators=[django.core.validators.MinValueValidator(700), django.core.validators.MaxValueValidator(1600)], verbose_name='Peak 2P excitation')), + ('twop_peakGM', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(200)], verbose_name='Peak 2P cross-section of S0->S1 (GM)')), + ('twop_qy', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='2P Quantum Yield')), + ('is_dark', models.BooleanField(default=False, help_text='This state does not fluorescence', verbose_name='Dark State')), + ('label', models.CharField(db_index=True, max_length=255)), + ('slug', models.SlugField(unique=True)), + ('entity_type', models.CharField(choices=[('protein', 'Protein'), ('dye', 'Dye')], db_index=True, max_length=10)), + ('source_map', models.JSONField(blank=True, default=dict)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='ProteinCollection', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('name', models.CharField(max_length=100)), + ('description', models.CharField(blank=True, max_length=512)), + ('managers', django.contrib.postgres.fields.ArrayField(base_field=models.EmailField(max_length=254), blank=True, default=list, size=None)), + ('private', models.BooleanField(default=False, help_text='Private collections can not be seen by or shared with other users', verbose_name='Private Collection')), + ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)ss', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Protein', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('status', model_utils.fields.StatusField(choices=[('pending', 'pending'), ('approved', 'approved'), ('hidden', 'hidden')], default='pending', max_length=100, no_check_for_status=True, verbose_name='status')), + ('status_changed', model_utils.fields.MonitorField(default=django.utils.timezone.now, monitor='status', verbose_name='status changed')), + ('uuid', models.CharField(db_index=True, default=proteins.models.protein.prot_uuid, editable=False, max_length=5, unique=True, verbose_name='FPbase ID')), + ('name', models.CharField(db_index=True, help_text='Name of the fluorescent protein', max_length=128)), + ('slug', models.SlugField(help_text='URL slug for the protein', max_length=64, unique=True)), + ('base_name', models.CharField(max_length=128)), + ('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=200), blank=True, null=True, size=None)), + ('chromophore', proteins.models.protein._NonNullChar(blank=True, default='', max_length=5)), + ('seq_validated', models.BooleanField(default=False, help_text='Sequence has been validated by a moderator')), + ('seq', proteins.models._sequence_field.SequenceField(blank=True, help_text='Amino acid sequence (IPG ID is preferred)', null=True, unique=True, verbose_name='Sequence')), + ('seq_comment', models.CharField(blank=True, help_text='if necessary, comment on source of sequence', max_length=512)), + ('pdb', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=4), blank=True, null=True, size=None, verbose_name='Protein DataBank IDs')), + ('genbank', models.CharField(blank=True, help_text='NCBI Genbank Accession', max_length=12, null=True, unique=True, verbose_name='Genbank Accession')), + ('uniprot', models.CharField(blank=True, max_length=10, null=True, unique=True, validators=[django.core.validators.RegexValidator('[OPQ][0-9][A-Z0-9]{3}[0-9]|[A-NR-Z][0-9]([A-Z][A-Z0-9]{2}[0-9]){1,2}', 'Not a valid UniProt Accession')], verbose_name='UniProtKB Accession')), + ('ipg_id', models.CharField(blank=True, help_text='Identical Protein Group ID at Pubmed', max_length=12, null=True, unique=True, verbose_name='IPG ID')), + ('mw', models.FloatField(blank=True, help_text='Molecular Weight', null=True)), + ('agg', models.CharField(blank=True, choices=[('m', 'Monomer'), ('d', 'Dimer'), ('td', 'Tandem dimer'), ('wd', 'Weak dimer'), ('t', 'Tetramer')], help_text='Oligomerization tendency', max_length=2, verbose_name='Oligomerization')), + ('oser', models.FloatField(blank=True, help_text='OSER score', null=True)), + ('switch_type', models.CharField(blank=True, choices=[('b', 'Basic'), ('pa', 'Photoactivatable'), ('ps', 'Photoswitchable'), ('pc', 'Photoconvertible'), ('mp', 'Multi-photochromic'), ('t', 'Multistate'), ('o', 'Timer')], default='b', help_text='Photoswitching type (basic if none)', max_length=2, verbose_name='Switching Type')), + ('blurb', models.TextField(blank=True, help_text='Brief descriptive blurb', max_length=512)), + ('cofactor', models.CharField(blank=True, choices=[('br', 'Bilirubin'), ('bv', 'Biliverdin'), ('fl', 'Flavin'), ('pc', 'Phycocyanobilin'), ('rl', 'ribityl-lumazine')], help_text='Required for fluorescence', max_length=2)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='SnapGenePlasmid', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('plasmid_id', models.CharField(db_index=True, max_length=100, unique=True)), + ('name', models.CharField(max_length=200)), + ('description', models.TextField(blank=True)), + ('author', models.CharField(blank=True, max_length=200)), + ('size', models.IntegerField(blank=True, help_text='Size in base pairs', null=True)), + ('topology', models.CharField(blank=True, max_length=50)), + ], + options={ + 'verbose_name': 'SnapGene Plasmid', + 'verbose_name_plural': 'SnapGene Plasmids', + 'ordering': ['name'], + }, + ), migrations.CreateModel( name='BleachMeasurement', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), ('rate', models.FloatField(help_text='Photobleaching half-life (s)', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(3000)], verbose_name='Bleach Rate')), ('power', models.FloatField(blank=True, help_text="If not reported, use '-1'", null=True, validators=[django.core.validators.MinValueValidator(-1)], verbose_name='Illumination Power')), - ('units', models.CharField(blank=True, help_text='e.g. W/cm2', max_length=100, verbose_name='Power Unit')), + ('units', models.CharField(blank=True, help_text='e.g. W/cm2', max_length=100, verbose_name='Power Units')), ('light', models.CharField(blank=True, choices=[('a', 'Arc-lamp'), ('la', 'Laser'), ('le', 'LED'), ('o', 'Other')], max_length=2, verbose_name='Light Source')), + ('bandcenter', models.PositiveSmallIntegerField(blank=True, help_text='Band center of excitation light filter', null=True, validators=[django.core.validators.MinValueValidator(200), django.core.validators.MaxValueValidator(1600)], verbose_name='Band Center (nm)')), + ('bandwidth', models.PositiveSmallIntegerField(blank=True, help_text='Bandwidth of excitation light filter', null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(1000)], verbose_name='Bandwidth (nm)')), ('modality', models.CharField(blank=True, choices=[('wf', 'Widefield'), ('ps', 'Point Scanning Confocal'), ('sd', 'Spinning Disc Confocal'), ('s', 'Spectrophotometer'), ('t', 'TIRF'), ('o', 'Other')], max_length=2, verbose_name='Imaging Modality')), - ('temp', models.FloatField(blank=True, null=True, verbose_name='Temperature')), + ('temp', models.FloatField(blank=True, null=True, verbose_name='Temp (˚C)')), ('fusion', models.CharField(blank=True, help_text='(if applicable)', max_length=60, verbose_name='Fusion Protein')), ('in_cell', models.IntegerField(blank=True, choices=[(-1, 'Unkown'), (0, 'No'), (1, 'Yes')], default=-1, help_text='protein expressed in living cells', verbose_name='In cells?')), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='bleachmeasurement_author', to=settings.AUTH_USER_MODEL)), - ('reference', models.ForeignKey(blank=True, help_text='Reference where the measurement was made', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bleach_measurements', to='references.Reference', verbose_name='Measurement Reference')), + ('cell_type', models.CharField(blank=True, help_text='e.g. HeLa', max_length=60, verbose_name='Cell Type')), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), + ('reference', models.ForeignKey(blank=True, help_text='Reference where the measurement was made', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bleach_measurements', to='references.reference', verbose_name='Measurement Reference')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Camera', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('part', models.CharField(blank=True, max_length=128)), + ('url', models.URLField(blank=True)), + ('name', models.CharField(max_length=100)), + ('slug', models.SlugField(help_text='Unique slug for the %(class)', max_length=128, unique=True)), + ('manufacturer', models.CharField(blank=True, max_length=128)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL)), ], options={ + 'ordering': ['name'], 'abstract': False, }, ), migrations.CreateModel( - name='FRETpair', + name='Dye', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('radius', models.FloatField(blank=True, null=True)), + ('manufacturer', models.CharField(blank=True, max_length=128)), + ('part', models.CharField(blank=True, max_length=128)), + ('url', models.URLField(blank=True)), + ('name', models.CharField(db_index=True, max_length=255)), + ('slug', models.SlugField(unique=True)), + ('synonyms', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None)), + ('structural_status', models.CharField(choices=[('DEFINED', 'Defined Structure'), ('PROPRIETARY', 'Proprietary / Unknown Structure')], default='DEFINED', max_length=20)), + ('canonical_smiles', models.TextField(blank=True)), + ('inchi', models.TextField(blank=True)), + ('inchikey', models.CharField(blank=True, db_index=True, max_length=27)), + ('molblock', models.TextField(blank=True, help_text='V3000 Molfile for precise rendering')), + ('chemical_class', models.CharField(blank=True, db_index=True, max_length=100)), + ('equilibrium_constant_klz', models.FloatField(blank=True, help_text='Equilibrium constant between non-fluorescent lactone and fluorescent zwitterion.', null=True)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), + ('parent_mixture', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='isomers', to='proteins.dye')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='DyeState', + fields=[ + ('fluorophore_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='proteins.fluorophore')), + ('name', models.CharField(help_text="e.g., 'Bound to DNA' or 'In Methanol'", max_length=255)), + ('solvent', models.CharField(default='PBS', max_length=100)), + ('ph', models.FloatField(default=7.4)), + ('environment', models.CharField(choices=[], default='FREE', max_length=20)), + ('is_reference', models.BooleanField(default=False, help_text='If True, this is the default state shown on the dye summary card.')), ], options={ - 'verbose_name': 'FRET Pair', + 'abstract': False, }, + bases=('proteins.fluorophore',), ), migrations.CreateModel( - name='Mutation', + name='State', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('mutations', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=5), size=None, validators=[django.core.validators.RegexValidator('^[ACDEFGHIKLMNPQRSTVWY-][1-9][0-9]{0,2}[ACDEFGHIKLMNPQRSTVWY]$', 'not a valid mutation code: eg S65T')])), + ('fluorophore_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='proteins.fluorophore')), + ('name', models.CharField(default='default', max_length=64)), + ('maturation', models.FloatField(blank=True, help_text='Maturation time (min)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1600)])), ], + options={ + 'abstract': False, + }, + bases=('proteins.fluorophore',), ), migrations.CreateModel( - name='Organism', + name='Filter', fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('id', models.PositiveIntegerField(help_text='NCBI Taxonomy ID', primary_key=True, serialize=False, verbose_name='Taxonomy ID')), - ('scientific_name', models.CharField(blank=True, max_length=128)), - ('division', models.CharField(blank=True, max_length=128)), - ('common_name', models.CharField(blank=True, max_length=128)), - ('species', models.CharField(blank=True, max_length=128)), - ('genus', models.CharField(blank=True, max_length=128)), - ('rank', models.CharField(blank=True, max_length=128)), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='organism_author', to=settings.AUTH_USER_MODEL)), - ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='organism_modifier', to=settings.AUTH_USER_MODEL)), + ('manufacturer', models.CharField(blank=True, max_length=128)), + ('part', models.CharField(blank=True, max_length=128)), + ('url', models.URLField(blank=True)), + ('name', models.CharField(max_length=100)), + ('slug', models.SlugField(help_text='Unique slug for the %(class)', max_length=128, unique=True)), + ('bandcenter', models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(200), django.core.validators.MaxValueValidator(1600)])), + ('bandwidth', models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(900)])), + ('edge', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1600)])), + ('tavg', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)])), + ('aoi', models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(90)])), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL)), ], options={ - 'verbose_name': 'Organism', - 'ordering': ['scientific_name'], + 'ordering': ['bandcenter'], }, ), migrations.CreateModel( - name='Protein', + name='FilterPlacement', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('path', models.CharField(choices=[('ex', 'Excitation Path'), ('em', 'Emission Path'), ('bs', 'Both Paths')], max_length=2, verbose_name='Ex/Bs/Em Path')), + ('reflects', models.BooleanField(default=False, help_text='Filter reflects emission (if BS or EM filter)')), + ('filter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='proteins.filter')), + ], + ), + migrations.CreateModel( + name='FluorophoreCollection', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('proteincollection_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='proteins.proteincollection')), + ], + options={ + 'abstract': False, + }, + bases=('proteins.proteincollection',), + ), + migrations.CreateModel( + name='Light', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('status', model_utils.fields.StatusField(choices=[('pending', 'pending'), ('approved', 'approved'), ('hidden', 'hidden')], default='pending', max_length=100, no_check_for_status=True, verbose_name='status')), - ('status_changed', model_utils.fields.MonitorField(default=django.utils.timezone.now, monitor='status', verbose_name='status changed')), - ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), - ('name', models.CharField(db_index=True, help_text='Name of the fluorescent protein', max_length=128)), - ('slug', models.SlugField(help_text='URL slug for the protein', max_length=64, unique=True)), - ('base_name', models.CharField(max_length=128)), - ('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=200), blank=True, null=True, size=None)), - ('chromophore', models.CharField(blank=True, max_length=5, null=True)), - ('seq', models.CharField(blank=True, help_text='Amino acid sequence (IPG ID is preferred)', max_length=1024, null=True, unique=True, validators=[proteins.validators.protein_sequence_validator], verbose_name='Sequence')), - ('pdb', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=4), blank=True, null=True, size=None, verbose_name='Protein DataBank ID')), - ('genbank', models.CharField(blank=True, help_text='NCBI Genbank Accession', max_length=12, null=True, unique=True, verbose_name='Genbank Accession')), - ('uniprot', models.CharField(blank=True, max_length=10, null=True, unique=True, validators=[django.core.validators.RegexValidator('[OPQ][0-9][A-Z0-9]{3}[0-9]|[A-NR-Z][0-9]([A-Z][A-Z0-9]{2}[0-9]){1,2}', 'Not a valid UniProt Accession')], verbose_name='UniProtKB Accession')), - ('ipg_id', models.CharField(blank=True, help_text='Identical Protein Group ID at Pubmed', max_length=12, null=True, unique=True, verbose_name='IPG ID')), - ('mw', models.FloatField(blank=True, help_text='Molecular Weight', null=True)), - ('agg', models.CharField(blank=True, choices=[('m', 'Monomer'), ('d', 'Dimer'), ('td', 'Tandem dimer'), ('wd', 'Weak dimer'), ('t', 'Tetramer')], help_text='Oligomerization tendency', max_length=2)), - ('switch_type', models.CharField(blank=True, choices=[('b', 'Basic'), ('pa', 'Photoactivatable'), ('ps', 'Photoswitchable'), ('pc', 'Photoconvertible'), ('t', 'Timer'), ('o', 'Multistate')], help_text='Photoswitching type (basic if none)', max_length=2, verbose_name='Type')), - ('blurb', models.CharField(blank=True, help_text='Brief descriptive blurb', max_length=512)), - ('FRET_partner', models.ManyToManyField(blank=True, through='proteins.FRETpair', to='proteins.Protein')), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='protein_author', to=settings.AUTH_USER_MODEL)), + ('part', models.CharField(blank=True, max_length=128)), + ('url', models.URLField(blank=True)), + ('name', models.CharField(max_length=100)), + ('slug', models.SlugField(help_text='Unique slug for the %(class)', max_length=128, unique=True)), + ('manufacturer', models.CharField(blank=True, max_length=128)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL)), ], options={ 'ordering': ['name'], + 'abstract': False, }, ), migrations.CreateModel( - name='ProteinCollection', + name='Microscope', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), ('name', models.CharField(max_length=100)), ('description', models.CharField(blank=True, max_length=512)), - ('private', models.BooleanField(default=False, help_text='Private collections can not be seen by or shared with other users', verbose_name='Private Collection')), - ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='collections', to=settings.AUTH_USER_MODEL, verbose_name='Protein Collection')), - ('proteins', models.ManyToManyField(related_name='collection_memberships', to='proteins.Protein')), + ('managers', django.contrib.postgres.fields.ArrayField(base_field=models.EmailField(max_length=254), blank=True, default=list, size=None)), + ('id', models.CharField(default=proteins.util.helpers.shortuuid, editable=False, max_length=22, primary_key=True, serialize=False)), + ('extra_lasers', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1600)]), blank=True, default=list, size=None)), + ('cfg_calc_efficiency', models.BooleanField(default=True, help_text='Calculate efficiency on update.')), + ('cfg_fill_area', models.BooleanField(default=True, help_text='Fill area under spectra.')), + ('cfg_min_wave', models.PositiveSmallIntegerField(default=350, help_text='Minimum wavelength to display on page load.', validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1199)])), + ('cfg_max_wave', models.PositiveSmallIntegerField(default=800, help_text='Maximum wavelength to display on page load.', validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1199)])), + ('cfg_enable_pan_zoom', models.BooleanField(default=True, help_text='Enable pan and zoom on spectra plot.')), + ('collection', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='on_scope', to='proteins.proteincollection')), + ('extra_cameras', models.ManyToManyField(blank=True, related_name='microscopes', to='proteins.camera')), + ('extra_lights', models.ManyToManyField(blank=True, related_name='microscopes', to='proteins.light')), + ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)ss', to=settings.AUTH_USER_MODEL)), ], + options={ + 'ordering': ['created'], + }, ), migrations.CreateModel( - name='State', + name='OpticalConfig', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('name', models.CharField(default='default', max_length=64)), - ('slug', models.SlugField(help_text='Unique slug for the state', max_length=128, unique=True)), - ('is_dark', models.BooleanField(default=False, help_text='This state does not fluorescence', verbose_name='Dark State')), - ('ex_max', models.PositiveSmallIntegerField(blank=True, db_index=True, null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(900)])), - ('em_max', models.PositiveSmallIntegerField(blank=True, db_index=True, null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1000)])), - ('ex_spectra', proteins.fields.SpectrumField(blank=True, help_text='List of [[wavelength, value],...] pairs', null=True)), - ('em_spectra', proteins.fields.SpectrumField(blank=True, help_text='List of [[wavelength, value],...] pairs', null=True)), - ('ext_coeff', models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(300000)], verbose_name='Extinction Coefficient')), - ('qy', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='Quantum Yield')), - ('brightness', models.FloatField(blank=True, editable=False, null=True)), - ('pka', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(2), django.core.validators.MaxValueValidator(12)], verbose_name='pKa')), - ('maturation', models.FloatField(blank=True, help_text='Maturation time (min)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1600)])), - ('lifetime', models.FloatField(blank=True, help_text='Lifetime (ns)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(20)])), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='state_author', to=settings.AUTH_USER_MODEL)), - ('protein', models.ForeignKey(help_text='The protein to which this state belongs', on_delete=django.db.models.deletion.CASCADE, related_name='states', to='proteins.Protein')), + ('name', models.CharField(max_length=100)), + ('description', models.CharField(blank=True, max_length=512)), + ('managers', django.contrib.postgres.fields.ArrayField(base_field=models.EmailField(max_length=254), blank=True, default=list, size=None)), + ('comments', models.CharField(blank=True, max_length=256)), + ('laser', models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1600)])), + ('camera', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='optical_configs', to='proteins.camera')), + ('filters', models.ManyToManyField(blank=True, related_name='optical_configs', through='proteins.FilterPlacement', to='proteins.filter')), + ('light', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='optical_configs', to='proteins.light')), + ('microscope', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='optical_configs', to='proteins.microscope')), + ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)ss', to=settings.AUTH_USER_MODEL)), ], options={ - 'verbose_name': 'State', + 'ordering': ['name'], }, ), migrations.CreateModel( - name='StateTransition', + name='OcFluorEff', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('trans_wave', models.PositiveSmallIntegerField(blank=True, help_text='Wavelength required', null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1000)], verbose_name='Transition Wavelength')), - ('from_state', models.ForeignKey(help_text='The initial state ', on_delete=django.db.models.deletion.CASCADE, related_name='transitions_from', to='proteins.State', verbose_name='From state')), - ('protein', models.ForeignKey(help_text='The protein that demonstrates this transition', on_delete=django.db.models.deletion.CASCADE, related_name='transitions', to='proteins.Protein', verbose_name='Protein Transitioning')), - ('to_state', models.ForeignKey(help_text='The state after transition', on_delete=django.db.models.deletion.CASCADE, related_name='transitions_to', to='proteins.State', verbose_name='To state')), + ('object_id', models.PositiveIntegerField()), + ('fluor_name', models.CharField(blank=True, max_length=100)), + ('ex_eff', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='Excitation Efficiency')), + ('ex_eff_broad', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='Excitation Efficiency (Broadband)')), + ('em_eff', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='Emission Efficiency')), + ('brightness', models.FloatField(blank=True, null=True)), + ('content_type', models.ForeignKey(limit_choices_to=models.Q(models.Q(('app_label', 'proteins'), ('model', 'state')), models.Q(('app_label', 'proteins'), ('model', 'dye')), _connector='OR'), on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ('oc', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='proteins.opticalconfig')), ], - options={ - 'abstract': False, - }, ), migrations.AddField( - model_name='state', - name='transitions', - field=models.ManyToManyField(blank=True, related_name='transition_state', through='proteins.StateTransition', to='proteins.State', verbose_name='State Transitions'), + model_name='filterplacement', + name='config', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='proteins.opticalconfig'), ), - migrations.AddField( - model_name='state', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='state_modifier', to=settings.AUTH_USER_MODEL), + migrations.CreateModel( + name='Organism', + fields=[ + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('id', models.PositiveIntegerField(help_text='NCBI Taxonomy ID', primary_key=True, serialize=False, verbose_name='Taxonomy ID')), + ('scientific_name', models.CharField(blank=True, max_length=128)), + ('division', models.CharField(blank=True, max_length=128)), + ('common_name', models.CharField(blank=True, max_length=128)), + ('species', models.CharField(blank=True, max_length=128)), + ('genus', models.CharField(blank=True, max_length=128)), + ('rank', models.CharField(blank=True, max_length=128)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Organism', + 'ordering': ['scientific_name'], + }, ), migrations.AddField( - model_name='protein', - name='default_state', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_for', to='proteins.State'), + model_name='proteincollection', + name='proteins', + field=models.ManyToManyField(related_name='collection_memberships', to='proteins.protein'), ), migrations.AddField( model_name='protein', name='parent_organism', - field=models.ForeignKey(blank=True, help_text='Organism from which the protein was engineered', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='proteins', to='proteins.Organism', verbose_name='Parental organism'), + field=models.ForeignKey(blank=True, help_text='Organism from which the protein was engineered', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='proteins', to='proteins.organism', verbose_name='Parental organism'), ), migrations.AddField( model_name='protein', name='primary_reference', - field=models.ForeignKey(blank=True, help_text='Preferably the publication that introduced the protein', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_proteins', to='references.Reference', verbose_name='Primary Reference'), + field=models.ForeignKey(blank=True, help_text='Preferably the publication that introduced the protein', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_proteins', to='references.reference', verbose_name='Primary Reference'), ), migrations.AddField( model_name='protein', name='references', - field=models.ManyToManyField(blank=True, related_name='proteins', to='references.Reference'), + field=models.ManyToManyField(blank=True, related_name='proteins', to='references.reference'), ), migrations.AddField( model_name='protein', name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='protein_modifier', to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL), + ), + migrations.CreateModel( + name='OSERMeasurement', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('percent', models.FloatField(blank=True, help_text="Percentage of 'normal' looking cells", null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)], verbose_name='Percent Normal Cells')), + ('percent_stddev', models.FloatField(blank=True, help_text='Standard deviation of percent normal cells (if applicable)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)], verbose_name='StdDev')), + ('percent_ncells', models.IntegerField(blank=True, help_text='Number of cells analyzed in percent normal for this FP', null=True, verbose_name='Number of cells for percent measurement')), + ('oserne', models.FloatField(blank=True, help_text='Ratio of OSER to nuclear envelope (NE) fluorescence intensities', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)], verbose_name='OSER/NE ratio')), + ('oserne_stddev', models.FloatField(blank=True, help_text='Standard deviation of OSER/NE ratio (if applicable)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)], verbose_name='OSER/NE StdDev')), + ('oserne_ncells', models.IntegerField(blank=True, help_text='Number of cells analyzed in OSER/NE this FP', null=True, verbose_name='Number of cells for OSER/NE measurement')), + ('celltype', models.CharField(blank=True, help_text='e.g. COS-7, HeLa', max_length=64, verbose_name='Cell Type')), + ('temp', models.FloatField(blank=True, null=True, verbose_name='Temperature')), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), + ('reference', models.ForeignKey(blank=True, help_text='Reference where the measurement was made', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='oser_measurements', to='references.reference', verbose_name='Measurement Reference')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL)), + ('protein', models.ForeignKey(help_text='The protein on which this measurement was made', on_delete=django.db.models.deletion.CASCADE, related_name='oser_measurements', to='proteins.protein', verbose_name='Protein')), + ], + ), + migrations.CreateModel( + name='Lineage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('mutation', proteins.models.lineage.MutationSetField(blank=True, max_length=400)), + ('rootmut', models.CharField(blank=True, max_length=400)), + ('lft', models.PositiveIntegerField(editable=False)), + ('rght', models.PositiveIntegerField(editable=False)), + ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)), + ('level', models.PositiveIntegerField(editable=False)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), + ('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='proteins.lineage')), + ('reference', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='lineages', to='references.reference')), + ('root_node', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='descendants', to='proteins.lineage', verbose_name='Root Node')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL)), + ('protein', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='lineage', to='proteins.protein')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Excerpt', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('status', model_utils.fields.StatusField(choices=[('approved', 'approved'), ('flagged', 'flagged'), ('rejected', 'rejected')], default='approved', max_length=100, no_check_for_status=True, verbose_name='status')), + ('status_changed', model_utils.fields.MonitorField(default=django.utils.timezone.now, monitor='status', verbose_name='status changed')), + ('content', models.TextField(help_text='Brief excerpt describing this protein', max_length=1200)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), + ('reference', models.ForeignKey(help_text='Source of this excerpt', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='excerpts', to='references.reference')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL)), + ('proteins', models.ManyToManyField(blank=True, related_name='excerpts', to='proteins.protein')), + ], + options={ + 'ordering': ['reference__year', 'created'], + }, + ), + migrations.CreateModel( + name='ReactiveDerivative', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('reactive_group', models.CharField(choices=[('NHS_ESTER', 'NHS Ester'), ('HALO_TAG', 'HaloTag Ligand'), ('SNAP_TAG', 'SNAP-Tag Ligand'), ('CLIP_TAG', 'CLIP-Tag Ligand'), ('MALEIMIDE', 'Maleimide'), ('AZIDE', 'Azide'), ('ALKYNE', 'Alkyne'), ('BIOTIN', 'Biotin'), ('OTHER', 'Other')], max_length=10)), + ('full_smiles', models.TextField(blank=True, help_text='Structure of the complete reactive molecule')), + ('molecular_weight', models.FloatField()), + ('vendor', models.CharField(max_length=100)), + ('catalog_number', models.CharField(max_length=100)), + ('core_dye', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='derivatives', to='proteins.dye')), + ], + ), + migrations.AddField( + model_name='protein', + name='snapgene_plasmids', + field=models.ManyToManyField(blank=True, help_text='Associated SnapGene plasmids', related_name='proteins', to='proteins.snapgeneplasmid'), + ), + migrations.CreateModel( + name='Spectrum', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('status', model_utils.fields.StatusField(choices=[('approved', 'approved'), ('pending', 'pending'), ('rejected', 'rejected')], default='approved', max_length=100, no_check_for_status=True, verbose_name='status')), + ('status_changed', model_utils.fields.MonitorField(default=django.utils.timezone.now, monitor='status', verbose_name='status changed')), + ('data', proteins.models.spectrum.SpectrumData(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(max_length=10), size=2), size=None)), + ('category', models.CharField(choices=[('d', 'Dye'), ('p', 'Protein'), ('l', 'Light Source'), ('f', 'Filter'), ('c', 'Camera')], db_index=True, max_length=1, verbose_name='Spectrum Type')), + ('subtype', models.CharField(choices=[('ex', 'Excitation'), ('ab', 'Absorption'), ('em', 'Emission'), ('2p', 'Two Photon Abs'), ('bp', 'Bandpass'), ('bx', 'Bandpass-Ex'), ('bm', 'Bandpass-Em'), ('sp', 'Shortpass'), ('lp', 'Longpass'), ('bs', 'Beamsplitter'), ('qe', 'Quantum Efficiency'), ('pd', 'Power Distribution')], db_index=True, max_length=2, verbose_name='Spectrum Subtype')), + ('ph', models.FloatField(blank=True, null=True, verbose_name='pH')), + ('solvent', models.CharField(blank=True, max_length=128)), + ('source', models.CharField(blank=True, help_text='Source of the spectra data', max_length=128)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), + ('owner_camera', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectrum', to='proteins.camera')), + ('owner_dye', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectra', to='proteins.dye')), + ('owner_filter', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectrum', to='proteins.filter')), + ('owner_light', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectrum', to='proteins.light')), + ('reference', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='spectra', to='references.reference')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name_plural': 'spectra', + }, + bases=(models.Model, proteins.models.mixins.AdminURLMixin), + ), + migrations.CreateModel( + name='StateTransition', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('trans_wave', models.PositiveSmallIntegerField(blank=True, help_text='Wavelength required', null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1000)], verbose_name='Transition Wavelength')), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), + ('protein', models.ForeignKey(help_text='The protein that demonstrates this transition', on_delete=django.db.models.deletion.CASCADE, related_name='transitions', to='proteins.protein', verbose_name='Protein Transitioning')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddConstraint( + model_name='dye', + constraint=models.UniqueConstraint(condition=models.Q(('structural_status', 'DEFINED')), fields=('inchikey',), name='unique_defined_molecule'), ), migrations.AddField( - model_name='mutation', - name='parent', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='proteins', to='proteins.Protein', verbose_name='Parent Protein'), + model_name='dyestate', + name='dye', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='states', to='proteins.dye'), ), migrations.AddField( - model_name='fretpair', - name='acceptor', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='FK_FRETacceptor_protein', to='proteins.Protein', verbose_name='acceptor'), + model_name='statetransition', + name='from_state', + field=models.ForeignKey(help_text='The initial state ', on_delete=django.db.models.deletion.CASCADE, related_name='transitions_from', to='proteins.state', verbose_name='From state'), ), migrations.AddField( - model_name='fretpair', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='fretpair_author', to=settings.AUTH_USER_MODEL), + model_name='statetransition', + name='to_state', + field=models.ForeignKey(help_text='The state after transition', on_delete=django.db.models.deletion.CASCADE, related_name='transitions_to', to='proteins.state', verbose_name='To state'), ), migrations.AddField( - model_name='fretpair', - name='donor', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='FK_FRETdonor_protein', to='proteins.Protein', verbose_name='donor'), + model_name='state', + name='protein', + field=models.ForeignKey(help_text='The protein to which this state belongs', on_delete=django.db.models.deletion.CASCADE, related_name='states', to='proteins.protein'), ), migrations.AddField( - model_name='fretpair', - name='pair_references', - field=models.ManyToManyField(blank=True, related_name='FK_FRETpair_reference', to='references.Reference'), + model_name='state', + name='transitions', + field=models.ManyToManyField(blank=True, related_name='transition_state', through='proteins.StateTransition', to='proteins.state', verbose_name='State Transitions'), ), migrations.AddField( - model_name='fretpair', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='fretpair_modifier', to=settings.AUTH_USER_MODEL), + model_name='spectrum', + name='owner_state', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectra', to='proteins.state'), + ), + migrations.AddField( + model_name='protein', + name='default_state', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_for', to='proteins.state'), ), migrations.AddField( model_name='bleachmeasurement', name='state', - field=models.ForeignKey(help_text='The state on which this measurement was made', on_delete=django.db.models.deletion.CASCADE, related_name='bleach_measurements', to='proteins.State', verbose_name='Protein (state)'), + field=models.ForeignKey(help_text='The state on which this measurement was made', on_delete=django.db.models.deletion.CASCADE, related_name='bleach_measurements', to='proteins.state', verbose_name='Protein (state)'), + ), + migrations.AddIndex( + model_name='fluorophore', + index=models.Index(fields=['ex_max'], name='proteins_fl_ex_max_6b4df0_idx'), + ), + migrations.AddIndex( + model_name='fluorophore', + index=models.Index(fields=['em_max'], name='proteins_fl_em_max_a2aef9_idx'), ), migrations.AddField( - model_name='bleachmeasurement', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='bleachmeasurement_modifier', to=settings.AUTH_USER_MODEL), + model_name='microscope', + name='fluors', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='fluor_on_scope', to='proteins.fluorophorecollection'), + ), + migrations.AddField( + model_name='fluorophorecollection', + name='dyes', + field=models.ManyToManyField(blank=True, related_name='collection_memberships', to='proteins.dye'), ), migrations.AlterUniqueTogether( - name='state', - unique_together=set([('protein', 'ex_max', 'em_max', 'ext_coeff', 'qy')]), + name='opticalconfig', + unique_together={('name', 'microscope')}, + ), + migrations.AlterUniqueTogether( + name='ocfluoreff', + unique_together={('oc', 'content_type', 'object_id')}, ), migrations.AlterUniqueTogether( name='proteincollection', - unique_together=set([('owner', 'name')]), + unique_together={('owner', 'name')}, + ), + migrations.AlterUniqueTogether( + name='osermeasurement', + unique_together={('protein', 'reference')}, + ), + migrations.AddIndex( + model_name='spectrum', + index=models.Index(fields=['owner_state_id', 'status'], name='spectrum_state_status_idx'), + ), + migrations.AddIndex( + model_name='spectrum', + index=models.Index(fields=['status'], name='spectrum_status_idx'), + ), + migrations.AddIndex( + model_name='protein', + index=models.Index(fields=['status'], name='protein_status_idx'), ), ] diff --git a/backend/proteins/migrations/0002_auto_20180312_0156.py b/backend/proteins/migrations/0002_auto_20180312_0156.py deleted file mode 100644 index 05fc23d29..000000000 --- a/backend/proteins/migrations/0002_auto_20180312_0156.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.9 on 2018-03-12 01:56 -from __future__ import unicode_literals - -import django.core.validators -from django.db import migrations, models -import proteins.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='state', - name='twop_ex_max', - field=models.PositiveSmallIntegerField(blank=True, db_index=True, null=True, validators=[django.core.validators.MinValueValidator(700), django.core.validators.MaxValueValidator(1600)], verbose_name='Peak 2P excitation'), - ), - migrations.AddField( - model_name='state', - name='twop_ex_spectra', - field=proteins.fields.SpectrumField(blank=True, help_text='List of [[wavelength, value],...] pairs', null=True), - ), - migrations.AddField( - model_name='state', - name='twop_peakGM', - field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(200)], verbose_name='Peak 2P cross-section of S0->S1 (GM)'), - ), - migrations.AddField( - model_name='state', - name='twop_qy', - field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='2P Quantum Yield'), - ), - ] diff --git a/backend/proteins/migrations/0003_protein_oser.py b/backend/proteins/migrations/0003_protein_oser.py deleted file mode 100644 index d0c665dd2..000000000 --- a/backend/proteins/migrations/0003_protein_oser.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.9 on 2018-03-12 22:26 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0002_auto_20180312_0156'), - ] - - operations = [ - migrations.AddField( - model_name='protein', - name='oser', - field=models.FloatField(blank=True, help_text='OSER score', null=True), - ), - ] diff --git a/backend/proteins/migrations/0005_auto_20180328_1614.py b/backend/proteins/migrations/0005_auto_20180328_1614.py deleted file mode 100644 index f252bcc03..000000000 --- a/backend/proteins/migrations/0005_auto_20180328_1614.py +++ /dev/null @@ -1,67 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.11 on 2018-03-28 16:14 -from __future__ import unicode_literals - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0004_auto_20180315_1323'), - ] - - operations = [ - migrations.AlterField( - model_name='bleachmeasurement', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bleachmeasurement_author', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='bleachmeasurement', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bleachmeasurement_modifier', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='fretpair', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='fretpair_author', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='fretpair', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='fretpair_modifier', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='organism', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='organism_author', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='organism', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='organism_modifier', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='protein', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='protein_author', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='protein', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='protein_modifier', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='state', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='state_author', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='state', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='state_modifier', to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/backend/proteins/migrations/0005_auto_20180328_1614_squashed_0010_auto_20180501_1547.py b/backend/proteins/migrations/0005_auto_20180328_1614_squashed_0010_auto_20180501_1547.py deleted file mode 100644 index 14b0c98d9..000000000 --- a/backend/proteins/migrations/0005_auto_20180328_1614_squashed_0010_auto_20180501_1547.py +++ /dev/null @@ -1,287 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.11 on 2018-05-08 13:24 -from __future__ import unicode_literals - -from django.conf import settings -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone -import model_utils.fields -import proteins.models.spectrum - - -def move_spectra(apps, schema_editor): - States = apps.get_model("proteins", "State") - Spectrum = apps.get_model("proteins", "Spectrum") - it = 'p' - for state in States.objects.all(): - if state.ex_spectra: - Spectrum.objects.create( - data=state.ex_spectra.data, - owner_state=state, - subtype='ex', - category=it, - ) - if state.em_spectra: - Spectrum.objects.create( - data=state.em_spectra.data, - owner_state=state, - subtype='em', - category=it, - ) - if state.twop_ex_spectra: - spectrum = Spectrum( - data=state.twop_ex_spectra.data, - owner_state=state, - subtype='2p', - category=it, - ) - spectrum.save() - - -def undo_move_spectra(apps, schema_editor): - Spectrum = apps.get_model("proteins", "Spectrum") - for spectrum in Spectrum.objects.all(): - if spectrum.subtype and spectrum.owner_state: - if spectrum.subtype == 'ex': - spectrum.owner_state.ex_spectra = spectrum.data - elif spectrum.subtype == 'em': - spectrum.owner_state.em_spectra = spectrum.data - elif spectrum.subtype == '2p': - spectrum.owner_state.twop_ex_spectra = spectrum.data - - -class Migration(migrations.Migration): - - replaces = [('proteins', '0005_auto_20180328_1614'), ('proteins', '0006_auto_20180401_2009'), ('proteins', '0007_auto_20180403_0140'), ('proteins', '0008_auto_20180430_0308'), ('proteins', '0009_auto_20180430_0335'), ('proteins', '0010_auto_20180501_1547')] - - dependencies = [ - ('proteins', '0004_auto_20180315_1323'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AlterField( - model_name='bleachmeasurement', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bleachmeasurement_author', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='bleachmeasurement', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bleachmeasurement_modifier', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='fretpair', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='fretpair_author', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='fretpair', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='fretpair_modifier', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='organism', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='organism_author', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='organism', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='organism_modifier', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='protein', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='protein_author', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='protein', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='protein_modifier', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='state', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='state_author', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='state', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='state_modifier', to=settings.AUTH_USER_MODEL), - ), - migrations.CreateModel( - name='Camera', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('slug', models.SlugField(help_text='Unique slug for the %(class)', max_length=128, unique=True)), - ('name', models.CharField(max_length=100)), - ], - ), - migrations.CreateModel( - name='Dye', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('ex_max', models.PositiveSmallIntegerField(blank=True, db_index=True, null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(900)])), - ('em_max', models.PositiveSmallIntegerField(blank=True, db_index=True, null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1000)])), - ('twop_ex_max', models.PositiveSmallIntegerField(blank=True, db_index=True, null=True, validators=[django.core.validators.MinValueValidator(700), django.core.validators.MaxValueValidator(1600)], verbose_name='Peak 2P excitation')), - ('ext_coeff', models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(300000)], verbose_name='Extinction Coefficient')), - ('twop_peakGM', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(200)], verbose_name='Peak 2P cross-section of S0->S1 (GM)')), - ('qy', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='Quantum Yield')), - ('twop_qy', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='2P Quantum Yield')), - ('brightness', models.FloatField(blank=True, editable=False, null=True)), - ('pka', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(2), django.core.validators.MaxValueValidator(12)], verbose_name='pKa')), - ('lifetime', models.FloatField(blank=True, help_text='Lifetime (ns)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(20)])), - ('slug', models.SlugField(help_text='Unique slug for the %(class)', max_length=128, unique=True)), - ('name', models.CharField(max_length=100)), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='Filter', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('slug', models.SlugField(help_text='Unique slug for the %(class)', max_length=128, unique=True)), - ('name', models.CharField(max_length=100)), - ], - ), - migrations.CreateModel( - name='Light', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('slug', models.SlugField(help_text='Unique slug for the %(class)', max_length=128, unique=True)), - ('name', models.CharField(max_length=100)), - ], - ), - migrations.CreateModel( - name='Spectrum', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('data', proteins.models.spectrum.SpectrumData(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(max_length=10), size=2), size=None)), - ('category', models.CharField(blank=True, choices=[('d', 'Dye'), ('p', 'Protein'), ('l', 'Light Source'), ('f', 'Filter'), ('c', 'Camera')], db_index=True, max_length=1, verbose_name='Item Type')), - ('subtype', models.CharField(choices=[('ex', 'excitation'), ('ab', 'absorption'), ('em', 'emission'), ('2p', 'two photon absorption'), ('bx', 'bandpass (excitation)'), ('bm', 'bandpass (emission)'), ('sp', 'shortpass'), ('lp', 'longpass'), ('bs', 'beamsplitter'), ('qe', 'Quantum Efficiency'), ('pd', 'Power Distribution')], db_index=True, max_length=2, verbose_name='Spectra Subtype')), - ('ph', models.FloatField(blank=True, null=True, verbose_name='pH')), - ('solvent', models.CharField(blank=True, max_length=128)), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='spectrum_author', to=settings.AUTH_USER_MODEL)), - ('owner_camera', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectra', to='proteins.Camera')), - ('owner_dye', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectra', to='proteins.Dye')), - ('owner_filter', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectra', to='proteins.Filter')), - ('owner_light', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectra', to='proteins.Light')), - ('owner_state', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectra', to='proteins.State')), - ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='spectrum_modifier', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'abstract': False, - }, - ), - migrations.RunPython( - code=move_spectra, - reverse_code=undo_move_spectra, - ), - migrations.RemoveField( - model_name='state', - name='em_spectra', - ), - migrations.RemoveField( - model_name='state', - name='ex_spectra', - ), - migrations.RemoveField( - model_name='state', - name='twop_ex_spectra', - ), - migrations.AddField( - model_name='filter', - name='aoi', - field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(90)]), - ), - migrations.AddField( - model_name='filter', - name='bandcenter', - field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(200), django.core.validators.MaxValueValidator(1600)]), - ), - migrations.AddField( - model_name='filter', - name='bandwidth', - field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(900)]), - ), - migrations.AddField( - model_name='filter', - name='edge', - field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(200)]), - ), - migrations.AddField( - model_name='filter', - name='tavg', - field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), - ), - migrations.AddField( - model_name='camera', - name='manufacturer', - field=models.CharField(blank=True, max_length=128), - ), - migrations.AddField( - model_name='filter', - name='manufacturer', - field=models.CharField(blank=True, max_length=128), - ), - migrations.AddField( - model_name='light', - name='manufacturer', - field=models.CharField(blank=True, max_length=128), - ), - migrations.AlterField( - model_name='spectrum', - name='category', - field=models.CharField(choices=[('d', 'Dye'), ('p', 'Protein'), ('l', 'Light Source'), ('f', 'Filter'), ('c', 'Camera')], db_index=True, max_length=1, verbose_name='Item Type'), - ), - migrations.AlterField( - model_name='spectrum', - name='subtype', - field=models.CharField(blank=True, choices=[('ex', 'excitation'), ('ab', 'absorption'), ('em', 'emission'), ('2p', 'two photon absorption'), ('bx', 'bandpass (excitation)'), ('bm', 'bandpass (emission)'), ('sp', 'shortpass'), ('lp', 'longpass'), ('bs', 'beamsplitter'), ('qe', 'Quantum Efficiency'), ('pd', 'Power Distribution')], db_index=True, max_length=2, verbose_name='Spectra Subtype'), - ), - migrations.AlterField( - model_name='state', - name='slug', - field=models.SlugField(help_text='Unique slug for the %(class)', max_length=128, unique=True), - ), - migrations.AddField( - model_name='camera', - name='part', - field=models.CharField(blank=True, max_length=128), - ), - migrations.AddField( - model_name='dye', - name='manufacturer', - field=models.CharField(blank=True, max_length=128), - ), - migrations.AddField( - model_name='dye', - name='part', - field=models.CharField(blank=True, max_length=128), - ), - migrations.AddField( - model_name='filter', - name='part', - field=models.CharField(blank=True, max_length=128), - ), - migrations.AddField( - model_name='light', - name='part', - field=models.CharField(blank=True, max_length=128), - ), - migrations.AlterField( - model_name='spectrum', - name='subtype', - field=models.CharField(blank=True, choices=[('ex', 'Excitation'), ('ab', 'Absorption'), ('em', 'Emission'), ('2p', 'Two Photon Absorption'), ('bp', 'Bandpass'), ('bx', 'Bandpass (Excitation)'), ('bm', 'Bandpass (Emission)'), ('sp', 'Shortpass'), ('lp', 'Longpass'), ('bs', 'Beamsplitter'), ('qe', 'Quantum Efficiency'), ('pd', 'Power Distribution')], db_index=True, max_length=2, verbose_name='Spectra Subtype'), - ), - ] diff --git a/backend/proteins/migrations/0006_auto_20180401_2009.py b/backend/proteins/migrations/0006_auto_20180401_2009.py deleted file mode 100644 index 7003c9e81..000000000 --- a/backend/proteins/migrations/0006_auto_20180401_2009.py +++ /dev/null @@ -1,152 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.9 on 2018-04-01 20:09 -from __future__ import unicode_literals - -from django.conf import settings -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone -import model_utils.fields -import proteins.models.spectrum - - -def move_spectra(apps, schema_editor): - States = apps.get_model("proteins", "State") - Spectrum = apps.get_model("proteins", "Spectrum") - it = 'p' - for state in States.objects.all(): - prot_name = state.protein.name - if state.ex_spectrum: - Spectrum.objects.create( - data=state.ex_spectrum.data, - owner_state=state, - subtype='ex', - category=it, - ) - if state.em_spectrum: - Spectrum.objects.create( - data=state.em_spectrum.data, - owner_state=state, - subtype='em', - category=it, - ) - if state.twop_ex_spectrum: - spectrum = Spectrum( - data=state.twop_ex_spectrum.data, - owner_state=state, - subtype='2p', - category=it, - ) - spectrum.save() - - -def undo_move_spectra(apps, schema_editor): - Spectrum = apps.get_model("proteins", "Spectrum") - for spectrum in Spectrum.objects.all(): - if spectrum.subtype and spectrum.owner_state: - if spectrum.subtype == 'ex': - spectrum.owner_state.ex_spectra = spectrum.data - elif spectrum.subtype == 'em': - spectrum.owner_state.em_spectra = spectrum.data - elif spectrum.subtype == '2p': - spectrum.owner_state.twop_ex_spectra = spectrum.data - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('proteins', '0005_auto_20180328_1614'), - ] - - operations = [ - migrations.CreateModel( - name='Camera', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('slug', models.SlugField(help_text='Unique slug for the %(class)', max_length=128, unique=True)), - ('name', models.CharField(max_length=100)) - ], - ), - migrations.CreateModel( - name='Dye', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('ex_max', models.PositiveSmallIntegerField(blank=True, db_index=True, null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(900)])), - ('em_max', models.PositiveSmallIntegerField(blank=True, db_index=True, null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1000)])), - ('twop_ex_max', models.PositiveSmallIntegerField(blank=True, db_index=True, null=True, validators=[django.core.validators.MinValueValidator(700), django.core.validators.MaxValueValidator(1600)], verbose_name='Peak 2P excitation')), - ('ext_coeff', models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(300000)], verbose_name='Extinction Coefficient')), - ('twop_peakGM', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(200)], verbose_name='Peak 2P cross-section of S0->S1 (GM)')), - ('qy', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='Quantum Yield')), - ('twop_qy', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='2P Quantum Yield')), - ('brightness', models.FloatField(blank=True, editable=False, null=True)), - ('pka', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(2), django.core.validators.MaxValueValidator(12)], verbose_name='pKa')), - ('lifetime', models.FloatField(blank=True, help_text='Lifetime (ns)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(20)])), - ('slug', models.SlugField(help_text='Unique slug for the %(class)', max_length=128, unique=True)), - ('name', models.CharField(max_length=100)) - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='Filter', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('slug', models.SlugField(help_text='Unique slug for the %(class)', max_length=128, unique=True)), - ('name', models.CharField(max_length=100)) - ], - ), - migrations.CreateModel( - name='Light', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('slug', models.SlugField(help_text='Unique slug for the %(class)', max_length=128, unique=True)), - ('name', models.CharField(max_length=100)) - ], - ), - migrations.CreateModel( - name='Spectrum', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('data', proteins.models.spectrum.SpectrumData(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(max_length=10), size=2), size=None)), - ('category', models.CharField(blank=True, choices=[('d', 'Dye'), ('p', 'Protein'), ('l', 'Light Source'), ('f', 'Filter'), ('c', 'Camera')], db_index=True, max_length=1, verbose_name='Item Type')), - ('subtype', models.CharField(choices=[('ex', 'excitation'), ('ab', 'absorption'), ('em', 'emission'), ('2p', 'two photon absorption'), ('bx', 'bandpass (excitation)'), ('bm', 'bandpass (emission)'), ('sp', 'shortpass'), ('lp', 'longpass'), ('bs', 'beamsplitter'), ('qe', 'Quantum Efficiency'), ('pd', 'Power Distribution')], db_index=True, max_length=2, verbose_name='Spectra Subtype')), - ('ph', models.FloatField(blank=True, null=True, verbose_name='pH')), - ('solvent', models.CharField(blank=True, max_length=128)), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='spectrum_author', to=settings.AUTH_USER_MODEL)), - ('owner_camera', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectra', to='proteins.Camera')), - ('owner_dye', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectra', to='proteins.Dye')), - ('owner_filter', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectra', to='proteins.Filter')), - ('owner_light', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectra', to='proteins.Light')), - ('owner_state', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectra', to='proteins.State')), - ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='spectrum_modifier', to=settings.AUTH_USER_MODEL)) - ], - options={ - 'abstract': False, - }, - ), - migrations.RunPython( - code=move_spectra, - reverse_code=undo_move_spectra, - ), - migrations.RemoveField( - model_name='state', - name='em_spectra', - ), - migrations.RemoveField( - model_name='state', - name='ex_spectra', - ), - migrations.RemoveField( - model_name='state', - name='twop_ex_spectra', - ), - ] - diff --git a/backend/proteins/migrations/0007_auto_20180403_0140.py b/backend/proteins/migrations/0007_auto_20180403_0140.py deleted file mode 100644 index ea5ae33af..000000000 --- a/backend/proteins/migrations/0007_auto_20180403_0140.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.9 on 2018-04-03 01:40 -from __future__ import unicode_literals - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0006_auto_20180401_2009'), - ] - - operations = [ - migrations.AddField( - model_name='filter', - name='aoi', - field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(90)]), - ), - migrations.AddField( - model_name='filter', - name='bandcenter', - field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(200), django.core.validators.MaxValueValidator(1600)]), - ), - migrations.AddField( - model_name='filter', - name='bandwidth', - field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(900)]), - ), - migrations.AddField( - model_name='filter', - name='edge', - field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(200)]), - ), - migrations.AddField( - model_name='filter', - name='tavg', - field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(900)]), - ), - ] diff --git a/backend/proteins/migrations/0007_auto_20180513_2346.py b/backend/proteins/migrations/0007_auto_20180513_2346.py deleted file mode 100644 index 783ddaf4e..000000000 --- a/backend/proteins/migrations/0007_auto_20180513_2346.py +++ /dev/null @@ -1,56 +0,0 @@ -# Generated by Django 2.0.5 on 2018-05-13 23:46 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('proteins', '0006_auto_20180512_0058'), - ] - - operations = [ - migrations.AddField( - model_name='camera', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='camera_author', to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='camera', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='camera_modifier', to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='dye', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dye_author', to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='dye', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dye_modifier', to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='filter', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='filter_author', to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='filter', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='filter_modifier', to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='light', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='light_author', to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='light', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='light_modifier', to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/backend/proteins/migrations/0008_auto_20180430_0308.py b/backend/proteins/migrations/0008_auto_20180430_0308.py deleted file mode 100644 index 177f243ae..000000000 --- a/backend/proteins/migrations/0008_auto_20180430_0308.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.9 on 2018-04-30 03:08 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0007_auto_20180403_0140'), - ] - - operations = [ - migrations.AddField( - model_name='camera', - name='manufacturer', - field=models.CharField(blank=True, max_length=128), - ), - migrations.AddField( - model_name='filter', - name='manufacturer', - field=models.CharField(blank=True, max_length=128), - ), - migrations.AddField( - model_name='light', - name='manufacturer', - field=models.CharField(blank=True, max_length=128), - ), - migrations.AlterField( - model_name='spectrum', - name='category', - field=models.CharField(choices=[('d', 'Dye'), ('p', 'Protein'), ('l', 'Light Source'), ('f', 'Filter'), ('c', 'Camera')], db_index=True, max_length=1, verbose_name='Item Type'), - ), - migrations.AlterField( - model_name='spectrum', - name='subtype', - field=models.CharField(blank=True, choices=[('ex', 'excitation'), ('ab', 'absorption'), ('em', 'emission'), ('2p', 'two photon absorption'), ('bx', 'bandpass (excitation)'), ('bm', 'bandpass (emission)'), ('sp', 'shortpass'), ('lp', 'longpass'), ('bs', 'beamsplitter'), ('qe', 'Quantum Efficiency'), ('pd', 'Power Distribution')], db_index=True, max_length=2, verbose_name='Spectra Subtype'), - ), - migrations.AlterField( - model_name='state', - name='slug', - field=models.SlugField(help_text='Unique slug for the %(class)', max_length=128, unique=True), - ), - ] diff --git a/backend/proteins/migrations/0009_auto_20180430_0335.py b/backend/proteins/migrations/0009_auto_20180430_0335.py deleted file mode 100644 index 57df2dc01..000000000 --- a/backend/proteins/migrations/0009_auto_20180430_0335.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.9 on 2018-04-30 03:35 -from __future__ import unicode_literals - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0008_auto_20180430_0308'), - ] - - operations = [ - migrations.AlterField( - model_name='filter', - name='tavg', - field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), - ), - ] diff --git a/backend/proteins/migrations/0009_auto_20180525_1640.py b/backend/proteins/migrations/0009_auto_20180525_1640.py deleted file mode 100644 index bbe33f0c9..000000000 --- a/backend/proteins/migrations/0009_auto_20180525_1640.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.0.5 on 2018-05-25 16:40 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0008_auto_20180515_1659'), - ] - - operations = [ - migrations.AlterField( - model_name='spectrum', - name='subtype', - field=models.CharField(choices=[('ex', 'Excitation'), ('ab', 'Absorption'), ('em', 'Emission'), ('2p', 'Two Photon Absorption'), ('bp', 'Bandpass'), ('bx', 'Bandpass (Excitation)'), ('bm', 'Bandpass (Emission)'), ('sp', 'Shortpass'), ('lp', 'Longpass'), ('bs', 'Beamsplitter'), ('qe', 'Quantum Efficiency'), ('pd', 'Power Distribution')], db_index=True, max_length=2, verbose_name='Spectra Subtype'), - ), - ] diff --git a/backend/proteins/migrations/0010_auto_20180501_1547.py b/backend/proteins/migrations/0010_auto_20180501_1547.py deleted file mode 100644 index 088f635c8..000000000 --- a/backend/proteins/migrations/0010_auto_20180501_1547.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.11 on 2018-05-01 15:47 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0009_auto_20180430_0335'), - ] - - operations = [ - migrations.AddField( - model_name='camera', - name='part', - field=models.CharField(blank=True, max_length=128), - ), - migrations.AddField( - model_name='dye', - name='manufacturer', - field=models.CharField(blank=True, max_length=128), - ), - migrations.AddField( - model_name='dye', - name='part', - field=models.CharField(blank=True, max_length=128), - ), - migrations.AddField( - model_name='filter', - name='part', - field=models.CharField(blank=True, max_length=128), - ), - migrations.AddField( - model_name='light', - name='part', - field=models.CharField(blank=True, max_length=128), - ), - migrations.AlterField( - model_name='spectrum', - name='subtype', - field=models.CharField(blank=True, choices=[('ex', 'Excitation'), ('ab', 'Absorption'), ('em', 'Emission'), ('2p', 'Two Photon Absorption'), ('bp', 'Bandpass'), ('bx', 'Bandpass (Excitation)'), ('bm', 'Bandpass (Emission)'), ('sp', 'Shortpass'), ('lp', 'Longpass'), ('bs', 'Beamsplitter'), ('qe', 'Quantum Efficiency'), ('pd', 'Power Distribution')], db_index=True, max_length=2, verbose_name='Spectra Subtype'), - ), - ] diff --git a/backend/proteins/migrations/0010_auto_20180525_1840.py b/backend/proteins/migrations/0010_auto_20180525_1840.py deleted file mode 100644 index 469c4add0..000000000 --- a/backend/proteins/migrations/0010_auto_20180525_1840.py +++ /dev/null @@ -1,45 +0,0 @@ -# Generated by Django 2.0.5 on 2018-05-25 18:40 - -from django.db import migrations -import django.utils.timezone -import model_utils.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0009_auto_20180525_1640'), - ] - - operations = [ - migrations.AddField( - model_name='camera', - name='created', - field=model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created'), - ), - migrations.AddField( - model_name='camera', - name='modified', - field=model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified'), - ), - migrations.AddField( - model_name='filter', - name='created', - field=model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created'), - ), - migrations.AddField( - model_name='filter', - name='modified', - field=model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified'), - ), - migrations.AddField( - model_name='light', - name='created', - field=model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created'), - ), - migrations.AddField( - model_name='light', - name='modified', - field=model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified'), - ), - ] diff --git a/backend/proteins/migrations/0011_auto_20180525_2349.py b/backend/proteins/migrations/0011_auto_20180525_2349.py deleted file mode 100644 index 00e7bcfd8..000000000 --- a/backend/proteins/migrations/0011_auto_20180525_2349.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.0.5 on 2018-05-25 23:49 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0010_auto_20180525_1840'), - ] - - operations = [ - migrations.AlterField( - model_name='spectrum', - name='category', - field=models.CharField(choices=[('d', 'Dye'), ('p', 'Protein'), ('l', 'Light Source'), ('f', 'Filter'), ('c', 'Camera')], db_index=True, max_length=1, verbose_name='Owner Type'), - ), - migrations.AlterField( - model_name='spectrum', - name='subtype', - field=models.CharField(choices=[('ex', 'Excitation'), ('ab', 'Absorption'), ('em', 'Emission'), ('2p', 'Two Photon Abs'), ('bp', 'Bandpass'), ('bx', 'Bandpass-Ex'), ('bm', 'Bandpass-Em'), ('sp', 'Shortpass'), ('lp', 'Longpass'), ('bs', 'Beamsplitter'), ('qe', 'Quantum Efficiency'), ('pd', 'Power Distribution')], db_index=True, max_length=2, verbose_name='Spectrum Subtype'), - ), - ] diff --git a/backend/proteins/migrations/0012_auto_20180708_1811.py b/backend/proteins/migrations/0012_auto_20180708_1811.py deleted file mode 100644 index 48d9960a6..000000000 --- a/backend/proteins/migrations/0012_auto_20180708_1811.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 2.0.6 on 2018-07-08 18:11 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('proteins', '0011_auto_20180525_2349'), - ] - - operations = [ - migrations.AddField( - model_name='statetransition', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='statetransition_author', to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='statetransition', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='statetransition_modifier', to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/backend/proteins/migrations/0013_auto_20180718_1717.py b/backend/proteins/migrations/0013_auto_20180718_1717.py deleted file mode 100644 index 9c3321369..000000000 --- a/backend/proteins/migrations/0013_auto_20180718_1717.py +++ /dev/null @@ -1,208 +0,0 @@ -# Generated by Django 2.0.6 on 2018-07-18 17:17 - -from django.conf import settings -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone -import model_utils.fields -import proteins.util.helpers - - -def resavestuff(apps, schema_editor): - Filter = apps.get_model("proteins", "Filter") - Light = apps.get_model("proteins", "Light") - - for f in Filter.objects.filter( - name__icontains='ff0', - spectrum__subtype='bs', - manufacturer__icontains='semrock'): - f.subtype = 'bp' - for l in Light.objects.filter(manufacturer='lumencor'): - l.manufacturer = 'Lumencor' - l.save() - for f in Filter.objects.filter(manufacturer='lumencor'): - f.manufacturer = 'Lumencor' - if f.part[:5].isdigit(): - f.part = f.part[:3] + '/' + f.part[3:] - f.save() - - -def resavestuff_back(apps, schema_editor): - pass - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('proteins', '0012_auto_20180708_1811'), - ] - - operations = [ - migrations.CreateModel( - name='FilterPlacement', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('path', models.CharField(choices=[('ex', 'Excitation Path'), ('em', 'Emission Path'), ('bs', 'Both Paths')], max_length=2, verbose_name='Ex/Em Path')), - ('reflects', models.BooleanField(default=False, help_text='Filter reflects light at this position in the light path')), - ], - ), - migrations.CreateModel( - name='FluorophoreCollection', - fields=[ - ('proteincollection_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='proteins.ProteinCollection')), - ('dyes', models.ManyToManyField(blank=True, related_name='collection_memberships', to='proteins.Dye')), - ], - options={ - 'abstract': False, - }, - bases=('proteins.proteincollection',), - ), - migrations.CreateModel( - name='Microscope', - fields=[ - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('name', models.CharField(max_length=100)), - ('description', models.CharField(blank=True, max_length=512)), - ('id', models.CharField(default=proteins.util.helpers.shortuuid, editable=False, max_length=22, primary_key=True, serialize=False)), - ('lasers', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1600)]), blank=True, default=list, size=None)), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='OpticalConfig', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('name', models.CharField(max_length=100)), - ('description', models.CharField(blank=True, max_length=512)), - ('laser', models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1600)])), - ], - options={ - 'ordering': ['name'], - }, - ), - migrations.AlterModelOptions( - name='camera', - options={'ordering': ['name']}, - ), - migrations.AlterModelOptions( - name='filter', - options={'ordering': ['bandcenter']}, - ), - migrations.AlterModelOptions( - name='light', - options={'ordering': ['name']}, - ), - migrations.AlterField( - model_name='proteincollection', - name='owner', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='proteincollections', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='spectrum', - name='category', - field=models.CharField(choices=[('d', 'Dye'), ('p', 'Protein'), ('l', 'Light Source'), ('f', 'Filter'), ('c', 'Camera')], db_index=True, max_length=1, verbose_name='Spectrum Type'), - ), - migrations.AlterField( - model_name='spectrum', - name='owner_camera', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectrum', to='proteins.Camera'), - ), - migrations.AlterField( - model_name='spectrum', - name='owner_filter', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectrum', to='proteins.Filter'), - ), - migrations.AlterField( - model_name='spectrum', - name='owner_light', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectrum', to='proteins.Light'), - ), - migrations.AddField( - model_name='opticalconfig', - name='camera', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='optical_configs', to='proteins.Camera'), - ), - migrations.AddField( - model_name='opticalconfig', - name='filters', - field=models.ManyToManyField(blank=True, related_name='optical_configs', through='proteins.FilterPlacement', to='proteins.Filter'), - ), - migrations.AddField( - model_name='opticalconfig', - name='light', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='optical_configs', to='proteins.Light'), - ), - migrations.AddField( - model_name='opticalconfig', - name='microscope', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='optical_configs', to='proteins.Microscope'), - ), - migrations.AddField( - model_name='opticalconfig', - name='owner', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='opticalconfigs', to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='microscope', - name='bs_filters', - field=models.ManyToManyField(blank=True, related_name='as_bs_filter', to='proteins.Filter'), - ), - migrations.AddField( - model_name='microscope', - name='cameras', - field=models.ManyToManyField(blank=True, related_name='microscopes', to='proteins.Camera'), - ), - migrations.AddField( - model_name='microscope', - name='collection', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='on_scope', to='proteins.ProteinCollection'), - ), - migrations.AddField( - model_name='microscope', - name='em_filters', - field=models.ManyToManyField(blank=True, related_name='as_em_filter', to='proteins.Filter'), - ), - migrations.AddField( - model_name='microscope', - name='ex_filters', - field=models.ManyToManyField(blank=True, related_name='as_ex_filter', to='proteins.Filter'), - ), - migrations.AddField( - model_name='microscope', - name='fluors', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='fluor_on_scope', to='proteins.FluorophoreCollection'), - ), - migrations.AddField( - model_name='microscope', - name='lights', - field=models.ManyToManyField(blank=True, related_name='microscopes', to='proteins.Light'), - ), - migrations.AddField( - model_name='microscope', - name='owner', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='microscopes', to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='filterplacement', - name='config', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='proteins.OpticalConfig'), - ), - migrations.AddField( - model_name='filterplacement', - name='filter', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='proteins.Filter'), - ), - migrations.AlterUniqueTogether( - name='opticalconfig', - unique_together={('name', 'microscope')}, - ), - migrations.RunPython(resavestuff, resavestuff_back), - ] diff --git a/backend/proteins/migrations/0014_auto_20180718_2340.py b/backend/proteins/migrations/0014_auto_20180718_2340.py deleted file mode 100644 index b5b7f1f3c..000000000 --- a/backend/proteins/migrations/0014_auto_20180718_2340.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.0.7 on 2018-07-18 23:40 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0013_auto_20180718_1717'), - ] - - operations = [ - migrations.AlterField( - model_name='opticalconfig', - name='camera', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='optical_configs', to='proteins.Camera'), - ), - migrations.AlterField( - model_name='opticalconfig', - name='light', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='optical_configs', to='proteins.Light'), - ), - ] diff --git a/backend/proteins/migrations/0015_auto_20180720_1921.py b/backend/proteins/migrations/0015_auto_20180720_1921.py deleted file mode 100644 index c45c08e3c..000000000 --- a/backend/proteins/migrations/0015_auto_20180720_1921.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 2.0.6 on 2018-07-20 19:21 - -import django.contrib.postgres.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0014_auto_20180718_2340'), - ] - - operations = [ - migrations.AlterModelOptions( - name='microscope', - options={'ordering': ['created']}, - ), - migrations.AddField( - model_name='microscope', - name='managers', - field=django.contrib.postgres.fields.ArrayField(base_field=models.EmailField(max_length=254), blank=True, default=list, size=None), - ), - migrations.AddField( - model_name='opticalconfig', - name='managers', - field=django.contrib.postgres.fields.ArrayField(base_field=models.EmailField(max_length=254), blank=True, default=list, size=None), - ), - migrations.AddField( - model_name='proteincollection', - name='managers', - field=django.contrib.postgres.fields.ArrayField(base_field=models.EmailField(max_length=254), blank=True, default=list, size=None), - ), - ] diff --git a/backend/proteins/migrations/0017_auto_20180722_1626.py b/backend/proteins/migrations/0017_auto_20180722_1626.py deleted file mode 100644 index c401d4bb0..000000000 --- a/backend/proteins/migrations/0017_auto_20180722_1626.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 2.0.7 on 2018-07-22 16:26 - -import django.contrib.postgres.fields -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0016_auto_20180722_1314'), - ] - - operations = [ - migrations.AddField( - model_name='microscope', - name='extra_cameras', - field=models.ManyToManyField(blank=True, related_name='microscopes', to='proteins.Camera'), - ), - migrations.AddField( - model_name='microscope', - name='extra_lasers', - field=django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1600)]), blank=True, default=list, size=None), - ), - migrations.AddField( - model_name='microscope', - name='extra_lights', - field=models.ManyToManyField(blank=True, related_name='microscopes', to='proteins.Light'), - ), - ] diff --git a/backend/proteins/migrations/0019_auto_20180723_2200.py b/backend/proteins/migrations/0019_auto_20180723_2200.py deleted file mode 100644 index 37ac856b2..000000000 --- a/backend/proteins/migrations/0019_auto_20180723_2200.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.0.7 on 2018-07-23 22:00 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0018_protein_cofactor'), - ] - - operations = [ - migrations.AlterField( - model_name='protein', - name='cofactor', - field=models.CharField(blank=True, choices=[('bv', 'Biliverdin'), ('fl', 'Flavin')], help_text='Required for fluorescence', max_length=2), - ), - ] diff --git a/backend/proteins/migrations/0020_auto_20180729_0234.py b/backend/proteins/migrations/0020_auto_20180729_0234.py deleted file mode 100644 index 11ae115d4..000000000 --- a/backend/proteins/migrations/0020_auto_20180729_0234.py +++ /dev/null @@ -1,36 +0,0 @@ -# Generated by Django 2.0.7 on 2018-07-29 02:34 - -import django.core.validators -from django.db import migrations, models -import proteins.models.protein -import proteins.validators - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0019_auto_20180723_2200'), - ] - - operations = [ - migrations.AddField( - model_name='protein', - name='seq_validated', - field=models.BooleanField(default=False, help_text='Sequence has been validated by a moderator'), - ), - migrations.AlterField( - model_name='filter', - name='bandwidth', - field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(900)]), - ), - migrations.AlterField( - model_name='filter', - name='edge', - field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1600)]), - ), - migrations.AlterField( - model_name='protein', - name='seq', - field=proteins.models.protein.SequenceField(blank=True, help_text='Amino acid sequence (IPG ID is preferred)', max_length=1024, null=True, unique=True, validators=[proteins.validators.protein_sequence_validator], verbose_name='Sequence'), - ), - ] diff --git a/backend/proteins/migrations/0022_osermeasurement.py b/backend/proteins/migrations/0022_osermeasurement.py deleted file mode 100644 index d53324b36..000000000 --- a/backend/proteins/migrations/0022_osermeasurement.py +++ /dev/null @@ -1,43 +0,0 @@ -# Generated by Django 2.0.9 on 2018-10-08 18:11 - -from django.conf import settings -import django.core.validators -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone -import model_utils.fields - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('references', '0003_auto_20180804_0203'), - ('proteins', '0021_auto_20180804_0203'), - ] - - operations = [ - migrations.CreateModel( - name='OSERMeasurement', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('percent', models.FloatField(blank=True, help_text='Photobleaching half-life (s)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)], verbose_name='Percent Normal Cells')), - ('percent_stddev', models.FloatField(blank=True, help_text='Standard deviation of percent normal cells (if applicable)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)], verbose_name='StdDev')), - ('percent_ncells', models.IntegerField(blank=True, help_text='Number of cells analyzed in percent normal for this FP', null=True, verbose_name='Number of cells for percent measurement')), - ('oserne', models.FloatField(blank=True, help_text='Ratio of OSER to nuclear envelope (NE) fluorescence intensities', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)], verbose_name='OSER/NE ratio')), - ('oserne_stddev', models.FloatField(blank=True, help_text='Standard deviation of OSER/NE ratio (if applicable)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)], verbose_name='OSER/NE StdDev')), - ('oserne_ncells', models.IntegerField(blank=True, help_text='Number of cells analyzed in OSER/NE this FP', null=True, verbose_name='Number of cells for OSER/NE measurement')), - ('celltype', models.CharField(blank=True, help_text='e.g. COS-7, HeLa', max_length=64, verbose_name='Cell Type')), - ('temp', models.FloatField(blank=True, null=True, verbose_name='Temperature')), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='osermeasurement_author', to=settings.AUTH_USER_MODEL)), - ('protein', models.ForeignKey(help_text='The protein on which this measurement was made', on_delete=django.db.models.deletion.CASCADE, related_name='oser_measurements', to='proteins.Protein', verbose_name='Protein')), - ('reference', models.ForeignKey(blank=True, help_text='Reference where the measurement was made', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='oser_measurements', to='references.Reference', verbose_name='Measurement Reference')), - ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='osermeasurement_modifier', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/backend/proteins/migrations/0023_spectrum_reference.py b/backend/proteins/migrations/0023_spectrum_reference.py deleted file mode 100644 index a8cfc1f9f..000000000 --- a/backend/proteins/migrations/0023_spectrum_reference.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.0.9 on 2018-10-09 19:58 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('references', '0003_auto_20180804_0203'), - ('proteins', '0022_osermeasurement'), - ] - - operations = [ - migrations.AddField( - model_name='spectrum', - name='reference', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='spectra', to='references.Reference'), - ), - ] diff --git a/backend/proteins/migrations/0025_auto_20181011_1715.py b/backend/proteins/migrations/0025_auto_20181011_1715.py deleted file mode 100644 index d657684ef..000000000 --- a/backend/proteins/migrations/0025_auto_20181011_1715.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.0.9 on 2018-10-11 17:15 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0024_auto_20181011_1659'), - ] - - operations = [ - migrations.AddField( - model_name='bleachmeasurement', - name='bandcenter', - field=models.PositiveSmallIntegerField(blank=True, help_text='Band center of excitation light filter', null=True, validators=[django.core.validators.MinValueValidator(200), django.core.validators.MaxValueValidator(1600)], verbose_name='Band Center (nm)'), - ), - migrations.AddField( - model_name='bleachmeasurement', - name='bandwidth', - field=models.PositiveSmallIntegerField(blank=True, help_text='Bandwidth of excitation light filter', null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(1000)], verbose_name='Bandwidth (nm)'), - ), - ] diff --git a/backend/proteins/migrations/0026_bleachmeasurement_cell_type.py b/backend/proteins/migrations/0026_bleachmeasurement_cell_type.py deleted file mode 100644 index 9b127517b..000000000 --- a/backend/proteins/migrations/0026_bleachmeasurement_cell_type.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.0.9 on 2018-10-11 17:22 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0025_auto_20181011_1715'), - ] - - operations = [ - migrations.AddField( - model_name='bleachmeasurement', - name='cell_type', - field=models.CharField(blank=True, help_text='e.g. HeLa', max_length=60, verbose_name='Cell Type'), - ), - ] diff --git a/backend/proteins/migrations/0028_auto_20181012_2011.py b/backend/proteins/migrations/0028_auto_20181012_2011.py deleted file mode 100644 index 4b6f58267..000000000 --- a/backend/proteins/migrations/0028_auto_20181012_2011.py +++ /dev/null @@ -1,54 +0,0 @@ -# Generated by Django 2.0.9 on 2018-10-12 20:11 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone -import model_utils.fields - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('references', '0003_auto_20180804_0203'), - ('proteins', '0027_auto_20181011_1754'), - ] - - operations = [ - migrations.CreateModel( - name='Excerpt', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('status', model_utils.fields.StatusField(choices=[('approved', 'approved'), ('flagged', 'flagged'), ('rejected', 'rejected')], default='approved', max_length=100, no_check_for_status=True, verbose_name='status')), - ('status_changed', model_utils.fields.MonitorField(default=django.utils.timezone.now, monitor='status', verbose_name='status changed')), - ('content', models.TextField(help_text='Brief excerpt describing this protein', max_length=1024)), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='excerpt_author', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'abstract': False, - }, - ), - migrations.AlterField( - model_name='protein', - name='blurb', - field=models.TextField(blank=True, help_text='Brief descriptive blurb', max_length=512), - ), - migrations.AddField( - model_name='excerpt', - name='protein', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='excerpts', to='proteins.Protein'), - ), - migrations.AddField( - model_name='excerpt', - name='reference', - field=models.ForeignKey(help_text='Source of this excerpt', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='excerpt', to='references.Reference'), - ), - migrations.AddField( - model_name='excerpt', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='excerpt_modifier', to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/backend/proteins/migrations/0029_auto_20181014_1241.py b/backend/proteins/migrations/0029_auto_20181014_1241.py deleted file mode 100644 index a4846c7cf..000000000 --- a/backend/proteins/migrations/0029_auto_20181014_1241.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 2.0.9 on 2018-10-14 12:41 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0028_auto_20181012_2011'), - ] - - operations = [ - migrations.AlterModelOptions( - name='excerpt', - options={'ordering': ['reference__year', 'created']}, - ), - migrations.AddField( - model_name='protein', - name='seq_comment', - field=models.CharField(blank=True, help_text='if necessary, comment on source of sequence', max_length=512), - ), - ] diff --git a/backend/proteins/migrations/0030_lineage.py b/backend/proteins/migrations/0030_lineage.py deleted file mode 100644 index 98496cc5e..000000000 --- a/backend/proteins/migrations/0030_lineage.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 2.1.2 on 2018-10-28 22:20 - -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone -import model_utils.fields -import mptt.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('references', '0004_auto_20181026_1547'), - ('proteins', '0029_auto_20181014_1241'), - ] - - operations = [ - migrations.CreateModel( - name='Lineage', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('mutation', models.CharField(blank=True, max_length=400)), - ('lft', models.PositiveIntegerField(db_index=True, editable=False)), - ('rght', models.PositiveIntegerField(db_index=True, editable=False)), - ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)), - ('level', models.PositiveIntegerField(db_index=True, editable=False)), - ('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='proteins.Lineage')), - ('protein', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='lineage', to='proteins.Protein')), - ('reference', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='lineages', to='references.Reference')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/backend/proteins/migrations/0033_auto_20181107_2119.py b/backend/proteins/migrations/0033_auto_20181107_2119.py deleted file mode 100644 index 1a5486f0a..000000000 --- a/backend/proteins/migrations/0033_auto_20181107_2119.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 2.1.2 on 2018-11-07 21:19 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('proteins', '0032_auto_20181107_2015'), - ] - - operations = [ - migrations.AddField( - model_name='lineage', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='lineage_author', to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='lineage', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='lineage_modifier', to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/backend/proteins/migrations/0036_lineage_root_node.py b/backend/proteins/migrations/0036_lineage_root_node.py deleted file mode 100644 index 6fa7951d1..000000000 --- a/backend/proteins/migrations/0036_lineage_root_node.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.1.2 on 2018-12-02 15:58 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0035_auto_20181110_0103'), - ] - - operations = [ - migrations.AddField( - model_name='lineage', - name='root_node', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='descendants', to='proteins.Lineage', verbose_name='Root Node'), - ), - ] diff --git a/backend/proteins/migrations/0037_auto_20181205_2035.py b/backend/proteins/migrations/0037_auto_20181205_2035.py deleted file mode 100644 index 5fb290afb..000000000 --- a/backend/proteins/migrations/0037_auto_20181205_2035.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.1.2 on 2018-12-05 20:35 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0036_lineage_root_node'), - ] - - operations = [ - migrations.AlterField( - model_name='protein', - name='cofactor', - field=models.CharField(blank=True, choices=[('bv', 'Biliverdin'), ('fl', 'Flavin'), ('pcb', 'Phycocyanobilin')], help_text='Required for fluorescence', max_length=2), - ), - ] diff --git a/backend/proteins/migrations/0038_auto_20181205_2044.py b/backend/proteins/migrations/0038_auto_20181205_2044.py deleted file mode 100644 index bf05e5e42..000000000 --- a/backend/proteins/migrations/0038_auto_20181205_2044.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.1.2 on 2018-12-05 20:44 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0037_auto_20181205_2035'), - ] - - operations = [ - migrations.AlterField( - model_name='protein', - name='cofactor', - field=models.CharField(blank=True, choices=[('bv', 'Biliverdin'), ('fl', 'Flavin'), ('pc', 'Phycocyanobilin')], help_text='Required for fluorescence', max_length=2), - ), - ] diff --git a/backend/proteins/migrations/0040_auto_20181210_0345.py b/backend/proteins/migrations/0040_auto_20181210_0345.py deleted file mode 100644 index e6b62ea01..000000000 --- a/backend/proteins/migrations/0040_auto_20181210_0345.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.1.2 on 2018-12-10 03:45 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0039_auto_20181206_0009'), - ] - - operations = [ - migrations.AlterField( - model_name='protein', - name='cofactor', - field=models.CharField(blank=True, choices=[('br', 'Bilirubin'), ('bv', 'Biliverdin'), ('fl', 'Flavin'), ('pc', 'Phycocyanobilin')], help_text='Required for fluorescence', max_length=2), - ), - ] diff --git a/backend/proteins/migrations/0041_auto_20181216_1743.py b/backend/proteins/migrations/0041_auto_20181216_1743.py deleted file mode 100644 index d2b42fc39..000000000 --- a/backend/proteins/migrations/0041_auto_20181216_1743.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 2.1.2 on 2018-12-16 17:43 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0040_auto_20181210_0345'), - ] - - operations = [ - migrations.AddField( - model_name='excerpt', - name='proteins', - field=models.ManyToManyField(related_name='excerpts', to='proteins.Protein'), - ), - migrations.AlterField( - model_name='excerpt', - name='protein', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='excerpt', to='proteins.Protein'), - ), - migrations.AlterField( - model_name='excerpt', - name='reference', - field=models.ForeignKey(help_text='Source of this excerpt', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='excerpts', to='references.Reference'), - ), - ] diff --git a/backend/proteins/migrations/0045_dye_is_dark.py b/backend/proteins/migrations/0045_dye_is_dark.py deleted file mode 100644 index 72ce667ae..000000000 --- a/backend/proteins/migrations/0045_dye_is_dark.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.1.2 on 2018-12-29 18:47 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0044_auto_20181218_1310'), - ] - - operations = [ - migrations.AddField( - model_name='dye', - name='is_dark', - field=models.BooleanField(default=False, help_text='This state does not fluorescence', verbose_name='Dark State'), - ), - ] diff --git a/backend/proteins/migrations/0046_auto_20190121_1341.py b/backend/proteins/migrations/0046_auto_20190121_1341.py deleted file mode 100644 index 7a8978c88..000000000 --- a/backend/proteins/migrations/0046_auto_20190121_1341.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.1.2 on 2019-01-21 13:41 - -import django.contrib.postgres.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0045_dye_is_dark'), - ] - - operations = [ - migrations.AlterField( - model_name='protein', - name='pdb', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=4), blank=True, null=True, size=None, verbose_name='Protein DataBank IDs'), - ), - migrations.AlterField( - model_name='protein', - name='switch_type', - field=models.CharField(blank=True, choices=[('b', 'Basic'), ('pa', 'Photoactivatable'), ('ps', 'Photoswitchable'), ('pc', 'Photoconvertible'), ('mp', 'Multi-photochromic'), ('o', 'Multistate'), ('t', 'Timer')], default='b', help_text='Photoswitching type (basic if none)', max_length=2, verbose_name='Switching Type'), - ), - ] diff --git a/backend/proteins/migrations/0047_auto_20190319_1525.py b/backend/proteins/migrations/0047_auto_20190319_1525.py deleted file mode 100644 index a3b006738..000000000 --- a/backend/proteins/migrations/0047_auto_20190319_1525.py +++ /dev/null @@ -1,38 +0,0 @@ -# Generated by Django 2.1.7 on 2019-03-19 15:25 - -import django.core.validators -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone -import model_utils.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - ('proteins', '0046_auto_20190121_1341'), - ] - - operations = [ - migrations.CreateModel( - name='OcFluorEff', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('object_id', models.PositiveIntegerField()), - ('fluor_name', models.CharField(blank=True, max_length=100)), - ('ex_eff', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='Excitation Efficiency')), - ('ex_eff_broad', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='Excitation Efficiency (Broadband)')), - ('em_eff', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='Emission Efficiency')), - ('brightness', models.FloatField(blank=True, null=True)), - ('content_type', models.ForeignKey(limit_choices_to=models.Q(models.Q(('app_label', 'proteins'), ('model', 'state')), models.Q(('app_label', 'proteins'), ('model', 'dye')), _connector='OR'), on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), - ('oc', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='proteins.OpticalConfig')), - ], - ), - migrations.AlterUniqueTogether( - name='ocfluoreff', - unique_together={('oc', 'content_type', 'object_id')}, - ), - ] diff --git a/backend/proteins/migrations/0049_auto_20190323_1947.py b/backend/proteins/migrations/0049_auto_20190323_1947.py deleted file mode 100644 index 81478f3c3..000000000 --- a/backend/proteins/migrations/0049_auto_20190323_1947.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.1.7 on 2019-03-23 19:47 - -from django.db import migrations, models -import proteins.models.protein - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0048_change_protein_uuid'), - ] - - operations = [ - migrations.AlterField( - model_name='protein', - name='uuid', - field=models.CharField(db_index=True, default=proteins.models.protein.prot_uuid, editable=False, max_length=5, unique=True, verbose_name='FPbase ID'), - ), - ] diff --git a/backend/proteins/migrations/0056_spectrum_spectrum_state_status_idx_and_more.py b/backend/proteins/migrations/0056_spectrum_spectrum_state_status_idx_and_more.py deleted file mode 100644 index bbbab8532..000000000 --- a/backend/proteins/migrations/0056_spectrum_spectrum_state_status_idx_and_more.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-22 14:06 - -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0055_spectrum_status_spectrum_status_changed'), - ('references', '0008_alter_reference_year'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AddIndex( - model_name='spectrum', - index=models.Index(fields=['owner_state_id', 'status'], name='spectrum_state_status_idx'), - ), - migrations.AddIndex( - model_name='spectrum', - index=models.Index(fields=['status'], name='spectrum_status_idx'), - ), - ] diff --git a/backend/proteins/migrations/0058_snapgeneplasmid_protein_snapgene_plasmids.py b/backend/proteins/migrations/0058_snapgeneplasmid_protein_snapgene_plasmids.py deleted file mode 100644 index 8deaec527..000000000 --- a/backend/proteins/migrations/0058_snapgeneplasmid_protein_snapgene_plasmids.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-15 14:28 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('proteins', '0057_add_status_index'), - ] - - operations = [ - migrations.CreateModel( - name='SnapGenePlasmid', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('plasmid_id', models.CharField(db_index=True, max_length=100, unique=True)), - ('name', models.CharField(max_length=200)), - ('description', models.TextField(blank=True)), - ('author', models.CharField(blank=True, max_length=200)), - ('size', models.IntegerField(blank=True, help_text='Size in base pairs', null=True)), - ('topology', models.CharField(blank=True, max_length=50)), - ], - options={ - 'verbose_name': 'SnapGene Plasmid', - 'verbose_name_plural': 'SnapGene Plasmids', - 'ordering': ['name'], - }, - ), - migrations.AddField( - model_name='protein', - name='snapgene_plasmids', - field=models.ManyToManyField(blank=True, help_text='Associated SnapGene plasmids', related_name='proteins', to='proteins.snapgeneplasmid'), - ), - ] diff --git a/backend/proteins/migrations_old/0001_initial.py b/backend/proteins/migrations_old/0001_initial.py new file mode 100644 index 000000000..48d52dce1 --- /dev/null +++ b/backend/proteins/migrations_old/0001_initial.py @@ -0,0 +1,799 @@ +# Generated by Django 1.11.9 on 2018-02-13 01:08 + +import uuid + +import django.contrib.postgres.fields +import django.core.validators +import django.db.models.deletion +import django.utils.timezone +import model_utils.fields +from django.conf import settings +from django.contrib.postgres.operations import TrigramExtension +from django.db import migrations, models + +import proteins.fields +import proteins.validators + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("references", "0001_initial"), + ] + + operations = [ + TrigramExtension(), + migrations.CreateModel( + name="BleachMeasurement", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), + ( + "rate", + models.FloatField( + help_text="Photobleaching half-life (s)", + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(3000), + ], + verbose_name="Bleach Rate", + ), + ), + ( + "power", + models.FloatField( + blank=True, + help_text="If not reported, use '-1'", + null=True, + validators=[django.core.validators.MinValueValidator(-1)], + verbose_name="Illumination Power", + ), + ), + ( + "units", + models.CharField(blank=True, help_text="e.g. W/cm2", max_length=100, verbose_name="Power Unit"), + ), + ( + "light", + models.CharField( + blank=True, + choices=[("a", "Arc-lamp"), ("la", "Laser"), ("le", "LED"), ("o", "Other")], + max_length=2, + verbose_name="Light Source", + ), + ), + ( + "modality", + models.CharField( + blank=True, + choices=[ + ("wf", "Widefield"), + ("ps", "Point Scanning Confocal"), + ("sd", "Spinning Disc Confocal"), + ("s", "Spectrophotometer"), + ("t", "TIRF"), + ("o", "Other"), + ], + max_length=2, + verbose_name="Imaging Modality", + ), + ), + ("temp", models.FloatField(blank=True, null=True, verbose_name="Temperature")), + ( + "fusion", + models.CharField( + blank=True, help_text="(if applicable)", max_length=60, verbose_name="Fusion Protein" + ), + ), + ( + "in_cell", + models.IntegerField( + blank=True, + choices=[(-1, "Unkown"), (0, "No"), (1, "Yes")], + default=-1, + help_text="protein expressed in living cells", + verbose_name="In cells?", + ), + ), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="bleachmeasurement_author", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "reference", + models.ForeignKey( + blank=True, + help_text="Reference where the measurement was made", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="bleach_measurements", + to="references.Reference", + verbose_name="Measurement Reference", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="FRETpair", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), + ("radius", models.FloatField(blank=True, null=True)), + ], + options={ + "verbose_name": "FRET Pair", + }, + ), + migrations.CreateModel( + name="Mutation", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "mutations", + django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=5), + size=None, + validators=[ + django.core.validators.RegexValidator( + "^[ACDEFGHIKLMNPQRSTVWY-][1-9][0-9]{0,2}[ACDEFGHIKLMNPQRSTVWY]$", + "not a valid mutation code: eg S65T", + ) + ], + ), + ), + ], + ), + migrations.CreateModel( + name="Organism", + fields=[ + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), + ( + "id", + models.PositiveIntegerField( + help_text="NCBI Taxonomy ID", primary_key=True, serialize=False, verbose_name="Taxonomy ID" + ), + ), + ("scientific_name", models.CharField(blank=True, max_length=128)), + ("division", models.CharField(blank=True, max_length=128)), + ("common_name", models.CharField(blank=True, max_length=128)), + ("species", models.CharField(blank=True, max_length=128)), + ("genus", models.CharField(blank=True, max_length=128)), + ("rank", models.CharField(blank=True, max_length=128)), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="organism_author", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "updated_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="organism_modifier", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "Organism", + "ordering": ["scientific_name"], + }, + ), + migrations.CreateModel( + name="Protein", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), + ( + "status", + model_utils.fields.StatusField( + choices=[("pending", "pending"), ("approved", "approved"), ("hidden", "hidden")], + default="pending", + max_length=100, + no_check_for_status=True, + verbose_name="status", + ), + ), + ( + "status_changed", + model_utils.fields.MonitorField( + default=django.utils.timezone.now, monitor="status", verbose_name="status changed" + ), + ), + ("uuid", models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ("name", models.CharField(db_index=True, help_text="Name of the fluorescent protein", max_length=128)), + ("slug", models.SlugField(help_text="URL slug for the protein", max_length=64, unique=True)), + ("base_name", models.CharField(max_length=128)), + ( + "aliases", + django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=200), blank=True, null=True, size=None + ), + ), + ("chromophore", models.CharField(blank=True, max_length=5, null=True)), + ( + "seq", + models.CharField( + blank=True, + help_text="Amino acid sequence (IPG ID is preferred)", + max_length=1024, + null=True, + unique=True, + validators=[proteins.validators.protein_sequence_validator], + verbose_name="Sequence", + ), + ), + ( + "pdb", + django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=4), + blank=True, + null=True, + size=None, + verbose_name="Protein DataBank ID", + ), + ), + ( + "genbank", + models.CharField( + blank=True, + help_text="NCBI Genbank Accession", + max_length=12, + null=True, + unique=True, + verbose_name="Genbank Accession", + ), + ), + ( + "uniprot", + models.CharField( + blank=True, + max_length=10, + null=True, + unique=True, + validators=[ + django.core.validators.RegexValidator( + "[OPQ][0-9][A-Z0-9]{3}[0-9]|[A-NR-Z][0-9]([A-Z][A-Z0-9]{2}[0-9]){1,2}", + "Not a valid UniProt Accession", + ) + ], + verbose_name="UniProtKB Accession", + ), + ), + ( + "ipg_id", + models.CharField( + blank=True, + help_text="Identical Protein Group ID at Pubmed", + max_length=12, + null=True, + unique=True, + verbose_name="IPG ID", + ), + ), + ("mw", models.FloatField(blank=True, help_text="Molecular Weight", null=True)), + ( + "agg", + models.CharField( + blank=True, + choices=[ + ("m", "Monomer"), + ("d", "Dimer"), + ("td", "Tandem dimer"), + ("wd", "Weak dimer"), + ("t", "Tetramer"), + ], + help_text="Oligomerization tendency", + max_length=2, + ), + ), + ( + "switch_type", + models.CharField( + blank=True, + choices=[ + ("b", "Basic"), + ("pa", "Photoactivatable"), + ("ps", "Photoswitchable"), + ("pc", "Photoconvertible"), + ("t", "Timer"), + ("o", "Multistate"), + ], + help_text="Photoswitching type (basic if none)", + max_length=2, + verbose_name="Type", + ), + ), + ("blurb", models.CharField(blank=True, help_text="Brief descriptive blurb", max_length=512)), + ( + "FRET_partner", + models.ManyToManyField(blank=True, through="proteins.FRETpair", to="proteins.Protein"), + ), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="protein_author", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "ordering": ["name"], + }, + ), + migrations.CreateModel( + name="ProteinCollection", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), + ("name", models.CharField(max_length=100)), + ("description", models.CharField(blank=True, max_length=512)), + ( + "private", + models.BooleanField( + default=False, + help_text="Private collections can not be seen by or shared with other users", + verbose_name="Private Collection", + ), + ), + ( + "owner", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="collections", + to=settings.AUTH_USER_MODEL, + verbose_name="Protein Collection", + ), + ), + ("proteins", models.ManyToManyField(related_name="collection_memberships", to="proteins.Protein")), + ], + ), + migrations.CreateModel( + name="State", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), + ("name", models.CharField(default="default", max_length=64)), + ("slug", models.SlugField(help_text="Unique slug for the state", max_length=128, unique=True)), + ( + "is_dark", + models.BooleanField( + default=False, help_text="This state does not fluorescence", verbose_name="Dark State" + ), + ), + ( + "ex_max", + models.PositiveSmallIntegerField( + blank=True, + db_index=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(300), + django.core.validators.MaxValueValidator(900), + ], + ), + ), + ( + "em_max", + models.PositiveSmallIntegerField( + blank=True, + db_index=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(300), + django.core.validators.MaxValueValidator(1000), + ], + ), + ), + ( + "ex_spectra", + proteins.fields.SpectrumField( + blank=True, help_text="List of [[wavelength, value],...] pairs", null=True + ), + ), + ( + "em_spectra", + proteins.fields.SpectrumField( + blank=True, help_text="List of [[wavelength, value],...] pairs", null=True + ), + ), + ( + "ext_coeff", + models.IntegerField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(300000), + ], + verbose_name="Extinction Coefficient", + ), + ), + ( + "qy", + models.FloatField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(1), + ], + verbose_name="Quantum Yield", + ), + ), + ("brightness", models.FloatField(blank=True, editable=False, null=True)), + ( + "pka", + models.FloatField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(2), + django.core.validators.MaxValueValidator(12), + ], + verbose_name="pKa", + ), + ), + ( + "maturation", + models.FloatField( + blank=True, + help_text="Maturation time (min)", + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(1600), + ], + ), + ), + ( + "lifetime", + models.FloatField( + blank=True, + help_text="Lifetime (ns)", + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(20), + ], + ), + ), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="state_author", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "protein", + models.ForeignKey( + help_text="The protein to which this state belongs", + on_delete=django.db.models.deletion.CASCADE, + related_name="states", + to="proteins.Protein", + ), + ), + ], + options={ + "verbose_name": "State", + }, + ), + migrations.CreateModel( + name="StateTransition", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), + ( + "trans_wave", + models.PositiveSmallIntegerField( + blank=True, + help_text="Wavelength required", + null=True, + validators=[ + django.core.validators.MinValueValidator(300), + django.core.validators.MaxValueValidator(1000), + ], + verbose_name="Transition Wavelength", + ), + ), + ( + "from_state", + models.ForeignKey( + help_text="The initial state ", + on_delete=django.db.models.deletion.CASCADE, + related_name="transitions_from", + to="proteins.State", + verbose_name="From state", + ), + ), + ( + "protein", + models.ForeignKey( + help_text="The protein that demonstrates this transition", + on_delete=django.db.models.deletion.CASCADE, + related_name="transitions", + to="proteins.Protein", + verbose_name="Protein Transitioning", + ), + ), + ( + "to_state", + models.ForeignKey( + help_text="The state after transition", + on_delete=django.db.models.deletion.CASCADE, + related_name="transitions_to", + to="proteins.State", + verbose_name="To state", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.AddField( + model_name="state", + name="transitions", + field=models.ManyToManyField( + blank=True, + related_name="transition_state", + through="proteins.StateTransition", + to="proteins.State", + verbose_name="State Transitions", + ), + ), + migrations.AddField( + model_name="state", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="state_modifier", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="protein", + name="default_state", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="default_for", + to="proteins.State", + ), + ), + migrations.AddField( + model_name="protein", + name="parent_organism", + field=models.ForeignKey( + blank=True, + help_text="Organism from which the protein was engineered", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="proteins", + to="proteins.Organism", + verbose_name="Parental organism", + ), + ), + migrations.AddField( + model_name="protein", + name="primary_reference", + field=models.ForeignKey( + blank=True, + help_text="Preferably the publication that introduced the protein", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="primary_proteins", + to="references.Reference", + verbose_name="Primary Reference", + ), + ), + migrations.AddField( + model_name="protein", + name="references", + field=models.ManyToManyField(blank=True, related_name="proteins", to="references.Reference"), + ), + migrations.AddField( + model_name="protein", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="protein_modifier", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="mutation", + name="parent", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="proteins", + to="proteins.Protein", + verbose_name="Parent Protein", + ), + ), + migrations.AddField( + model_name="fretpair", + name="acceptor", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="FK_FRETacceptor_protein", + to="proteins.Protein", + verbose_name="acceptor", + ), + ), + migrations.AddField( + model_name="fretpair", + name="created_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="fretpair_author", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="fretpair", + name="donor", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="FK_FRETdonor_protein", + to="proteins.Protein", + verbose_name="donor", + ), + ), + migrations.AddField( + model_name="fretpair", + name="pair_references", + field=models.ManyToManyField(blank=True, related_name="FK_FRETpair_reference", to="references.Reference"), + ), + migrations.AddField( + model_name="fretpair", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="fretpair_modifier", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="bleachmeasurement", + name="state", + field=models.ForeignKey( + help_text="The state on which this measurement was made", + on_delete=django.db.models.deletion.CASCADE, + related_name="bleach_measurements", + to="proteins.State", + verbose_name="Protein (state)", + ), + ), + migrations.AddField( + model_name="bleachmeasurement", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="bleachmeasurement_modifier", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterUniqueTogether( + name="state", + unique_together={("protein", "ex_max", "em_max", "ext_coeff", "qy")}, + ), + migrations.AlterUniqueTogether( + name="proteincollection", + unique_together={("owner", "name")}, + ), + ] diff --git a/backend/proteins/migrations_old/0002_auto_20180312_0156.py b/backend/proteins/migrations_old/0002_auto_20180312_0156.py new file mode 100644 index 000000000..d2c657c43 --- /dev/null +++ b/backend/proteins/migrations_old/0002_auto_20180312_0156.py @@ -0,0 +1,59 @@ +# Generated by Django 1.11.9 on 2018-03-12 01:56 + +import django.core.validators +from django.db import migrations, models + +import proteins.fields + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="state", + name="twop_ex_max", + field=models.PositiveSmallIntegerField( + blank=True, + db_index=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(700), + django.core.validators.MaxValueValidator(1600), + ], + verbose_name="Peak 2P excitation", + ), + ), + migrations.AddField( + model_name="state", + name="twop_ex_spectra", + field=proteins.fields.SpectrumField( + blank=True, help_text="List of [[wavelength, value],...] pairs", null=True + ), + ), + migrations.AddField( + model_name="state", + name="twop_peakGM", + field=models.FloatField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(200), + ], + verbose_name="Peak 2P cross-section of S0->S1 (GM)", + ), + ), + migrations.AddField( + model_name="state", + name="twop_qy", + field=models.FloatField( + blank=True, + null=True, + validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], + verbose_name="2P Quantum Yield", + ), + ), + ] diff --git a/backend/proteins/migrations_old/0003_protein_oser.py b/backend/proteins/migrations_old/0003_protein_oser.py new file mode 100644 index 000000000..9bf837ff8 --- /dev/null +++ b/backend/proteins/migrations_old/0003_protein_oser.py @@ -0,0 +1,17 @@ +# Generated by Django 1.11.9 on 2018-03-12 22:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0002_auto_20180312_0156"), + ] + + operations = [ + migrations.AddField( + model_name="protein", + name="oser", + field=models.FloatField(blank=True, help_text="OSER score", null=True), + ), + ] diff --git a/backend/proteins/migrations/0004_auto_20180315_1323.py b/backend/proteins/migrations_old/0004_auto_20180315_1323.py similarity index 53% rename from backend/proteins/migrations/0004_auto_20180315_1323.py rename to backend/proteins/migrations_old/0004_auto_20180315_1323.py index d81658d8d..e51d59851 100644 --- a/backend/proteins/migrations/0004_auto_20180315_1323.py +++ b/backend/proteins/migrations_old/0004_auto_20180315_1323.py @@ -1,19 +1,16 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.11.9 on 2018-03-15 13:23 -from __future__ import unicode_literals from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('proteins', '0003_protein_oser'), + ("proteins", "0003_protein_oser"), ] operations = [ migrations.AlterUniqueTogether( - name='fretpair', - unique_together=set([('donor', 'acceptor')]), + name="fretpair", + unique_together={("donor", "acceptor")}, ), ] diff --git a/backend/proteins/migrations_old/0005_auto_20180328_1614.py b/backend/proteins/migrations_old/0005_auto_20180328_1614.py new file mode 100644 index 000000000..cacc46453 --- /dev/null +++ b/backend/proteins/migrations_old/0005_auto_20180328_1614.py @@ -0,0 +1,124 @@ +# Generated by Django 1.11.11 on 2018-03-28 16:14 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0004_auto_20180315_1323"), + ] + + operations = [ + migrations.AlterField( + model_name="bleachmeasurement", + name="created_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="bleachmeasurement_author", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="bleachmeasurement", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="bleachmeasurement_modifier", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="fretpair", + name="created_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="fretpair_author", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="fretpair", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="fretpair_modifier", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="organism", + name="created_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="organism_author", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="organism", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="organism_modifier", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="protein", + name="created_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="protein_author", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="protein", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="protein_modifier", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="state", + name="created_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="state_author", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="state", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="state_modifier", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/backend/proteins/migrations_old/0005_auto_20180328_1614_squashed_0010_auto_20180501_1547.py b/backend/proteins/migrations_old/0005_auto_20180328_1614_squashed_0010_auto_20180501_1547.py new file mode 100644 index 000000000..6ee936cc7 --- /dev/null +++ b/backend/proteins/migrations_old/0005_auto_20180328_1614_squashed_0010_auto_20180501_1547.py @@ -0,0 +1,649 @@ +# Generated by Django 1.11.11 on 2018-05-08 13:24 + +import django.contrib.postgres.fields +import django.core.validators +import django.db.models.deletion +import django.utils.timezone +import model_utils.fields +from django.conf import settings +from django.db import migrations, models + +import proteins.models.spectrum + + +def move_spectra(apps, schema_editor): + States = apps.get_model("proteins", "State") + Spectrum = apps.get_model("proteins", "Spectrum") + it = "p" + for state in States.objects.all(): + if state.ex_spectra: + Spectrum.objects.create( + data=state.ex_spectra.data, + owner_state=state, + subtype="ex", + category=it, + ) + if state.em_spectra: + Spectrum.objects.create( + data=state.em_spectra.data, + owner_state=state, + subtype="em", + category=it, + ) + if state.twop_ex_spectra: + spectrum = Spectrum( + data=state.twop_ex_spectra.data, + owner_state=state, + subtype="2p", + category=it, + ) + spectrum.save() + + +def undo_move_spectra(apps, schema_editor): + Spectrum = apps.get_model("proteins", "Spectrum") + for spectrum in Spectrum.objects.all(): + if spectrum.subtype and spectrum.owner_state: + if spectrum.subtype == "ex": + spectrum.owner_state.ex_spectra = spectrum.data + elif spectrum.subtype == "em": + spectrum.owner_state.em_spectra = spectrum.data + elif spectrum.subtype == "2p": + spectrum.owner_state.twop_ex_spectra = spectrum.data + + +class Migration(migrations.Migration): + replaces = [ + ("proteins", "0005_auto_20180328_1614"), + ("proteins", "0006_auto_20180401_2009"), + ("proteins", "0007_auto_20180403_0140"), + ("proteins", "0008_auto_20180430_0308"), + ("proteins", "0009_auto_20180430_0335"), + ("proteins", "0010_auto_20180501_1547"), + ] + + dependencies = [ + ("proteins", "0004_auto_20180315_1323"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name="bleachmeasurement", + name="created_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="bleachmeasurement_author", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="bleachmeasurement", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="bleachmeasurement_modifier", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="fretpair", + name="created_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="fretpair_author", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="fretpair", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="fretpair_modifier", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="organism", + name="created_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="organism_author", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="organism", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="organism_modifier", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="protein", + name="created_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="protein_author", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="protein", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="protein_modifier", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="state", + name="created_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="state_author", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="state", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="state_modifier", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.CreateModel( + name="Camera", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("slug", models.SlugField(help_text="Unique slug for the %(class)", max_length=128, unique=True)), + ("name", models.CharField(max_length=100)), + ], + ), + migrations.CreateModel( + name="Dye", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), + ( + "ex_max", + models.PositiveSmallIntegerField( + blank=True, + db_index=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(300), + django.core.validators.MaxValueValidator(900), + ], + ), + ), + ( + "em_max", + models.PositiveSmallIntegerField( + blank=True, + db_index=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(300), + django.core.validators.MaxValueValidator(1000), + ], + ), + ), + ( + "twop_ex_max", + models.PositiveSmallIntegerField( + blank=True, + db_index=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(700), + django.core.validators.MaxValueValidator(1600), + ], + verbose_name="Peak 2P excitation", + ), + ), + ( + "ext_coeff", + models.IntegerField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(300000), + ], + verbose_name="Extinction Coefficient", + ), + ), + ( + "twop_peakGM", + models.FloatField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(200), + ], + verbose_name="Peak 2P cross-section of S0->S1 (GM)", + ), + ), + ( + "qy", + models.FloatField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(1), + ], + verbose_name="Quantum Yield", + ), + ), + ( + "twop_qy", + models.FloatField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(1), + ], + verbose_name="2P Quantum Yield", + ), + ), + ("brightness", models.FloatField(blank=True, editable=False, null=True)), + ( + "pka", + models.FloatField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(2), + django.core.validators.MaxValueValidator(12), + ], + verbose_name="pKa", + ), + ), + ( + "lifetime", + models.FloatField( + blank=True, + help_text="Lifetime (ns)", + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(20), + ], + ), + ), + ("slug", models.SlugField(help_text="Unique slug for the %(class)", max_length=128, unique=True)), + ("name", models.CharField(max_length=100)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="Filter", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("slug", models.SlugField(help_text="Unique slug for the %(class)", max_length=128, unique=True)), + ("name", models.CharField(max_length=100)), + ], + ), + migrations.CreateModel( + name="Light", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("slug", models.SlugField(help_text="Unique slug for the %(class)", max_length=128, unique=True)), + ("name", models.CharField(max_length=100)), + ], + ), + migrations.CreateModel( + name="Spectrum", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), + ( + "data", + proteins.models.spectrum.SpectrumData( + base_field=django.contrib.postgres.fields.ArrayField( + base_field=models.FloatField(max_length=10), size=2 + ), + size=None, + ), + ), + ( + "category", + models.CharField( + blank=True, + choices=[ + ("d", "Dye"), + ("p", "Protein"), + ("l", "Light Source"), + ("f", "Filter"), + ("c", "Camera"), + ], + db_index=True, + max_length=1, + verbose_name="Item Type", + ), + ), + ( + "subtype", + models.CharField( + choices=[ + ("ex", "excitation"), + ("ab", "absorption"), + ("em", "emission"), + ("2p", "two photon absorption"), + ("bx", "bandpass (excitation)"), + ("bm", "bandpass (emission)"), + ("sp", "shortpass"), + ("lp", "longpass"), + ("bs", "beamsplitter"), + ("qe", "Quantum Efficiency"), + ("pd", "Power Distribution"), + ], + db_index=True, + max_length=2, + verbose_name="Spectra Subtype", + ), + ), + ("ph", models.FloatField(blank=True, null=True, verbose_name="pH")), + ("solvent", models.CharField(blank=True, max_length=128)), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="spectrum_author", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "owner_camera", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="spectra", + to="proteins.Camera", + ), + ), + ( + "owner_dye", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="spectra", + to="proteins.Dye", + ), + ), + ( + "owner_filter", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="spectra", + to="proteins.Filter", + ), + ), + ( + "owner_light", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="spectra", + to="proteins.Light", + ), + ), + ( + "owner_state", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="spectra", + to="proteins.State", + ), + ), + ( + "updated_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="spectrum_modifier", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.RunPython( + code=move_spectra, + reverse_code=undo_move_spectra, + ), + migrations.RemoveField( + model_name="state", + name="em_spectra", + ), + migrations.RemoveField( + model_name="state", + name="ex_spectra", + ), + migrations.RemoveField( + model_name="state", + name="twop_ex_spectra", + ), + migrations.AddField( + model_name="filter", + name="aoi", + field=models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(90)], + ), + ), + migrations.AddField( + model_name="filter", + name="bandcenter", + field=models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(200), + django.core.validators.MaxValueValidator(1600), + ], + ), + ), + migrations.AddField( + model_name="filter", + name="bandwidth", + field=models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(300), + django.core.validators.MaxValueValidator(900), + ], + ), + ), + migrations.AddField( + model_name="filter", + name="edge", + field=models.FloatField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(200), + ], + ), + ), + migrations.AddField( + model_name="filter", + name="tavg", + field=models.FloatField( + blank=True, + null=True, + validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], + ), + ), + migrations.AddField( + model_name="camera", + name="manufacturer", + field=models.CharField(blank=True, max_length=128), + ), + migrations.AddField( + model_name="filter", + name="manufacturer", + field=models.CharField(blank=True, max_length=128), + ), + migrations.AddField( + model_name="light", + name="manufacturer", + field=models.CharField(blank=True, max_length=128), + ), + migrations.AlterField( + model_name="spectrum", + name="category", + field=models.CharField( + choices=[("d", "Dye"), ("p", "Protein"), ("l", "Light Source"), ("f", "Filter"), ("c", "Camera")], + db_index=True, + max_length=1, + verbose_name="Item Type", + ), + ), + migrations.AlterField( + model_name="spectrum", + name="subtype", + field=models.CharField( + blank=True, + choices=[ + ("ex", "excitation"), + ("ab", "absorption"), + ("em", "emission"), + ("2p", "two photon absorption"), + ("bx", "bandpass (excitation)"), + ("bm", "bandpass (emission)"), + ("sp", "shortpass"), + ("lp", "longpass"), + ("bs", "beamsplitter"), + ("qe", "Quantum Efficiency"), + ("pd", "Power Distribution"), + ], + db_index=True, + max_length=2, + verbose_name="Spectra Subtype", + ), + ), + migrations.AlterField( + model_name="state", + name="slug", + field=models.SlugField(help_text="Unique slug for the %(class)", max_length=128, unique=True), + ), + migrations.AddField( + model_name="camera", + name="part", + field=models.CharField(blank=True, max_length=128), + ), + migrations.AddField( + model_name="dye", + name="manufacturer", + field=models.CharField(blank=True, max_length=128), + ), + migrations.AddField( + model_name="dye", + name="part", + field=models.CharField(blank=True, max_length=128), + ), + migrations.AddField( + model_name="filter", + name="part", + field=models.CharField(blank=True, max_length=128), + ), + migrations.AddField( + model_name="light", + name="part", + field=models.CharField(blank=True, max_length=128), + ), + migrations.AlterField( + model_name="spectrum", + name="subtype", + field=models.CharField( + blank=True, + choices=[ + ("ex", "Excitation"), + ("ab", "Absorption"), + ("em", "Emission"), + ("2p", "Two Photon Absorption"), + ("bp", "Bandpass"), + ("bx", "Bandpass (Excitation)"), + ("bm", "Bandpass (Emission)"), + ("sp", "Shortpass"), + ("lp", "Longpass"), + ("bs", "Beamsplitter"), + ("qe", "Quantum Efficiency"), + ("pd", "Power Distribution"), + ], + db_index=True, + max_length=2, + verbose_name="Spectra Subtype", + ), + ), + ] diff --git a/backend/proteins/migrations_old/0006_auto_20180401_2009.py b/backend/proteins/migrations_old/0006_auto_20180401_2009.py new file mode 100644 index 000000000..10ddb38b9 --- /dev/null +++ b/backend/proteins/migrations_old/0006_auto_20180401_2009.py @@ -0,0 +1,374 @@ +# Generated by Django 1.11.9 on 2018-04-01 20:09 + +import django.contrib.postgres.fields +import django.core.validators +import django.db.models.deletion +import django.utils.timezone +import model_utils.fields +from django.conf import settings +from django.db import migrations, models + +import proteins.models.spectrum + + +def move_spectra(apps, schema_editor): + States = apps.get_model("proteins", "State") + Spectrum = apps.get_model("proteins", "Spectrum") + it = "p" + for state in States.objects.all(): + if state.ex_spectrum: + Spectrum.objects.create( + data=state.ex_spectrum.data, + owner_state=state, + subtype="ex", + category=it, + ) + if state.em_spectrum: + Spectrum.objects.create( + data=state.em_spectrum.data, + owner_state=state, + subtype="em", + category=it, + ) + if state.twop_ex_spectrum: + spectrum = Spectrum( + data=state.twop_ex_spectrum.data, + owner_state=state, + subtype="2p", + category=it, + ) + spectrum.save() + + +def undo_move_spectra(apps, schema_editor): + Spectrum = apps.get_model("proteins", "Spectrum") + for spectrum in Spectrum.objects.all(): + if spectrum.subtype and spectrum.owner_state: + if spectrum.subtype == "ex": + spectrum.owner_state.ex_spectra = spectrum.data + elif spectrum.subtype == "em": + spectrum.owner_state.em_spectra = spectrum.data + elif spectrum.subtype == "2p": + spectrum.owner_state.twop_ex_spectra = spectrum.data + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("proteins", "0005_auto_20180328_1614"), + ] + + operations = [ + migrations.CreateModel( + name="Camera", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("slug", models.SlugField(help_text="Unique slug for the %(class)", max_length=128, unique=True)), + ("name", models.CharField(max_length=100)), + ], + ), + migrations.CreateModel( + name="Dye", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), + ( + "ex_max", + models.PositiveSmallIntegerField( + blank=True, + db_index=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(300), + django.core.validators.MaxValueValidator(900), + ], + ), + ), + ( + "em_max", + models.PositiveSmallIntegerField( + blank=True, + db_index=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(300), + django.core.validators.MaxValueValidator(1000), + ], + ), + ), + ( + "twop_ex_max", + models.PositiveSmallIntegerField( + blank=True, + db_index=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(700), + django.core.validators.MaxValueValidator(1600), + ], + verbose_name="Peak 2P excitation", + ), + ), + ( + "ext_coeff", + models.IntegerField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(300000), + ], + verbose_name="Extinction Coefficient", + ), + ), + ( + "twop_peakGM", + models.FloatField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(200), + ], + verbose_name="Peak 2P cross-section of S0->S1 (GM)", + ), + ), + ( + "qy", + models.FloatField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(1), + ], + verbose_name="Quantum Yield", + ), + ), + ( + "twop_qy", + models.FloatField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(1), + ], + verbose_name="2P Quantum Yield", + ), + ), + ("brightness", models.FloatField(blank=True, editable=False, null=True)), + ( + "pka", + models.FloatField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(2), + django.core.validators.MaxValueValidator(12), + ], + verbose_name="pKa", + ), + ), + ( + "lifetime", + models.FloatField( + blank=True, + help_text="Lifetime (ns)", + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(20), + ], + ), + ), + ("slug", models.SlugField(help_text="Unique slug for the %(class)", max_length=128, unique=True)), + ("name", models.CharField(max_length=100)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="Filter", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("slug", models.SlugField(help_text="Unique slug for the %(class)", max_length=128, unique=True)), + ("name", models.CharField(max_length=100)), + ], + ), + migrations.CreateModel( + name="Light", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("slug", models.SlugField(help_text="Unique slug for the %(class)", max_length=128, unique=True)), + ("name", models.CharField(max_length=100)), + ], + ), + migrations.CreateModel( + name="Spectrum", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), + ( + "data", + proteins.models.spectrum.SpectrumData( + base_field=django.contrib.postgres.fields.ArrayField( + base_field=models.FloatField(max_length=10), size=2 + ), + size=None, + ), + ), + ( + "category", + models.CharField( + blank=True, + choices=[ + ("d", "Dye"), + ("p", "Protein"), + ("l", "Light Source"), + ("f", "Filter"), + ("c", "Camera"), + ], + db_index=True, + max_length=1, + verbose_name="Item Type", + ), + ), + ( + "subtype", + models.CharField( + choices=[ + ("ex", "excitation"), + ("ab", "absorption"), + ("em", "emission"), + ("2p", "two photon absorption"), + ("bx", "bandpass (excitation)"), + ("bm", "bandpass (emission)"), + ("sp", "shortpass"), + ("lp", "longpass"), + ("bs", "beamsplitter"), + ("qe", "Quantum Efficiency"), + ("pd", "Power Distribution"), + ], + db_index=True, + max_length=2, + verbose_name="Spectra Subtype", + ), + ), + ("ph", models.FloatField(blank=True, null=True, verbose_name="pH")), + ("solvent", models.CharField(blank=True, max_length=128)), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="spectrum_author", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "owner_camera", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="spectra", + to="proteins.Camera", + ), + ), + ( + "owner_dye", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="spectra", + to="proteins.Dye", + ), + ), + ( + "owner_filter", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="spectra", + to="proteins.Filter", + ), + ), + ( + "owner_light", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="spectra", + to="proteins.Light", + ), + ), + ( + "owner_state", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="spectra", + to="proteins.State", + ), + ), + ( + "updated_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="spectrum_modifier", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.RunPython( + code=move_spectra, + reverse_code=undo_move_spectra, + ), + migrations.RemoveField( + model_name="state", + name="em_spectra", + ), + migrations.RemoveField( + model_name="state", + name="ex_spectra", + ), + migrations.RemoveField( + model_name="state", + name="twop_ex_spectra", + ), + ] diff --git a/backend/proteins/migrations/0006_auto_20180512_0058.py b/backend/proteins/migrations_old/0006_auto_20180512_0058.py similarity index 57% rename from backend/proteins/migrations/0006_auto_20180512_0058.py rename to backend/proteins/migrations_old/0006_auto_20180512_0058.py index aa2c13e30..31d0b226b 100644 --- a/backend/proteins/migrations/0006_auto_20180512_0058.py +++ b/backend/proteins/migrations_old/0006_auto_20180512_0058.py @@ -4,14 +4,13 @@ class Migration(migrations.Migration): - dependencies = [ - ('proteins', '0005_auto_20180328_1614_squashed_0010_auto_20180501_1547'), + ("proteins", "0005_auto_20180328_1614_squashed_0010_auto_20180501_1547"), ] operations = [ migrations.AlterModelOptions( - name='spectrum', - options={'verbose_name_plural': 'spectra'}, + name="spectrum", + options={"verbose_name_plural": "spectra"}, ), ] diff --git a/backend/proteins/migrations_old/0007_auto_20180403_0140.py b/backend/proteins/migrations_old/0007_auto_20180403_0140.py new file mode 100644 index 000000000..39e67da2a --- /dev/null +++ b/backend/proteins/migrations_old/0007_auto_20180403_0140.py @@ -0,0 +1,70 @@ +# Generated by Django 1.11.9 on 2018-04-03 01:40 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0006_auto_20180401_2009"), + ] + + operations = [ + migrations.AddField( + model_name="filter", + name="aoi", + field=models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(90)], + ), + ), + migrations.AddField( + model_name="filter", + name="bandcenter", + field=models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(200), + django.core.validators.MaxValueValidator(1600), + ], + ), + ), + migrations.AddField( + model_name="filter", + name="bandwidth", + field=models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(300), + django.core.validators.MaxValueValidator(900), + ], + ), + ), + migrations.AddField( + model_name="filter", + name="edge", + field=models.FloatField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(200), + ], + ), + ), + migrations.AddField( + model_name="filter", + name="tavg", + field=models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(300), + django.core.validators.MaxValueValidator(900), + ], + ), + ), + ] diff --git a/backend/proteins/migrations_old/0007_auto_20180513_2346.py b/backend/proteins/migrations_old/0007_auto_20180513_2346.py new file mode 100644 index 000000000..0bbebf40c --- /dev/null +++ b/backend/proteins/migrations_old/0007_auto_20180513_2346.py @@ -0,0 +1,103 @@ +# Generated by Django 2.0.5 on 2018-05-13 23:46 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("proteins", "0006_auto_20180512_0058"), + ] + + operations = [ + migrations.AddField( + model_name="camera", + name="created_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="camera_author", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="camera", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="camera_modifier", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="dye", + name="created_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="dye_author", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="dye", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="dye_modifier", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="filter", + name="created_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="filter_author", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="filter", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="filter_modifier", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="light", + name="created_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="light_author", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="light", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="light_modifier", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/backend/proteins/migrations_old/0008_auto_20180430_0308.py b/backend/proteins/migrations_old/0008_auto_20180430_0308.py new file mode 100644 index 000000000..0456efc81 --- /dev/null +++ b/backend/proteins/migrations_old/0008_auto_20180430_0308.py @@ -0,0 +1,65 @@ +# Generated by Django 1.11.9 on 2018-04-30 03:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0007_auto_20180403_0140"), + ] + + operations = [ + migrations.AddField( + model_name="camera", + name="manufacturer", + field=models.CharField(blank=True, max_length=128), + ), + migrations.AddField( + model_name="filter", + name="manufacturer", + field=models.CharField(blank=True, max_length=128), + ), + migrations.AddField( + model_name="light", + name="manufacturer", + field=models.CharField(blank=True, max_length=128), + ), + migrations.AlterField( + model_name="spectrum", + name="category", + field=models.CharField( + choices=[("d", "Dye"), ("p", "Protein"), ("l", "Light Source"), ("f", "Filter"), ("c", "Camera")], + db_index=True, + max_length=1, + verbose_name="Item Type", + ), + ), + migrations.AlterField( + model_name="spectrum", + name="subtype", + field=models.CharField( + blank=True, + choices=[ + ("ex", "excitation"), + ("ab", "absorption"), + ("em", "emission"), + ("2p", "two photon absorption"), + ("bx", "bandpass (excitation)"), + ("bm", "bandpass (emission)"), + ("sp", "shortpass"), + ("lp", "longpass"), + ("bs", "beamsplitter"), + ("qe", "Quantum Efficiency"), + ("pd", "Power Distribution"), + ], + db_index=True, + max_length=2, + verbose_name="Spectra Subtype", + ), + ), + migrations.AlterField( + model_name="state", + name="slug", + field=models.SlugField(help_text="Unique slug for the %(class)", max_length=128, unique=True), + ), + ] diff --git a/backend/proteins/migrations/0008_auto_20180515_1659.py b/backend/proteins/migrations_old/0008_auto_20180515_1659.py similarity index 66% rename from backend/proteins/migrations/0008_auto_20180515_1659.py rename to backend/proteins/migrations_old/0008_auto_20180515_1659.py index cc61bdba3..b4fb8ae8b 100644 --- a/backend/proteins/migrations/0008_auto_20180515_1659.py +++ b/backend/proteins/migrations_old/0008_auto_20180515_1659.py @@ -4,30 +4,29 @@ class Migration(migrations.Migration): - dependencies = [ - ('proteins', '0007_auto_20180513_2346'), + ("proteins", "0007_auto_20180513_2346"), ] operations = [ migrations.AddField( - model_name='camera', - name='url', + model_name="camera", + name="url", field=models.URLField(blank=True), ), migrations.AddField( - model_name='dye', - name='url', + model_name="dye", + name="url", field=models.URLField(blank=True), ), migrations.AddField( - model_name='filter', - name='url', + model_name="filter", + name="url", field=models.URLField(blank=True), ), migrations.AddField( - model_name='light', - name='url', + model_name="light", + name="url", field=models.URLField(blank=True), ), ] diff --git a/backend/proteins/migrations_old/0009_auto_20180430_0335.py b/backend/proteins/migrations_old/0009_auto_20180430_0335.py new file mode 100644 index 000000000..d57f11276 --- /dev/null +++ b/backend/proteins/migrations_old/0009_auto_20180430_0335.py @@ -0,0 +1,22 @@ +# Generated by Django 1.11.9 on 2018-04-30 03:35 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0008_auto_20180430_0308"), + ] + + operations = [ + migrations.AlterField( + model_name="filter", + name="tavg", + field=models.FloatField( + blank=True, + null=True, + validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], + ), + ), + ] diff --git a/backend/proteins/migrations_old/0009_auto_20180525_1640.py b/backend/proteins/migrations_old/0009_auto_20180525_1640.py new file mode 100644 index 000000000..235286172 --- /dev/null +++ b/backend/proteins/migrations_old/0009_auto_20180525_1640.py @@ -0,0 +1,35 @@ +# Generated by Django 2.0.5 on 2018-05-25 16:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0008_auto_20180515_1659"), + ] + + operations = [ + migrations.AlterField( + model_name="spectrum", + name="subtype", + field=models.CharField( + choices=[ + ("ex", "Excitation"), + ("ab", "Absorption"), + ("em", "Emission"), + ("2p", "Two Photon Absorption"), + ("bp", "Bandpass"), + ("bx", "Bandpass (Excitation)"), + ("bm", "Bandpass (Emission)"), + ("sp", "Shortpass"), + ("lp", "Longpass"), + ("bs", "Beamsplitter"), + ("qe", "Quantum Efficiency"), + ("pd", "Power Distribution"), + ], + db_index=True, + max_length=2, + verbose_name="Spectra Subtype", + ), + ), + ] diff --git a/backend/proteins/migrations_old/0010_auto_20180501_1547.py b/backend/proteins/migrations_old/0010_auto_20180501_1547.py new file mode 100644 index 000000000..1dcac3fc3 --- /dev/null +++ b/backend/proteins/migrations_old/0010_auto_20180501_1547.py @@ -0,0 +1,61 @@ +# Generated by Django 1.11.11 on 2018-05-01 15:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0009_auto_20180430_0335"), + ] + + operations = [ + migrations.AddField( + model_name="camera", + name="part", + field=models.CharField(blank=True, max_length=128), + ), + migrations.AddField( + model_name="dye", + name="manufacturer", + field=models.CharField(blank=True, max_length=128), + ), + migrations.AddField( + model_name="dye", + name="part", + field=models.CharField(blank=True, max_length=128), + ), + migrations.AddField( + model_name="filter", + name="part", + field=models.CharField(blank=True, max_length=128), + ), + migrations.AddField( + model_name="light", + name="part", + field=models.CharField(blank=True, max_length=128), + ), + migrations.AlterField( + model_name="spectrum", + name="subtype", + field=models.CharField( + blank=True, + choices=[ + ("ex", "Excitation"), + ("ab", "Absorption"), + ("em", "Emission"), + ("2p", "Two Photon Absorption"), + ("bp", "Bandpass"), + ("bx", "Bandpass (Excitation)"), + ("bm", "Bandpass (Emission)"), + ("sp", "Shortpass"), + ("lp", "Longpass"), + ("bs", "Beamsplitter"), + ("qe", "Quantum Efficiency"), + ("pd", "Power Distribution"), + ], + db_index=True, + max_length=2, + verbose_name="Spectra Subtype", + ), + ), + ] diff --git a/backend/proteins/migrations_old/0010_auto_20180525_1840.py b/backend/proteins/migrations_old/0010_auto_20180525_1840.py new file mode 100644 index 000000000..a66784450 --- /dev/null +++ b/backend/proteins/migrations_old/0010_auto_20180525_1840.py @@ -0,0 +1,56 @@ +# Generated by Django 2.0.5 on 2018-05-25 18:40 + +import django.utils.timezone +import model_utils.fields +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0009_auto_20180525_1640"), + ] + + operations = [ + migrations.AddField( + model_name="camera", + name="created", + field=model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + migrations.AddField( + model_name="camera", + name="modified", + field=model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), + migrations.AddField( + model_name="filter", + name="created", + field=model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + migrations.AddField( + model_name="filter", + name="modified", + field=model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), + migrations.AddField( + model_name="light", + name="created", + field=model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + migrations.AddField( + model_name="light", + name="modified", + field=model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), + ] diff --git a/backend/proteins/migrations_old/0011_auto_20180525_2349.py b/backend/proteins/migrations_old/0011_auto_20180525_2349.py new file mode 100644 index 000000000..1698f57c0 --- /dev/null +++ b/backend/proteins/migrations_old/0011_auto_20180525_2349.py @@ -0,0 +1,45 @@ +# Generated by Django 2.0.5 on 2018-05-25 23:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0010_auto_20180525_1840"), + ] + + operations = [ + migrations.AlterField( + model_name="spectrum", + name="category", + field=models.CharField( + choices=[("d", "Dye"), ("p", "Protein"), ("l", "Light Source"), ("f", "Filter"), ("c", "Camera")], + db_index=True, + max_length=1, + verbose_name="Owner Type", + ), + ), + migrations.AlterField( + model_name="spectrum", + name="subtype", + field=models.CharField( + choices=[ + ("ex", "Excitation"), + ("ab", "Absorption"), + ("em", "Emission"), + ("2p", "Two Photon Abs"), + ("bp", "Bandpass"), + ("bx", "Bandpass-Ex"), + ("bm", "Bandpass-Em"), + ("sp", "Shortpass"), + ("lp", "Longpass"), + ("bs", "Beamsplitter"), + ("qe", "Quantum Efficiency"), + ("pd", "Power Distribution"), + ], + db_index=True, + max_length=2, + verbose_name="Spectrum Subtype", + ), + ), + ] diff --git a/backend/proteins/migrations_old/0012_auto_20180708_1811.py b/backend/proteins/migrations_old/0012_auto_20180708_1811.py new file mode 100644 index 000000000..2d7aa3f76 --- /dev/null +++ b/backend/proteins/migrations_old/0012_auto_20180708_1811.py @@ -0,0 +1,37 @@ +# Generated by Django 2.0.6 on 2018-07-08 18:11 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("proteins", "0011_auto_20180525_2349"), + ] + + operations = [ + migrations.AddField( + model_name="statetransition", + name="created_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="statetransition_author", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="statetransition", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="statetransition_modifier", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/backend/proteins/migrations_old/0013_auto_20180718_1717.py b/backend/proteins/migrations_old/0013_auto_20180718_1717.py new file mode 100644 index 000000000..088ae3c7f --- /dev/null +++ b/backend/proteins/migrations_old/0013_auto_20180718_1717.py @@ -0,0 +1,348 @@ +# Generated by Django 2.0.6 on 2018-07-18 17:17 + +import django.contrib.postgres.fields +import django.core.validators +import django.db.models.deletion +import django.utils.timezone +import model_utils.fields +from django.conf import settings +from django.db import migrations, models + +import proteins.util.helpers + + +def resavestuff(apps, schema_editor): + Filter = apps.get_model("proteins", "Filter") + Light = apps.get_model("proteins", "Light") + + for f in Filter.objects.filter(name__icontains="ff0", spectrum__subtype="bs", manufacturer__icontains="semrock"): + f.subtype = "bp" + for l in Light.objects.filter(manufacturer="lumencor"): + l.manufacturer = "Lumencor" + l.save() + for f in Filter.objects.filter(manufacturer="lumencor"): + f.manufacturer = "Lumencor" + if f.part[:5].isdigit(): + f.part = f.part[:3] + "/" + f.part[3:] + f.save() + + +def resavestuff_back(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("proteins", "0012_auto_20180708_1811"), + ] + + operations = [ + migrations.CreateModel( + name="FilterPlacement", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "path", + models.CharField( + choices=[("ex", "Excitation Path"), ("em", "Emission Path"), ("bs", "Both Paths")], + max_length=2, + verbose_name="Ex/Em Path", + ), + ), + ( + "reflects", + models.BooleanField( + default=False, help_text="Filter reflects light at this position in the light path" + ), + ), + ], + ), + migrations.CreateModel( + name="FluorophoreCollection", + fields=[ + ( + "proteincollection_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="proteins.ProteinCollection", + ), + ), + ("dyes", models.ManyToManyField(blank=True, related_name="collection_memberships", to="proteins.Dye")), + ], + options={ + "abstract": False, + }, + bases=("proteins.proteincollection",), + ), + migrations.CreateModel( + name="Microscope", + fields=[ + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), + ("name", models.CharField(max_length=100)), + ("description", models.CharField(blank=True, max_length=512)), + ( + "id", + models.CharField( + default=proteins.util.helpers.shortuuid, + editable=False, + max_length=22, + primary_key=True, + serialize=False, + ), + ), + ( + "lasers", + django.contrib.postgres.fields.ArrayField( + base_field=models.PositiveSmallIntegerField( + validators=[ + django.core.validators.MinValueValidator(300), + django.core.validators.MaxValueValidator(1600), + ] + ), + blank=True, + default=list, + size=None, + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="OpticalConfig", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), + ("name", models.CharField(max_length=100)), + ("description", models.CharField(blank=True, max_length=512)), + ( + "laser", + models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(300), + django.core.validators.MaxValueValidator(1600), + ], + ), + ), + ], + options={ + "ordering": ["name"], + }, + ), + migrations.AlterModelOptions( + name="camera", + options={"ordering": ["name"]}, + ), + migrations.AlterModelOptions( + name="filter", + options={"ordering": ["bandcenter"]}, + ), + migrations.AlterModelOptions( + name="light", + options={"ordering": ["name"]}, + ), + migrations.AlterField( + model_name="proteincollection", + name="owner", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="proteincollections", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="spectrum", + name="category", + field=models.CharField( + choices=[("d", "Dye"), ("p", "Protein"), ("l", "Light Source"), ("f", "Filter"), ("c", "Camera")], + db_index=True, + max_length=1, + verbose_name="Spectrum Type", + ), + ), + migrations.AlterField( + model_name="spectrum", + name="owner_camera", + field=models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="spectrum", + to="proteins.Camera", + ), + ), + migrations.AlterField( + model_name="spectrum", + name="owner_filter", + field=models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="spectrum", + to="proteins.Filter", + ), + ), + migrations.AlterField( + model_name="spectrum", + name="owner_light", + field=models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="spectrum", + to="proteins.Light", + ), + ), + migrations.AddField( + model_name="opticalconfig", + name="camera", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="optical_configs", + to="proteins.Camera", + ), + ), + migrations.AddField( + model_name="opticalconfig", + name="filters", + field=models.ManyToManyField( + blank=True, related_name="optical_configs", through="proteins.FilterPlacement", to="proteins.Filter" + ), + ), + migrations.AddField( + model_name="opticalconfig", + name="light", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="optical_configs", + to="proteins.Light", + ), + ), + migrations.AddField( + model_name="opticalconfig", + name="microscope", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, related_name="optical_configs", to="proteins.Microscope" + ), + ), + migrations.AddField( + model_name="opticalconfig", + name="owner", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="opticalconfigs", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="microscope", + name="bs_filters", + field=models.ManyToManyField(blank=True, related_name="as_bs_filter", to="proteins.Filter"), + ), + migrations.AddField( + model_name="microscope", + name="cameras", + field=models.ManyToManyField(blank=True, related_name="microscopes", to="proteins.Camera"), + ), + migrations.AddField( + model_name="microscope", + name="collection", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="on_scope", + to="proteins.ProteinCollection", + ), + ), + migrations.AddField( + model_name="microscope", + name="em_filters", + field=models.ManyToManyField(blank=True, related_name="as_em_filter", to="proteins.Filter"), + ), + migrations.AddField( + model_name="microscope", + name="ex_filters", + field=models.ManyToManyField(blank=True, related_name="as_ex_filter", to="proteins.Filter"), + ), + migrations.AddField( + model_name="microscope", + name="fluors", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="fluor_on_scope", + to="proteins.FluorophoreCollection", + ), + ), + migrations.AddField( + model_name="microscope", + name="lights", + field=models.ManyToManyField(blank=True, related_name="microscopes", to="proteins.Light"), + ), + migrations.AddField( + model_name="microscope", + name="owner", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="microscopes", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="filterplacement", + name="config", + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="proteins.OpticalConfig"), + ), + migrations.AddField( + model_name="filterplacement", + name="filter", + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="proteins.Filter"), + ), + migrations.AlterUniqueTogether( + name="opticalconfig", + unique_together={("name", "microscope")}, + ), + migrations.RunPython(resavestuff, resavestuff_back), + ] diff --git a/backend/proteins/migrations_old/0014_auto_20180718_2340.py b/backend/proteins/migrations_old/0014_auto_20180718_2340.py new file mode 100644 index 000000000..0c8b48bf6 --- /dev/null +++ b/backend/proteins/migrations_old/0014_auto_20180718_2340.py @@ -0,0 +1,35 @@ +# Generated by Django 2.0.7 on 2018-07-18 23:40 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0013_auto_20180718_1717"), + ] + + operations = [ + migrations.AlterField( + model_name="opticalconfig", + name="camera", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="optical_configs", + to="proteins.Camera", + ), + ), + migrations.AlterField( + model_name="opticalconfig", + name="light", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="optical_configs", + to="proteins.Light", + ), + ), + ] diff --git a/backend/proteins/migrations_old/0015_auto_20180720_1921.py b/backend/proteins/migrations_old/0015_auto_20180720_1921.py new file mode 100644 index 000000000..d6da9353a --- /dev/null +++ b/backend/proteins/migrations_old/0015_auto_20180720_1921.py @@ -0,0 +1,38 @@ +# Generated by Django 2.0.6 on 2018-07-20 19:21 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0014_auto_20180718_2340"), + ] + + operations = [ + migrations.AlterModelOptions( + name="microscope", + options={"ordering": ["created"]}, + ), + migrations.AddField( + model_name="microscope", + name="managers", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.EmailField(max_length=254), blank=True, default=list, size=None + ), + ), + migrations.AddField( + model_name="opticalconfig", + name="managers", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.EmailField(max_length=254), blank=True, default=list, size=None + ), + ), + migrations.AddField( + model_name="proteincollection", + name="managers", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.EmailField(max_length=254), blank=True, default=list, size=None + ), + ), + ] diff --git a/backend/proteins/migrations/0016_auto_20180722_1314.py b/backend/proteins/migrations_old/0016_auto_20180722_1314.py similarity index 51% rename from backend/proteins/migrations/0016_auto_20180722_1314.py rename to backend/proteins/migrations_old/0016_auto_20180722_1314.py index d106dec6e..3b2300ee7 100644 --- a/backend/proteins/migrations/0016_auto_20180722_1314.py +++ b/backend/proteins/migrations_old/0016_auto_20180722_1314.py @@ -4,39 +4,38 @@ class Migration(migrations.Migration): - dependencies = [ - ('proteins', '0015_auto_20180720_1921'), + ("proteins", "0015_auto_20180720_1921"), ] operations = [ migrations.RemoveField( - model_name='microscope', - name='bs_filters', + model_name="microscope", + name="bs_filters", ), migrations.RemoveField( - model_name='microscope', - name='cameras', + model_name="microscope", + name="cameras", ), migrations.RemoveField( - model_name='microscope', - name='em_filters', + model_name="microscope", + name="em_filters", ), migrations.RemoveField( - model_name='microscope', - name='ex_filters', + model_name="microscope", + name="ex_filters", ), migrations.RemoveField( - model_name='microscope', - name='lasers', + model_name="microscope", + name="lasers", ), migrations.RemoveField( - model_name='microscope', - name='lights', + model_name="microscope", + name="lights", ), migrations.AddField( - model_name='opticalconfig', - name='comments', + model_name="opticalconfig", + name="comments", field=models.CharField(blank=True, max_length=256), ), ] diff --git a/backend/proteins/migrations_old/0017_auto_20180722_1626.py b/backend/proteins/migrations_old/0017_auto_20180722_1626.py new file mode 100644 index 000000000..b2e949377 --- /dev/null +++ b/backend/proteins/migrations_old/0017_auto_20180722_1626.py @@ -0,0 +1,39 @@ +# Generated by Django 2.0.7 on 2018-07-22 16:26 + +import django.contrib.postgres.fields +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0016_auto_20180722_1314"), + ] + + operations = [ + migrations.AddField( + model_name="microscope", + name="extra_cameras", + field=models.ManyToManyField(blank=True, related_name="microscopes", to="proteins.Camera"), + ), + migrations.AddField( + model_name="microscope", + name="extra_lasers", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.PositiveSmallIntegerField( + validators=[ + django.core.validators.MinValueValidator(300), + django.core.validators.MaxValueValidator(1600), + ] + ), + blank=True, + default=list, + size=None, + ), + ), + migrations.AddField( + model_name="microscope", + name="extra_lights", + field=models.ManyToManyField(blank=True, related_name="microscopes", to="proteins.Light"), + ), + ] diff --git a/backend/proteins/migrations/0018_protein_cofactor.py b/backend/proteins/migrations_old/0018_protein_cofactor.py similarity index 51% rename from backend/proteins/migrations/0018_protein_cofactor.py rename to backend/proteins/migrations_old/0018_protein_cofactor.py index edfe0b6dc..6d9a69d40 100644 --- a/backend/proteins/migrations/0018_protein_cofactor.py +++ b/backend/proteins/migrations_old/0018_protein_cofactor.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('proteins', '0017_auto_20180722_1626'), + ("proteins", "0017_auto_20180722_1626"), ] operations = [ migrations.AddField( - model_name='protein', - name='cofactor', - field=models.CharField(blank=True, choices=[('bv', 'Biliverdin')], max_length=2), + model_name="protein", + name="cofactor", + field=models.CharField(blank=True, choices=[("bv", "Biliverdin")], max_length=2), ), ] diff --git a/backend/proteins/migrations_old/0019_auto_20180723_2200.py b/backend/proteins/migrations_old/0019_auto_20180723_2200.py new file mode 100644 index 000000000..27efede00 --- /dev/null +++ b/backend/proteins/migrations_old/0019_auto_20180723_2200.py @@ -0,0 +1,22 @@ +# Generated by Django 2.0.7 on 2018-07-23 22:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0018_protein_cofactor"), + ] + + operations = [ + migrations.AlterField( + model_name="protein", + name="cofactor", + field=models.CharField( + blank=True, + choices=[("bv", "Biliverdin"), ("fl", "Flavin")], + help_text="Required for fluorescence", + max_length=2, + ), + ), + ] diff --git a/backend/proteins/migrations_old/0020_auto_20180729_0234.py b/backend/proteins/migrations_old/0020_auto_20180729_0234.py new file mode 100644 index 000000000..eaf6756e8 --- /dev/null +++ b/backend/proteins/migrations_old/0020_auto_20180729_0234.py @@ -0,0 +1,58 @@ +# Generated by Django 2.0.7 on 2018-07-29 02:34 + +import django.core.validators +from django.db import migrations, models + +import proteins.models.protein +import proteins.validators + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0019_auto_20180723_2200"), + ] + + operations = [ + migrations.AddField( + model_name="protein", + name="seq_validated", + field=models.BooleanField(default=False, help_text="Sequence has been validated by a moderator"), + ), + migrations.AlterField( + model_name="filter", + name="bandwidth", + field=models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(900), + ], + ), + ), + migrations.AlterField( + model_name="filter", + name="edge", + field=models.FloatField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(300), + django.core.validators.MaxValueValidator(1600), + ], + ), + ), + migrations.AlterField( + model_name="protein", + name="seq", + field=proteins.models.protein.SequenceField( + blank=True, + help_text="Amino acid sequence (IPG ID is preferred)", + max_length=1024, + null=True, + unique=True, + validators=[proteins.validators.protein_sequence_validator], + verbose_name="Sequence", + ), + ), + ] diff --git a/backend/proteins/migrations/0021_auto_20180804_0203.py b/backend/proteins/migrations_old/0021_auto_20180804_0203.py similarity index 52% rename from backend/proteins/migrations/0021_auto_20180804_0203.py rename to backend/proteins/migrations_old/0021_auto_20180804_0203.py index efe7910f9..0679f4b18 100644 --- a/backend/proteins/migrations/0021_auto_20180804_0203.py +++ b/backend/proteins/migrations_old/0021_auto_20180804_0203.py @@ -4,41 +4,40 @@ class Migration(migrations.Migration): - dependencies = [ - ('proteins', '0020_auto_20180729_0234'), + ("proteins", "0020_auto_20180729_0234"), ] operations = [ migrations.AlterUniqueTogether( - name='fretpair', + name="fretpair", unique_together=set(), ), migrations.RemoveField( - model_name='fretpair', - name='acceptor', + model_name="fretpair", + name="acceptor", ), migrations.RemoveField( - model_name='fretpair', - name='created_by', + model_name="fretpair", + name="created_by", ), migrations.RemoveField( - model_name='fretpair', - name='donor', + model_name="fretpair", + name="donor", ), migrations.RemoveField( - model_name='fretpair', - name='pair_references', + model_name="fretpair", + name="pair_references", ), migrations.RemoveField( - model_name='fretpair', - name='updated_by', + model_name="fretpair", + name="updated_by", ), migrations.RemoveField( - model_name='protein', - name='FRET_partner', + model_name="protein", + name="FRET_partner", ), migrations.DeleteModel( - name='FRETpair', + name="FRETpair", ), ] diff --git a/backend/proteins/migrations_old/0022_osermeasurement.py b/backend/proteins/migrations_old/0022_osermeasurement.py new file mode 100644 index 000000000..40a6de31f --- /dev/null +++ b/backend/proteins/migrations_old/0022_osermeasurement.py @@ -0,0 +1,159 @@ +# Generated by Django 2.0.9 on 2018-10-08 18:11 + +import django.core.validators +import django.db.models.deletion +import django.utils.timezone +import model_utils.fields +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("references", "0003_auto_20180804_0203"), + ("proteins", "0021_auto_20180804_0203"), + ] + + operations = [ + migrations.CreateModel( + name="OSERMeasurement", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), + ( + "percent", + models.FloatField( + blank=True, + help_text="Photobleaching half-life (s)", + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(100), + ], + verbose_name="Percent Normal Cells", + ), + ), + ( + "percent_stddev", + models.FloatField( + blank=True, + help_text="Standard deviation of percent normal cells (if applicable)", + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(100), + ], + verbose_name="StdDev", + ), + ), + ( + "percent_ncells", + models.IntegerField( + blank=True, + help_text="Number of cells analyzed in percent normal for this FP", + null=True, + verbose_name="Number of cells for percent measurement", + ), + ), + ( + "oserne", + models.FloatField( + blank=True, + help_text="Ratio of OSER to nuclear envelope (NE) fluorescence intensities", + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(100), + ], + verbose_name="OSER/NE ratio", + ), + ), + ( + "oserne_stddev", + models.FloatField( + blank=True, + help_text="Standard deviation of OSER/NE ratio (if applicable)", + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(100), + ], + verbose_name="OSER/NE StdDev", + ), + ), + ( + "oserne_ncells", + models.IntegerField( + blank=True, + help_text="Number of cells analyzed in OSER/NE this FP", + null=True, + verbose_name="Number of cells for OSER/NE measurement", + ), + ), + ( + "celltype", + models.CharField( + blank=True, help_text="e.g. COS-7, HeLa", max_length=64, verbose_name="Cell Type" + ), + ), + ("temp", models.FloatField(blank=True, null=True, verbose_name="Temperature")), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="osermeasurement_author", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "protein", + models.ForeignKey( + help_text="The protein on which this measurement was made", + on_delete=django.db.models.deletion.CASCADE, + related_name="oser_measurements", + to="proteins.Protein", + verbose_name="Protein", + ), + ), + ( + "reference", + models.ForeignKey( + blank=True, + help_text="Reference where the measurement was made", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="oser_measurements", + to="references.Reference", + verbose_name="Measurement Reference", + ), + ), + ( + "updated_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="osermeasurement_modifier", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/backend/proteins/migrations_old/0023_spectrum_reference.py b/backend/proteins/migrations_old/0023_spectrum_reference.py new file mode 100644 index 000000000..730b7926d --- /dev/null +++ b/backend/proteins/migrations_old/0023_spectrum_reference.py @@ -0,0 +1,25 @@ +# Generated by Django 2.0.9 on 2018-10-09 19:58 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("references", "0003_auto_20180804_0203"), + ("proteins", "0022_osermeasurement"), + ] + + operations = [ + migrations.AddField( + model_name="spectrum", + name="reference", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="spectra", + to="references.Reference", + ), + ), + ] diff --git a/backend/proteins/migrations/0024_auto_20181011_1659.py b/backend/proteins/migrations_old/0024_auto_20181011_1659.py similarity index 50% rename from backend/proteins/migrations/0024_auto_20181011_1659.py rename to backend/proteins/migrations_old/0024_auto_20181011_1659.py index da868bf20..4efbc378f 100644 --- a/backend/proteins/migrations/0024_auto_20181011_1659.py +++ b/backend/proteins/migrations_old/0024_auto_20181011_1659.py @@ -4,20 +4,19 @@ class Migration(migrations.Migration): - dependencies = [ - ('proteins', '0023_spectrum_reference'), + ("proteins", "0023_spectrum_reference"), ] operations = [ migrations.AlterField( - model_name='bleachmeasurement', - name='temp', - field=models.FloatField(blank=True, null=True, verbose_name='Temp (˚C)'), + model_name="bleachmeasurement", + name="temp", + field=models.FloatField(blank=True, null=True, verbose_name="Temp (˚C)"), ), migrations.AlterField( - model_name='bleachmeasurement', - name='units', - field=models.CharField(blank=True, help_text='e.g. W/cm2', max_length=100, verbose_name='Power Units'), + model_name="bleachmeasurement", + name="units", + field=models.CharField(blank=True, help_text="e.g. W/cm2", max_length=100, verbose_name="Power Units"), ), ] diff --git a/backend/proteins/migrations_old/0025_auto_20181011_1715.py b/backend/proteins/migrations_old/0025_auto_20181011_1715.py new file mode 100644 index 000000000..c383d19fe --- /dev/null +++ b/backend/proteins/migrations_old/0025_auto_20181011_1715.py @@ -0,0 +1,41 @@ +# Generated by Django 2.0.9 on 2018-10-11 17:15 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0024_auto_20181011_1659"), + ] + + operations = [ + migrations.AddField( + model_name="bleachmeasurement", + name="bandcenter", + field=models.PositiveSmallIntegerField( + blank=True, + help_text="Band center of excitation light filter", + null=True, + validators=[ + django.core.validators.MinValueValidator(200), + django.core.validators.MaxValueValidator(1600), + ], + verbose_name="Band Center (nm)", + ), + ), + migrations.AddField( + model_name="bleachmeasurement", + name="bandwidth", + field=models.PositiveSmallIntegerField( + blank=True, + help_text="Bandwidth of excitation light filter", + null=True, + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(1000), + ], + verbose_name="Bandwidth (nm)", + ), + ), + ] diff --git a/backend/proteins/migrations_old/0026_bleachmeasurement_cell_type.py b/backend/proteins/migrations_old/0026_bleachmeasurement_cell_type.py new file mode 100644 index 000000000..b599fb69c --- /dev/null +++ b/backend/proteins/migrations_old/0026_bleachmeasurement_cell_type.py @@ -0,0 +1,17 @@ +# Generated by Django 2.0.9 on 2018-10-11 17:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0025_auto_20181011_1715"), + ] + + operations = [ + migrations.AddField( + model_name="bleachmeasurement", + name="cell_type", + field=models.CharField(blank=True, help_text="e.g. HeLa", max_length=60, verbose_name="Cell Type"), + ), + ] diff --git a/backend/proteins/migrations/0027_auto_20181011_1754.py b/backend/proteins/migrations_old/0027_auto_20181011_1754.py similarity index 52% rename from backend/proteins/migrations/0027_auto_20181011_1754.py rename to backend/proteins/migrations_old/0027_auto_20181011_1754.py index 31062389f..83edc1016 100644 --- a/backend/proteins/migrations/0027_auto_20181011_1754.py +++ b/backend/proteins/migrations_old/0027_auto_20181011_1754.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('references', '0003_auto_20180804_0203'), - ('proteins', '0026_bleachmeasurement_cell_type'), + ("references", "0003_auto_20180804_0203"), + ("proteins", "0026_bleachmeasurement_cell_type"), ] operations = [ migrations.AlterUniqueTogether( - name='osermeasurement', - unique_together={('protein', 'reference')}, + name="osermeasurement", + unique_together={("protein", "reference")}, ), ] diff --git a/backend/proteins/migrations_old/0028_auto_20181012_2011.py b/backend/proteins/migrations_old/0028_auto_20181012_2011.py new file mode 100644 index 000000000..822bc1e42 --- /dev/null +++ b/backend/proteins/migrations_old/0028_auto_20181012_2011.py @@ -0,0 +1,100 @@ +# Generated by Django 2.0.9 on 2018-10-12 20:11 + +import django.db.models.deletion +import django.utils.timezone +import model_utils.fields +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("references", "0003_auto_20180804_0203"), + ("proteins", "0027_auto_20181011_1754"), + ] + + operations = [ + migrations.CreateModel( + name="Excerpt", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), + ( + "status", + model_utils.fields.StatusField( + choices=[("approved", "approved"), ("flagged", "flagged"), ("rejected", "rejected")], + default="approved", + max_length=100, + no_check_for_status=True, + verbose_name="status", + ), + ), + ( + "status_changed", + model_utils.fields.MonitorField( + default=django.utils.timezone.now, monitor="status", verbose_name="status changed" + ), + ), + ("content", models.TextField(help_text="Brief excerpt describing this protein", max_length=1024)), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="excerpt_author", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.AlterField( + model_name="protein", + name="blurb", + field=models.TextField(blank=True, help_text="Brief descriptive blurb", max_length=512), + ), + migrations.AddField( + model_name="excerpt", + name="protein", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, related_name="excerpts", to="proteins.Protein" + ), + ), + migrations.AddField( + model_name="excerpt", + name="reference", + field=models.ForeignKey( + help_text="Source of this excerpt", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="excerpt", + to="references.Reference", + ), + ), + migrations.AddField( + model_name="excerpt", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="excerpt_modifier", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/backend/proteins/migrations_old/0029_auto_20181014_1241.py b/backend/proteins/migrations_old/0029_auto_20181014_1241.py new file mode 100644 index 000000000..b167b1966 --- /dev/null +++ b/backend/proteins/migrations_old/0029_auto_20181014_1241.py @@ -0,0 +1,23 @@ +# Generated by Django 2.0.9 on 2018-10-14 12:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0028_auto_20181012_2011"), + ] + + operations = [ + migrations.AlterModelOptions( + name="excerpt", + options={"ordering": ["reference__year", "created"]}, + ), + migrations.AddField( + model_name="protein", + name="seq_comment", + field=models.CharField( + blank=True, help_text="if necessary, comment on source of sequence", max_length=512 + ), + ), + ] diff --git a/backend/proteins/migrations_old/0030_lineage.py b/backend/proteins/migrations_old/0030_lineage.py new file mode 100644 index 000000000..be475d816 --- /dev/null +++ b/backend/proteins/migrations_old/0030_lineage.py @@ -0,0 +1,69 @@ +# Generated by Django 2.1.2 on 2018-10-28 22:20 + +import django.db.models.deletion +import django.utils.timezone +import model_utils.fields +import mptt.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("references", "0004_auto_20181026_1547"), + ("proteins", "0029_auto_20181014_1241"), + ] + + operations = [ + migrations.CreateModel( + name="Lineage", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), + ("mutation", models.CharField(blank=True, max_length=400)), + ("lft", models.PositiveIntegerField(db_index=True, editable=False)), + ("rght", models.PositiveIntegerField(db_index=True, editable=False)), + ("tree_id", models.PositiveIntegerField(db_index=True, editable=False)), + ("level", models.PositiveIntegerField(db_index=True, editable=False)), + ( + "parent", + mptt.fields.TreeForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="children", + to="proteins.Lineage", + ), + ), + ( + "protein", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, related_name="lineage", to="proteins.Protein" + ), + ), + ( + "reference", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="lineages", + to="references.Reference", + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/backend/proteins/migrations/0031_auto_20181103_1531.py b/backend/proteins/migrations_old/0031_auto_20181103_1531.py similarity index 52% rename from backend/proteins/migrations/0031_auto_20181103_1531.py rename to backend/proteins/migrations_old/0031_auto_20181103_1531.py index 77707cc93..3a16e9640 100644 --- a/backend/proteins/migrations/0031_auto_20181103_1531.py +++ b/backend/proteins/migrations_old/0031_auto_20181103_1531.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('proteins', '0030_lineage'), + ("proteins", "0030_lineage"), ] operations = [ migrations.AlterField( - model_name='excerpt', - name='content', - field=models.TextField(help_text='Brief excerpt describing this protein', max_length=1200), + model_name="excerpt", + name="content", + field=models.TextField(help_text="Brief excerpt describing this protein", max_length=1200), ), ] diff --git a/backend/proteins/migrations/0032_auto_20181107_2015.py b/backend/proteins/migrations_old/0032_auto_20181107_2015.py similarity index 52% rename from backend/proteins/migrations/0032_auto_20181107_2015.py rename to backend/proteins/migrations_old/0032_auto_20181107_2015.py index 4d53aae1f..ccf7e3e3f 100644 --- a/backend/proteins/migrations/0032_auto_20181107_2015.py +++ b/backend/proteins/migrations_old/0032_auto_20181107_2015.py @@ -2,25 +2,29 @@ import django.contrib.postgres.fields from django.db import migrations, models + import proteins.models.lineage import proteins.validators class Migration(migrations.Migration): - dependencies = [ - ('proteins', '0031_auto_20181103_1531'), + ("proteins", "0031_auto_20181103_1531"), ] operations = [ migrations.AlterField( - model_name='lineage', - name='mutation', + model_name="lineage", + name="mutation", field=proteins.models.lineage.MutationSetField(blank=True, max_length=400), ), migrations.AlterField( - model_name='mutation', - name='mutations', - field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=5), size=None, validators=[proteins.validators.validate_mutation]), + model_name="mutation", + name="mutations", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=5), + size=None, + validators=[proteins.validators.validate_mutation], + ), ), ] diff --git a/backend/proteins/migrations_old/0033_auto_20181107_2119.py b/backend/proteins/migrations_old/0033_auto_20181107_2119.py new file mode 100644 index 000000000..9405ab3f8 --- /dev/null +++ b/backend/proteins/migrations_old/0033_auto_20181107_2119.py @@ -0,0 +1,37 @@ +# Generated by Django 2.1.2 on 2018-11-07 21:19 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("proteins", "0032_auto_20181107_2015"), + ] + + operations = [ + migrations.AddField( + model_name="lineage", + name="created_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="lineage_author", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="lineage", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="lineage_modifier", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/backend/proteins/migrations/0034_lineage_rootmut.py b/backend/proteins/migrations_old/0034_lineage_rootmut.py similarity index 72% rename from backend/proteins/migrations/0034_lineage_rootmut.py rename to backend/proteins/migrations_old/0034_lineage_rootmut.py index 15202887f..101c120bc 100644 --- a/backend/proteins/migrations/0034_lineage_rootmut.py +++ b/backend/proteins/migrations_old/0034_lineage_rootmut.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('proteins', '0033_auto_20181107_2119'), + ("proteins", "0033_auto_20181107_2119"), ] operations = [ migrations.AddField( - model_name='lineage', - name='rootmut', + model_name="lineage", + name="rootmut", field=models.CharField(blank=True, max_length=400), ), ] diff --git a/backend/proteins/migrations/0035_auto_20181110_0103.py b/backend/proteins/migrations_old/0035_auto_20181110_0103.py similarity index 65% rename from backend/proteins/migrations/0035_auto_20181110_0103.py rename to backend/proteins/migrations_old/0035_auto_20181110_0103.py index 2c3421e68..d9c8c7dfb 100644 --- a/backend/proteins/migrations/0035_auto_20181110_0103.py +++ b/backend/proteins/migrations_old/0035_auto_20181110_0103.py @@ -4,17 +4,16 @@ class Migration(migrations.Migration): - dependencies = [ - ('proteins', '0034_lineage_rootmut'), + ("proteins", "0034_lineage_rootmut"), ] operations = [ migrations.RemoveField( - model_name='mutation', - name='parent', + model_name="mutation", + name="parent", ), migrations.DeleteModel( - name='Mutation', + name="Mutation", ), ] diff --git a/backend/proteins/migrations_old/0036_lineage_root_node.py b/backend/proteins/migrations_old/0036_lineage_root_node.py new file mode 100644 index 000000000..e097b0536 --- /dev/null +++ b/backend/proteins/migrations_old/0036_lineage_root_node.py @@ -0,0 +1,24 @@ +# Generated by Django 2.1.2 on 2018-12-02 15:58 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0035_auto_20181110_0103"), + ] + + operations = [ + migrations.AddField( + model_name="lineage", + name="root_node", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="descendants", + to="proteins.Lineage", + verbose_name="Root Node", + ), + ), + ] diff --git a/backend/proteins/migrations_old/0037_auto_20181205_2035.py b/backend/proteins/migrations_old/0037_auto_20181205_2035.py new file mode 100644 index 000000000..b6aece3df --- /dev/null +++ b/backend/proteins/migrations_old/0037_auto_20181205_2035.py @@ -0,0 +1,22 @@ +# Generated by Django 2.1.2 on 2018-12-05 20:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0036_lineage_root_node"), + ] + + operations = [ + migrations.AlterField( + model_name="protein", + name="cofactor", + field=models.CharField( + blank=True, + choices=[("bv", "Biliverdin"), ("fl", "Flavin"), ("pcb", "Phycocyanobilin")], + help_text="Required for fluorescence", + max_length=2, + ), + ), + ] diff --git a/backend/proteins/migrations_old/0038_auto_20181205_2044.py b/backend/proteins/migrations_old/0038_auto_20181205_2044.py new file mode 100644 index 000000000..87a91caa4 --- /dev/null +++ b/backend/proteins/migrations_old/0038_auto_20181205_2044.py @@ -0,0 +1,22 @@ +# Generated by Django 2.1.2 on 2018-12-05 20:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0037_auto_20181205_2035"), + ] + + operations = [ + migrations.AlterField( + model_name="protein", + name="cofactor", + field=models.CharField( + blank=True, + choices=[("bv", "Biliverdin"), ("fl", "Flavin"), ("pc", "Phycocyanobilin")], + help_text="Required for fluorescence", + max_length=2, + ), + ), + ] diff --git a/backend/proteins/migrations/0039_auto_20181206_0009.py b/backend/proteins/migrations_old/0039_auto_20181206_0009.py similarity index 68% rename from backend/proteins/migrations/0039_auto_20181206_0009.py rename to backend/proteins/migrations_old/0039_auto_20181206_0009.py index 6ff3f646a..8bac7ee42 100644 --- a/backend/proteins/migrations/0039_auto_20181206_0009.py +++ b/backend/proteins/migrations_old/0039_auto_20181206_0009.py @@ -4,30 +4,29 @@ class Migration(migrations.Migration): - dependencies = [ - ('proteins', '0038_auto_20181205_2044'), + ("proteins", "0038_auto_20181205_2044"), ] operations = [ migrations.AddField( - model_name='dye', - name='emhex', + model_name="dye", + name="emhex", field=models.CharField(blank=True, max_length=7), ), migrations.AddField( - model_name='dye', - name='exhex', + model_name="dye", + name="exhex", field=models.CharField(blank=True, max_length=7), ), migrations.AddField( - model_name='state', - name='emhex', + model_name="state", + name="emhex", field=models.CharField(blank=True, max_length=7), ), migrations.AddField( - model_name='state', - name='exhex', + model_name="state", + name="exhex", field=models.CharField(blank=True, max_length=7), ), ] diff --git a/backend/proteins/migrations_old/0040_auto_20181210_0345.py b/backend/proteins/migrations_old/0040_auto_20181210_0345.py new file mode 100644 index 000000000..06368193a --- /dev/null +++ b/backend/proteins/migrations_old/0040_auto_20181210_0345.py @@ -0,0 +1,22 @@ +# Generated by Django 2.1.2 on 2018-12-10 03:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0039_auto_20181206_0009"), + ] + + operations = [ + migrations.AlterField( + model_name="protein", + name="cofactor", + field=models.CharField( + blank=True, + choices=[("br", "Bilirubin"), ("bv", "Biliverdin"), ("fl", "Flavin"), ("pc", "Phycocyanobilin")], + help_text="Required for fluorescence", + max_length=2, + ), + ), + ] diff --git a/backend/proteins/migrations_old/0041_auto_20181216_1743.py b/backend/proteins/migrations_old/0041_auto_20181216_1743.py new file mode 100644 index 000000000..3dda36783 --- /dev/null +++ b/backend/proteins/migrations_old/0041_auto_20181216_1743.py @@ -0,0 +1,36 @@ +# Generated by Django 2.1.2 on 2018-12-16 17:43 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0040_auto_20181210_0345"), + ] + + operations = [ + migrations.AddField( + model_name="excerpt", + name="proteins", + field=models.ManyToManyField(related_name="excerpts", to="proteins.Protein"), + ), + migrations.AlterField( + model_name="excerpt", + name="protein", + field=models.ForeignKey( + null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="excerpt", to="proteins.Protein" + ), + ), + migrations.AlterField( + model_name="excerpt", + name="reference", + field=models.ForeignKey( + help_text="Source of this excerpt", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="excerpts", + to="references.Reference", + ), + ), + ] diff --git a/backend/proteins/migrations/0042_auto_20181216_1744.py b/backend/proteins/migrations_old/0042_auto_20181216_1744.py similarity index 64% rename from backend/proteins/migrations/0042_auto_20181216_1744.py rename to backend/proteins/migrations_old/0042_auto_20181216_1744.py index 44df33ea2..f76559150 100644 --- a/backend/proteins/migrations/0042_auto_20181216_1744.py +++ b/backend/proteins/migrations_old/0042_auto_20181216_1744.py @@ -5,19 +5,18 @@ def make_many_exerpts(apps, schema_editor): """ - Adds the Author object in Book.author to the - many-to-many relationship in Book.authors + Adds the Author object in Book.author to the + many-to-many relationship in Book.authors """ - Excerpt = apps.get_model('proteins', 'Excerpt') + Excerpt = apps.get_model("proteins", "Excerpt") for excerpt in Excerpt.objects.all(): excerpt.proteins.add(excerpt.protein) class Migration(migrations.Migration): - dependencies = [ - ('proteins', '0041_auto_20181216_1743'), + ("proteins", "0041_auto_20181216_1743"), ] operations = [ diff --git a/backend/proteins/migrations/0043_remove_excerpt_protein.py b/backend/proteins/migrations_old/0043_remove_excerpt_protein.py similarity index 66% rename from backend/proteins/migrations/0043_remove_excerpt_protein.py rename to backend/proteins/migrations_old/0043_remove_excerpt_protein.py index 527f7ca0b..2c234950e 100644 --- a/backend/proteins/migrations/0043_remove_excerpt_protein.py +++ b/backend/proteins/migrations_old/0043_remove_excerpt_protein.py @@ -4,14 +4,13 @@ class Migration(migrations.Migration): - dependencies = [ - ('proteins', '0042_auto_20181216_1744'), + ("proteins", "0042_auto_20181216_1744"), ] operations = [ migrations.RemoveField( - model_name='excerpt', - name='protein', + model_name="excerpt", + name="protein", ), ] diff --git a/backend/proteins/migrations/0044_auto_20181218_1310.py b/backend/proteins/migrations_old/0044_auto_20181218_1310.py similarity index 65% rename from backend/proteins/migrations/0044_auto_20181218_1310.py rename to backend/proteins/migrations_old/0044_auto_20181218_1310.py index 91a731b57..5d2ccc026 100644 --- a/backend/proteins/migrations/0044_auto_20181218_1310.py +++ b/backend/proteins/migrations_old/0044_auto_20181218_1310.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('proteins', '0043_remove_excerpt_protein'), + ("proteins", "0043_remove_excerpt_protein"), ] operations = [ migrations.AlterField( - model_name='excerpt', - name='proteins', - field=models.ManyToManyField(blank=True, related_name='excerpts', to='proteins.Protein'), + model_name="excerpt", + name="proteins", + field=models.ManyToManyField(blank=True, related_name="excerpts", to="proteins.Protein"), ), ] diff --git a/backend/proteins/migrations_old/0045_dye_is_dark.py b/backend/proteins/migrations_old/0045_dye_is_dark.py new file mode 100644 index 000000000..f5f5f069a --- /dev/null +++ b/backend/proteins/migrations_old/0045_dye_is_dark.py @@ -0,0 +1,19 @@ +# Generated by Django 2.1.2 on 2018-12-29 18:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0044_auto_20181218_1310"), + ] + + operations = [ + migrations.AddField( + model_name="dye", + name="is_dark", + field=models.BooleanField( + default=False, help_text="This state does not fluorescence", verbose_name="Dark State" + ), + ), + ] diff --git a/backend/proteins/migrations_old/0046_auto_20190121_1341.py b/backend/proteins/migrations_old/0046_auto_20190121_1341.py new file mode 100644 index 000000000..12d8af8cb --- /dev/null +++ b/backend/proteins/migrations_old/0046_auto_20190121_1341.py @@ -0,0 +1,44 @@ +# Generated by Django 2.1.2 on 2019-01-21 13:41 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0045_dye_is_dark"), + ] + + operations = [ + migrations.AlterField( + model_name="protein", + name="pdb", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=4), + blank=True, + null=True, + size=None, + verbose_name="Protein DataBank IDs", + ), + ), + migrations.AlterField( + model_name="protein", + name="switch_type", + field=models.CharField( + blank=True, + choices=[ + ("b", "Basic"), + ("pa", "Photoactivatable"), + ("ps", "Photoswitchable"), + ("pc", "Photoconvertible"), + ("mp", "Multi-photochromic"), + ("o", "Multistate"), + ("t", "Timer"), + ], + default="b", + help_text="Photoswitching type (basic if none)", + max_length=2, + verbose_name="Switching Type", + ), + ), + ] diff --git a/backend/proteins/migrations_old/0047_auto_20190319_1525.py b/backend/proteins/migrations_old/0047_auto_20190319_1525.py new file mode 100644 index 000000000..7f75074a8 --- /dev/null +++ b/backend/proteins/migrations_old/0047_auto_20190319_1525.py @@ -0,0 +1,91 @@ +# Generated by Django 2.1.7 on 2019-03-19 15:25 + +import django.core.validators +import django.db.models.deletion +import django.utils.timezone +import model_utils.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("contenttypes", "0002_remove_content_type_name"), + ("proteins", "0046_auto_20190121_1341"), + ] + + operations = [ + migrations.CreateModel( + name="OcFluorEff", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), + ("object_id", models.PositiveIntegerField()), + ("fluor_name", models.CharField(blank=True, max_length=100)), + ( + "ex_eff", + models.FloatField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(1), + ], + verbose_name="Excitation Efficiency", + ), + ), + ( + "ex_eff_broad", + models.FloatField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(1), + ], + verbose_name="Excitation Efficiency (Broadband)", + ), + ), + ( + "em_eff", + models.FloatField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(1), + ], + verbose_name="Emission Efficiency", + ), + ), + ("brightness", models.FloatField(blank=True, null=True)), + ( + "content_type", + models.ForeignKey( + limit_choices_to=models.Q( + models.Q(("app_label", "proteins"), ("model", "state")), + models.Q(("app_label", "proteins"), ("model", "dye")), + _connector="OR", + ), + on_delete=django.db.models.deletion.CASCADE, + to="contenttypes.ContentType", + ), + ), + ("oc", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="proteins.OpticalConfig")), + ], + ), + migrations.AlterUniqueTogether( + name="ocfluoreff", + unique_together={("oc", "content_type", "object_id")}, + ), + ] diff --git a/backend/proteins/migrations/0048_change_protein_uuid.py b/backend/proteins/migrations_old/0048_change_protein_uuid.py similarity index 80% rename from backend/proteins/migrations/0048_change_protein_uuid.py rename to backend/proteins/migrations_old/0048_change_protein_uuid.py index 7e754f0a6..7a4eabc8e 100644 --- a/backend/proteins/migrations/0048_change_protein_uuid.py +++ b/backend/proteins/migrations_old/0048_change_protein_uuid.py @@ -1,8 +1,10 @@ # Generated by Django 2.1.7 on 2019-03-22 00:59 +import uuid as uuid_lib + from django.db import migrations, models + from proteins.models.protein import prot_uuid -import uuid as uuid_lib def forwards_func(apps, schema_editor): @@ -24,31 +26,30 @@ def stub(a, b): class Migration(migrations.Migration): - dependencies = [ - ('proteins', '0047_auto_20190319_1525'), + ("proteins", "0047_auto_20190319_1525"), ] operations = [ migrations.AlterField( - model_name='protein', - name='uuid', + model_name="protein", + name="uuid", field=models.UUIDField(blank=True, null=True), ), migrations.RunPython(stub, reverse_func), migrations.RemoveField( - model_name='protein', - name='uuid', + model_name="protein", + name="uuid", ), migrations.AddField( - model_name='protein', - name='uuid', + model_name="protein", + name="uuid", field=models.CharField(blank=True, max_length=50, null=True), ), migrations.RunPython(forwards_func, stub), migrations.AlterField( - model_name='protein', - name='uuid', + model_name="protein", + name="uuid", field=models.CharField(db_index=True, default=prot_uuid, editable=False, max_length=5, unique=True), ), ] diff --git a/backend/proteins/migrations_old/0049_auto_20190323_1947.py b/backend/proteins/migrations_old/0049_auto_20190323_1947.py new file mode 100644 index 000000000..15531128e --- /dev/null +++ b/backend/proteins/migrations_old/0049_auto_20190323_1947.py @@ -0,0 +1,26 @@ +# Generated by Django 2.1.7 on 2019-03-23 19:47 + +from django.db import migrations, models + +import proteins.models.protein + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0048_change_protein_uuid"), + ] + + operations = [ + migrations.AlterField( + model_name="protein", + name="uuid", + field=models.CharField( + db_index=True, + default=proteins.models.protein.prot_uuid, + editable=False, + max_length=5, + unique=True, + verbose_name="FPbase ID", + ), + ), + ] diff --git a/backend/proteins/migrations/0050_auto_20190714_1318.py b/backend/proteins/migrations_old/0050_auto_20190714_1318.py similarity index 93% rename from backend/proteins/migrations/0050_auto_20190714_1318.py rename to backend/proteins/migrations_old/0050_auto_20190714_1318.py index 93b89aef5..873cb9da7 100644 --- a/backend/proteins/migrations/0050_auto_20190714_1318.py +++ b/backend/proteins/migrations_old/0050_auto_20190714_1318.py @@ -5,16 +5,13 @@ class Migration(migrations.Migration): - dependencies = [("proteins", "0049_auto_20190323_1947")] operations = [ migrations.AddField( model_name="spectrum", name="source", - field=models.CharField( - blank=True, help_text="Source of the spectra data", max_length=128 - ), + field=models.CharField(blank=True, help_text="Source of the spectra data", max_length=128), ), migrations.AlterField( model_name="lineage", diff --git a/backend/proteins/migrations/0051_alter_bleachmeasurement_created_by_and_more.py b/backend/proteins/migrations_old/0051_alter_bleachmeasurement_created_by_and_more.py similarity index 85% rename from backend/proteins/migrations/0051_alter_bleachmeasurement_created_by_and_more.py rename to backend/proteins/migrations_old/0051_alter_bleachmeasurement_created_by_and_more.py index ed16f1246..412d28b01 100644 --- a/backend/proteins/migrations/0051_alter_bleachmeasurement_created_by_and_more.py +++ b/backend/proteins/migrations_old/0051_alter_bleachmeasurement_created_by_and_more.py @@ -1,8 +1,8 @@ # Generated by Django 4.2 on 2023-05-14 10:45 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): @@ -26,9 +26,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="bleachmeasurement", name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), ), migrations.AlterField( model_name="bleachmeasurement", @@ -55,9 +53,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="camera", name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), ), migrations.AlterField( model_name="camera", @@ -84,9 +80,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="dye", name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), ), migrations.AlterField( model_name="dye", @@ -113,9 +107,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="excerpt", name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), ), migrations.AlterField( model_name="excerpt", @@ -142,9 +134,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="filter", name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), ), migrations.AlterField( model_name="filter", @@ -160,9 +150,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="filterplacement", name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), ), migrations.AlterField( model_name="filterplacement", @@ -180,9 +168,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="filterplacement", name="reflects", - field=models.BooleanField( - default=False, help_text="Filter reflects emission (if BS or EM filter)" - ), + field=models.BooleanField(default=False, help_text="Filter reflects emission (if BS or EM filter)"), ), migrations.AlterField( model_name="light", @@ -198,9 +184,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="light", name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), ), migrations.AlterField( model_name="light", @@ -227,9 +211,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="lineage", name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), ), migrations.AlterField( model_name="lineage", @@ -256,16 +238,12 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="ocfluoreff", name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), ), migrations.AlterField( model_name="opticalconfig", name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), ), migrations.AlterField( model_name="opticalconfig", @@ -314,9 +292,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="osermeasurement", name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), ), migrations.AlterField( model_name="osermeasurement", @@ -359,9 +335,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="protein", name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), ), migrations.AlterField( model_name="protein", @@ -377,9 +351,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="proteincollection", name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), ), migrations.AlterField( model_name="proteincollection", @@ -406,9 +378,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="spectrum", name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), ), migrations.AlterField( model_name="spectrum", @@ -435,9 +405,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="state", name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), ), migrations.AlterField( model_name="state", @@ -464,9 +432,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="statetransition", name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), ), migrations.AlterField( model_name="statetransition", diff --git a/backend/proteins/migrations/0052_alter_protein_chromophore.py b/backend/proteins/migrations_old/0052_alter_protein_chromophore.py similarity index 100% rename from backend/proteins/migrations/0052_alter_protein_chromophore.py rename to backend/proteins/migrations_old/0052_alter_protein_chromophore.py diff --git a/backend/proteins/migrations/0053_alter_protein_chromophore.py b/backend/proteins/migrations_old/0053_alter_protein_chromophore.py similarity index 99% rename from backend/proteins/migrations/0053_alter_protein_chromophore.py rename to backend/proteins/migrations_old/0053_alter_protein_chromophore.py index 0daaffc98..47df7f938 100644 --- a/backend/proteins/migrations/0053_alter_protein_chromophore.py +++ b/backend/proteins/migrations_old/0053_alter_protein_chromophore.py @@ -1,6 +1,7 @@ # Generated by Django 4.2.1 on 2023-06-11 16:00 from django.db import migrations + import proteins.models.protein diff --git a/backend/proteins/migrations/0054_microscope_cfg_calc_efficiency_and_more.py b/backend/proteins/migrations_old/0054_microscope_cfg_calc_efficiency_and_more.py similarity index 100% rename from backend/proteins/migrations/0054_microscope_cfg_calc_efficiency_and_more.py rename to backend/proteins/migrations_old/0054_microscope_cfg_calc_efficiency_and_more.py diff --git a/backend/proteins/migrations/0055_spectrum_status_spectrum_status_changed.py b/backend/proteins/migrations_old/0055_spectrum_status_spectrum_status_changed.py similarity index 99% rename from backend/proteins/migrations/0055_spectrum_status_spectrum_status_changed.py rename to backend/proteins/migrations_old/0055_spectrum_status_spectrum_status_changed.py index 3eb4015a6..7d88a863e 100644 --- a/backend/proteins/migrations/0055_spectrum_status_spectrum_status_changed.py +++ b/backend/proteins/migrations_old/0055_spectrum_status_spectrum_status_changed.py @@ -1,12 +1,11 @@ # Generated by Django 4.2.1 on 2024-10-01 16:26 -from django.db import migrations import django.utils.timezone import model_utils.fields +from django.db import migrations class Migration(migrations.Migration): - dependencies = [ ("proteins", "0054_microscope_cfg_calc_efficiency_and_more"), ] diff --git a/backend/proteins/migrations_old/0056_spectrum_spectrum_state_status_idx_and_more.py b/backend/proteins/migrations_old/0056_spectrum_spectrum_state_status_idx_and_more.py new file mode 100644 index 000000000..e089af41c --- /dev/null +++ b/backend/proteins/migrations_old/0056_spectrum_spectrum_state_status_idx_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.7 on 2025-10-22 14:06 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0055_spectrum_status_spectrum_status_changed"), + ("references", "0008_alter_reference_year"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddIndex( + model_name="spectrum", + index=models.Index(fields=["owner_state_id", "status"], name="spectrum_state_status_idx"), + ), + migrations.AddIndex( + model_name="spectrum", + index=models.Index(fields=["status"], name="spectrum_status_idx"), + ), + ] diff --git a/backend/proteins/migrations/0057_add_status_index.py b/backend/proteins/migrations_old/0057_add_status_index.py similarity index 57% rename from backend/proteins/migrations/0057_add_status_index.py rename to backend/proteins/migrations_old/0057_add_status_index.py index aa8ed3d53..b71eb0407 100644 --- a/backend/proteins/migrations/0057_add_status_index.py +++ b/backend/proteins/migrations_old/0057_add_status_index.py @@ -5,16 +5,15 @@ class Migration(migrations.Migration): - dependencies = [ - ('proteins', '0056_spectrum_spectrum_state_status_idx_and_more'), - ('references', '0008_alter_reference_year'), + ("proteins", "0056_spectrum_spectrum_state_status_idx_and_more"), + ("references", "0008_alter_reference_year"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.AddIndex( - model_name='protein', - index=models.Index(fields=['status'], name='protein_status_idx'), + model_name="protein", + index=models.Index(fields=["status"], name="protein_status_idx"), ), ] diff --git a/backend/proteins/migrations_old/0058_snapgeneplasmid_protein_snapgene_plasmids.py b/backend/proteins/migrations_old/0058_snapgeneplasmid_protein_snapgene_plasmids.py new file mode 100644 index 000000000..68d2d0520 --- /dev/null +++ b/backend/proteins/migrations_old/0058_snapgeneplasmid_protein_snapgene_plasmids.py @@ -0,0 +1,39 @@ +# Generated by Django 5.2.7 on 2025-11-15 14:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0057_add_status_index"), + ] + + operations = [ + migrations.CreateModel( + name="SnapGenePlasmid", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("plasmid_id", models.CharField(db_index=True, max_length=100, unique=True)), + ("name", models.CharField(max_length=200)), + ("description", models.TextField(blank=True)), + ("author", models.CharField(blank=True, max_length=200)), + ("size", models.IntegerField(blank=True, help_text="Size in base pairs", null=True)), + ("topology", models.CharField(blank=True, max_length=50)), + ], + options={ + "verbose_name": "SnapGene Plasmid", + "verbose_name_plural": "SnapGene Plasmids", + "ordering": ["name"], + }, + ), + migrations.AddField( + model_name="protein", + name="snapgene_plasmids", + field=models.ManyToManyField( + blank=True, + help_text="Associated SnapGene plasmids", + related_name="proteins", + to="proteins.snapgeneplasmid", + ), + ), + ] From 502a97567a92655609e2f8bfb6cc585f1b518644 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Thu, 20 Nov 2025 23:22:08 -0500 Subject: [PATCH 06/57] big change on Spectrum.owner_X --- backend/proteins/admin.py | 16 +-- backend/proteins/api/serializers.py | 12 +- backend/proteins/factories.py | 24 ++-- backend/proteins/filters.py | 2 +- backend/proteins/forms/spectrum.py | 24 ++-- backend/proteins/migrations/0001_initial.py | 42 ++++-- backend/proteins/models/__init__.py | 8 +- backend/proteins/models/bleach.py | 10 +- backend/proteins/models/collection.py | 13 +- backend/proteins/models/dye.py | 16 ++- backend/proteins/models/efficiency.py | 49 ++++--- backend/proteins/models/excerpt.py | 14 +- .../models/fluorescence_measurement.py | 5 +- backend/proteins/models/fluorophore.py | 21 ++- backend/proteins/models/lineage.py | 7 +- backend/proteins/models/microscope.py | 33 +++-- backend/proteins/models/mixins.py | 6 +- backend/proteins/models/organism.py | 3 +- backend/proteins/models/oser.py | 12 +- backend/proteins/models/protein.py | 16 ++- backend/proteins/models/spectrum.py | 121 +++++++++--------- backend/proteins/models/transition.py | 16 ++- backend/proteins/schema/query.py | 4 +- backend/proteins/schema/types.py | 16 ++- backend/proteins/tasks.py | 6 +- backend/proteins/util/helpers.py | 2 +- backend/proteins/util/maintain.py | 17 +-- backend/proteins/views/ajax.py | 37 +++--- backend/proteins/views/spectra.py | 21 +-- ...{test_schema.py => test_graphql_schema.py} | 10 +- .../tests/test_proteins/test_ajax_views.py | 32 +++-- backend/tests/test_proteins/test_forms.py | 12 +- backend/tests/test_proteins/test_models.py | 4 +- .../tests/test_proteins/test_spectra_views.py | 2 +- backend/tests/test_proteins/test_tasks.py | 2 +- backend/tests/test_proteins/test_views.py | 14 +- 36 files changed, 407 insertions(+), 242 deletions(-) rename backend/tests/test_fpbase/{test_schema.py => test_graphql_schema.py} (94%) diff --git a/backend/proteins/admin.py b/backend/proteins/admin.py index cfa9f10f6..6e2911eca 100644 --- a/backend/proteins/admin.py +++ b/backend/proteins/admin.py @@ -198,37 +198,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__label", "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) diff --git a/backend/proteins/api/serializers.py b/backend/proteins/api/serializers.py index 1da8ecde4..631a098bb 100644 --- a/backend/proteins/api/serializers.py +++ b/backend/proteins/api/serializers.py @@ -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 hasattr(obj.owner_fluor, "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 hasattr(obj.owner_fluor, "protein"): + return obj.owner_fluor.protein.slug + return None class StateTransitionSerializer(serializers.ModelSerializer): diff --git a/backend/proteins/factories.py b/backend/proteins/factories.py index 9187a7efe..88582e243 100644 --- a/backend/proteins/factories.py +++ b/backend/proteins/factories.py @@ -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 @@ -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") @@ -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) @@ -192,19 +194,19 @@ 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", ) diff --git a/backend/proteins/filters.py b/backend/proteins/filters.py index dfd7baf7b..b715182f6 100644 --- a/backend/proteins/filters.py +++ b/backend/proteins/filters.py @@ -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): diff --git a/backend/proteins/forms/spectrum.py b/backend/proteins/forms/spectrum.py index 6f22f4693..52d282f30 100644 --- a/backend/proteins/forms/spectrum.py +++ b/backend/proteins/forms/spectrum.py @@ -28,14 +28,14 @@ def __init__(self, *args, **kwargs): class SpectrumForm(forms.ModelForm): lookup = { - Spectrum.DYE: ("owner_dye", "Dye"), - Spectrum.PROTEIN: ("owner_state", "State"), + Spectrum.DYE: ("owner_fluor", "DyeState"), + Spectrum.PROTEIN: ("owner_fluor", "State"), Spectrum.FILTER: ("owner_filter", "Filter"), Spectrum.CAMERA: ("owner_camera", "Camera"), Spectrum.LIGHT: ("owner_light", "Light"), } - owner_state = forms.ModelChoiceField( + owner_fluor = forms.ModelChoiceField( required=False, label=mark_safe('Protein*'), queryset=State.objects.select_related("protein"), @@ -77,7 +77,7 @@ def __init__(self, *args, **kwargs): self.helper.layout = Layout( Div("category"), Div( - Div("owner_state", css_class="col-sm-6 col-xs-12 protein-owner hidden"), + Div("owner_fluor", css_class="col-sm-6 col-xs-12 protein-owner hidden"), Div("owner", css_class="col-sm-6 col-xs-12 non-protein-owner"), Div("subtype", css_class="col-sm-6 col-xs-12"), css_class="row", @@ -103,7 +103,7 @@ class Meta: "ph", "source", "solvent", - "owner_state", + "owner_fluor", "owner", ) widgets = {"data": forms.Textarea(attrs={"class": "vLargeTextField", "rows": 2})} @@ -166,19 +166,19 @@ def clean_file(self): self.data: dict = self.data.copy() self.data["data"] = self.cleaned_data["data"] - def clean_owner_state(self): - owner_state = self.cleaned_data.get("owner_state") + def clean_owner_fluor(self): + owner_fluor = self.cleaned_data.get("owner_fluor") stype = self.cleaned_data.get("subtype") - if self.cleaned_data.get("category") == Spectrum.PROTEIN: - spectra = Spectrum.objects.all_objects().filter(owner_state=owner_state, subtype=stype) + if self.cleaned_data.get("category") in (Spectrum.PROTEIN, Spectrum.DYE): + spectra = Spectrum.objects.all_objects().filter(owner_fluor=owner_fluor, subtype=stype) if spectra.exists(): first = spectra.first() self.add_error( - "owner_state", + "owner_fluor", forms.ValidationError( "%(owner)s already has a%(n)s %(stype)s spectrum %(status)s", params={ - "owner": owner_state, + "owner": owner_fluor, "stype": first.get_subtype_display().lower(), "n": "n" if stype != Spectrum.TWOP else "", "status": " (pending)" if first.status == Spectrum.STATUS.pending else "", @@ -186,7 +186,7 @@ def clean_owner_state(self): code="owner_exists", ), ) - return owner_state + return owner_fluor def clean_owner(self): # make sure an owner with the same category and name doesn't already exist diff --git a/backend/proteins/migrations/0001_initial.py b/backend/proteins/migrations/0001_initial.py index 11d20f86a..304f1b81c 100644 --- a/backend/proteins/migrations/0001_initial.py +++ b/backend/proteins/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.8 on 2025-11-21 01:23 +# Generated by Django 5.2.8 on 2025-11-21 02:22 import django.contrib.postgres.fields import django.core.validators @@ -246,6 +246,37 @@ class Migration(migrations.Migration): ('filter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='proteins.filter')), ], ), + migrations.CreateModel( + name='FluorescenceMeasurement', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('ex_max', models.PositiveSmallIntegerField(blank=True, db_index=True, help_text='Excitation maximum (nm)', null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(900)])), + ('em_max', models.PositiveSmallIntegerField(blank=True, db_index=True, help_text='Emission maximum (nm)', null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1000)])), + ('emhex', models.CharField(blank=True, max_length=7)), + ('exhex', models.CharField(blank=True, max_length=7)), + ('ext_coeff', models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(300000)], verbose_name='Extinction Coefficient (M-1 cm-1)')), + ('qy', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='Quantum Yield')), + ('brightness', models.FloatField(blank=True, editable=False, null=True)), + ('lifetime', models.FloatField(blank=True, help_text='Lifetime (ns)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(20)])), + ('pka', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(2), django.core.validators.MaxValueValidator(12)], verbose_name='pKa')), + ('twop_ex_max', models.PositiveSmallIntegerField(blank=True, db_index=True, null=True, validators=[django.core.validators.MinValueValidator(700), django.core.validators.MaxValueValidator(1600)], verbose_name='Peak 2P excitation')), + ('twop_peakGM', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(200)], verbose_name='Peak 2P cross-section of S0->S1 (GM)')), + ('twop_qy', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='2P Quantum Yield')), + ('is_dark', models.BooleanField(default=False, help_text='This state does not fluorescence', verbose_name='Dark State')), + ('date_measured', models.DateField(blank=True, null=True)), + ('conditions', models.TextField(blank=True, help_text='pH, solvent, temp, etc.')), + ('is_trusted', models.BooleanField(default=False, help_text='If True, this measurement overrides others.')), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), + ('fluorophore', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='measurements', to='proteins.fluorophore')), + ('reference', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='references.reference')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), migrations.CreateModel( name='FluorophoreCollection', fields=[ @@ -480,8 +511,8 @@ class Migration(migrations.Migration): ('source', models.CharField(blank=True, help_text='Source of the spectra data', max_length=128)), ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), ('owner_camera', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectrum', to='proteins.camera')), - ('owner_dye', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectra', to='proteins.dye')), ('owner_filter', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectrum', to='proteins.filter')), + ('owner_fluor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectra', to='proteins.fluorophore')), ('owner_light', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectrum', to='proteins.light')), ('reference', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='spectra', to='references.reference')), ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL)), @@ -535,11 +566,6 @@ class Migration(migrations.Migration): name='transitions', field=models.ManyToManyField(blank=True, related_name='transition_state', through='proteins.StateTransition', to='proteins.state', verbose_name='State Transitions'), ), - migrations.AddField( - model_name='spectrum', - name='owner_state', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectra', to='proteins.state'), - ), migrations.AddField( model_name='protein', name='default_state', @@ -586,7 +612,7 @@ class Migration(migrations.Migration): ), migrations.AddIndex( model_name='spectrum', - index=models.Index(fields=['owner_state_id', 'status'], name='spectrum_state_status_idx'), + index=models.Index(fields=['owner_fluor_id', 'status'], name='spectrum_fluor_status_idx'), ), migrations.AddIndex( model_name='spectrum', diff --git a/backend/proteins/models/__init__.py b/backend/proteins/models/__init__.py index 192a39917..fe93e8db3 100644 --- a/backend/proteins/models/__init__.py +++ b/backend/proteins/models/__init__.py @@ -1,15 +1,15 @@ from .bleach import BleachMeasurement from .collection import ProteinCollection -from .dye import Dye as Dye +from .dye import Dye, DyeState from .efficiency import OcFluorEff from .excerpt import Excerpt +from .fluorescence_measurement import FluorescenceMeasurement from .fluorophore import Fluorophore from .lineage import Lineage from .microscope import FilterPlacement, Microscope, OpticalConfig from .organism import Organism from .oser import OSERMeasurement -from .protein import Protein -from .protein import State as State +from .protein import Protein, State from .snapgene import SnapGenePlasmid from .spectrum import Camera, Filter, Light, Spectrum from .transition import StateTransition @@ -18,9 +18,11 @@ "BleachMeasurement", "Camera", "Dye", + "DyeState", "Excerpt", "Filter", "FilterPlacement", + "FluorescenceMeasurement", "Fluorophore", "Light", "Lineage", diff --git a/backend/proteins/models/bleach.py b/backend/proteins/models/bleach.py index 0c7859df3..453cb2ae1 100644 --- a/backend/proteins/models/bleach.py +++ b/backend/proteins/models/bleach.py @@ -1,10 +1,14 @@ +from typing import TYPE_CHECKING + from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from model_utils.models import TimeStampedModel +from proteins.models.mixins import Authorable from references.models import Reference -from .mixins import Authorable +if TYPE_CHECKING: + from proteins.models import State # noqa F401 class BleachMeasurement(Authorable, TimeStampedModel): @@ -88,7 +92,7 @@ class BleachMeasurement(Authorable, TimeStampedModel): help_text="protein expressed in living cells", ) cell_type = models.CharField(max_length=60, blank=True, verbose_name="Cell Type", help_text="e.g. HeLa") - reference = models.ForeignKey( + reference = models.ForeignKey[Reference | None]( Reference, related_name="bleach_measurements", verbose_name="Measurement Reference", @@ -97,7 +101,7 @@ class BleachMeasurement(Authorable, TimeStampedModel): on_delete=models.SET_NULL, help_text="Reference where the measurement was made", ) # usually, the original paper that published the protein - state = models.ForeignKey( + state = models.ForeignKey["State"]( "State", related_name="bleach_measurements", verbose_name="Protein (state)", diff --git a/backend/proteins/models/collection.py b/backend/proteins/models/collection.py index ba655ee96..178bc0110 100644 --- a/backend/proteins/models/collection.py +++ b/backend/proteins/models/collection.py @@ -1,16 +1,22 @@ +from typing import TYPE_CHECKING + from django.contrib.auth import get_user_model from django.contrib.postgres.fields import ArrayField from django.db import models from django.urls import reverse from model_utils.models import TimeStampedModel +if TYPE_CHECKING: + from proteins.models import Protein + User = get_user_model() class OwnedCollection(TimeStampedModel): name = models.CharField(max_length=100) description = models.CharField(max_length=512, blank=True) - owner = models.ForeignKey( + owner_id: int | None + owner = models.ForeignKey[User | None]( User, blank=True, null=True, @@ -37,7 +43,10 @@ class Meta: class ProteinCollection(OwnedCollection): - proteins = models.ManyToManyField("Protein", related_name="collection_memberships") + if TYPE_CHECKING: + proteins: models.ManyToManyField["Protein", "ProteinCollection"] + else: + proteins = models.ManyToManyField("Protein", related_name="collection_memberships") private = models.BooleanField( default=False, verbose_name="Private Collection", diff --git a/backend/proteins/models/dye.py b/backend/proteins/models/dye.py index edb243bb8..9ed5de7cd 100644 --- a/backend/proteins/models/dye.py +++ b/backend/proteins/models/dye.py @@ -41,8 +41,13 @@ class Dye(Authorable, TimeStampedModel, Product): # TODO: rename to SmallMolecu # --- Hierarchy & Ontology --- # Handles FITC (Parent) vs 5-FITC (Child) relationship - parent_mixture = models.ForeignKey( - "self", on_delete=models.SET_NULL, null=True, blank=True, related_name="isomers" + parent_mixture_id: int | None + parent_mixture = models.ForeignKey["Dye | None"]( + "self", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="isomers", ) # Automated classification (e.g., "Rhodamine", "Cyanine", "BODIPY") @@ -104,6 +109,13 @@ def save(self, *args, **kwargs): self.entity_type = "dye" super().save(*args, **kwargs) + def get_absolute_url(self): + # For now, return a URL based on the dye slug + # TODO: implement proper dye detail view + from django.urls import reverse + + return reverse("proteins:spectra") + f"?owner={self.dye.slug}" + # This section handles the commercial reality of purchasing dyes. # diff --git a/backend/proteins/models/efficiency.py b/backend/proteins/models/efficiency.py index 2d96010c6..93daf67bd 100644 --- a/backend/proteins/models/efficiency.py +++ b/backend/proteins/models/efficiency.py @@ -1,3 +1,5 @@ +from typing import TYPE_CHECKING + from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError @@ -6,29 +8,30 @@ from django.db.models import F, Max, OuterRef, Q, Subquery from model_utils.models import TimeStampedModel -from proteins.models.dye import Dye as Dye -from proteins.models.protein import State as State +from proteins.models.fluorophore import Fluorophore from proteins.util.efficiency import oc_efficiency_report +if TYPE_CHECKING: + from proteins.models import OpticalConfig # noqa F401 + class OcFluorEffQuerySet(models.QuerySet): def outdated(self): - state_id = ContentType.objects.get(app_label="proteins", model="state").id - dye_id = ContentType.objects.get(app_label="proteins", model="dye").id + fluor_objs = Fluorophore.objects.filter(id=OuterRef("object_id")) + spectra_mod = fluor_objs.annotate(latest_spec=Max("spectra__modified")).values("latest_spec")[:1] - state_mod = State.objects.filter(id=OuterRef("object_id")).annotate(m=Max("spectra__modified")).values("m") - dye_mod = Dye.objects.filter(id=OuterRef("object_id")).annotate(m=Max("spectra__modified")).values("m") - - q = self.filter(content_type=state_id).annotate(fluor_mod=F("state__modified"), spec_mod=Subquery(state_mod)) - r = self.filter(content_type=dye_id).annotate(fluor_mod=F("dye__modified"), spec_mod=Subquery(dye_mod)) - return (q | r).filter( - Q(modified__lt=F("fluor_mod")) | Q(modified__lt=F("spec_mod")) | Q(modified__lt=F("oc__modified")) + fluor_mod = fluor_objs.values("modified")[:1] + return self.annotate( + fluor_mod=Subquery(fluor_mod), + spec_mod=Subquery(spectra_mod), + ).filter( + Q(modified__lt=F("fluor_mod")) | Q(modified__lt=F("spec_mod")) | Q(modified__lt=F("oc__modified")), ) class OcFluorEff(TimeStampedModel): - oc = models.ForeignKey("OpticalConfig", on_delete=models.CASCADE) - limit = models.Q(app_label="proteins", model="state") | models.Q(app_label="proteins", model="dye") + oc = models.ForeignKey["OpticalConfig"]("OpticalConfig", on_delete=models.CASCADE) + limit = models.Q(app_label="proteins", model__in=("state", "dyestate")) content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, limit_choices_to=limit) object_id = models.PositiveIntegerField() fluor = GenericForeignKey("content_type", "object_id") @@ -58,9 +61,11 @@ class Meta: unique_together = ("oc", "content_type", "object_id") def clean(self): - if self.content_type_id not in ContentType.objects.filter(self.limit).values_list("id", flat=True): - mods = ContentType.objects.filter(self.limit).values_list("model", flat=True) - raise ValidationError("ContentType for OcFluorEff.fluor must be in: {}".format(",".join(mods))) + allowed = list(ContentType.objects.filter(self.limit).values_list("id", "model")) + allowed_ids = {item[0] for item in allowed} + if self.content_type_id not in allowed_ids: + models_list = ", ".join(sorted({item[1] for item in allowed})) + raise ValidationError(f"ContentType for OcFluorEff.fluor must be in: {models_list}") def update_effs(self): rep = oc_efficiency_report(self.oc, [self.fluor]).get(self.fluor.slug, {}) @@ -71,8 +76,16 @@ def update_effs(self): @property def outdated(self): - # smod = [s.modified for s in self.fluor.spectra.all()] - return (self.modified < self.oc.modified) or (self.modified < self.fluor.modified) + oc_modified = getattr(self.oc, "modified", None) + fluor_modified = getattr(self.fluor, "modified", None) + spect_modified = None + if hasattr(self.fluor, "spectra"): + spect_modified = self.fluor.spectra.aggregate(latest=Max("modified")).get("latest") + + for candidate in (oc_modified, fluor_modified, spect_modified): + if candidate and self.modified < candidate: + return True + return False def save(self, *args, **kwargs): if self.pk is None or self.outdated: diff --git a/backend/proteins/models/excerpt.py b/backend/proteins/models/excerpt.py index 63e185527..1adcc4ea1 100644 --- a/backend/proteins/models/excerpt.py +++ b/backend/proteins/models/excerpt.py @@ -1,20 +1,28 @@ +from typing import TYPE_CHECKING + from django.db import models from django.db.models import Q from model_utils import Choices from model_utils.managers import QueryManager from model_utils.models import StatusModel, TimeStampedModel +from proteins.models.mixins import Authorable from references.models import Reference -from .mixins import Authorable +if TYPE_CHECKING: + from proteins.models import Protein class Excerpt(Authorable, TimeStampedModel, StatusModel): STATUS = Choices("approved", "flagged", "rejected") content = models.TextField(max_length=1200, help_text="Brief excerpt describing this protein") - proteins = models.ManyToManyField("Protein", blank=True, related_name="excerpts") - reference = models.ForeignKey( + if TYPE_CHECKING: + proteins: models.ManyToManyField["Protein", "Excerpt"] + else: + proteins = models.ManyToManyField("Protein", blank=True, related_name="excerpts") + reference_id: int | None + reference = models.ForeignKey[Reference | None]( Reference, related_name="excerpts", null=True, diff --git a/backend/proteins/models/fluorescence_measurement.py b/backend/proteins/models/fluorescence_measurement.py index 341515796..7edc2b793 100644 --- a/backend/proteins/models/fluorescence_measurement.py +++ b/backend/proteins/models/fluorescence_measurement.py @@ -5,10 +5,9 @@ from proteins.models.fluorescence_data import AbstractFluorescenceData if TYPE_CHECKING: + from proteins.models import Fluorophore # noqa: F401 from references.models import Reference # noqa: F401 - from .fluorophore import Fluorophore # noqa: F401 - # The "evidence" class FluorescenceMeasurement(AbstractFluorescenceData): @@ -19,7 +18,7 @@ class FluorescenceMeasurement(AbstractFluorescenceData): "Fluorophore", related_name="measurements", on_delete=models.CASCADE ) reference_id: int - reference = models.ForeignKey["Reference"]("Reference", on_delete=models.CASCADE) + reference = models.ForeignKey["Reference"]("references.Reference", on_delete=models.CASCADE) # Metadata specific to the act of measuring date_measured = models.DateField(null=True, blank=True) diff --git a/backend/proteins/models/fluorophore.py b/backend/proteins/models/fluorophore.py index 58c4dbe38..835bec852 100644 --- a/backend/proteins/models/fluorophore.py +++ b/backend/proteins/models/fluorophore.py @@ -1,13 +1,26 @@ from typing import TYPE_CHECKING, Literal from django.db import models +from django.db.models import QuerySet from proteins.models.fluorescence_data import AbstractFluorescenceData if TYPE_CHECKING: + from typing import Self + from django.db.models.manager import RelatedManager - from proteins.models import Spectrum + from proteins.models import FluorescenceMeasurement, Spectrum # noqa: F401 + + +class FluorophoreManager[T: models.Model](models.Manager): + _queryset_class: type[QuerySet[T]] + + def notdark(self): + return self.filter(is_dark=False) + + def with_spectra(self): + return self.get_queryset().filter(spectra__isnull=False).distinct() # The Canonical Parent (The Summary) @@ -18,7 +31,7 @@ class Fluorophore(AbstractFluorescenceData): While fluorophores support multiple measurements of fluorescence data, `Fluorophore` also inherits `AbstractFluorescenceData`, and the values accessible - on this instance serve as the canonical (cached, "published", "composited") + on this instance serve as the canonical (i.e. "cached", "published", "composited") fluorescence properties for this entity. Contains the 'Accepted/Cached' values for generic querying. @@ -38,8 +51,12 @@ class EntityTypes(models.TextChoices): # Maps field names to Measurement IDs. e.g., {'ex_max': 102, 'qy': 105} source_map = models.JSONField(default=dict, blank=True) + # Managers + objects: "FluorophoreManager[Self]" = FluorophoreManager() + if TYPE_CHECKING: spectra = RelatedManager["Spectrum"]() + measurements = RelatedManager["FluorescenceMeasurement"]() class Meta: indexes = [ diff --git a/backend/proteins/models/lineage.py b/backend/proteins/models/lineage.py index efbf0da2a..3ba1a9dc4 100644 --- a/backend/proteins/models/lineage.py +++ b/backend/proteins/models/lineage.py @@ -5,12 +5,11 @@ from mptt.models import MPTTModel, TreeForeignKey from fpseq.mutations import MutationSet +from proteins.models.mixins import Authorable +from proteins.models.protein import Protein +from proteins.util.maintain import validate_node from references.models import Reference -from ..models.mixins import Authorable -from ..util.maintain import validate_node -from .protein import Protein - def parse_mutation(mut_string): try: diff --git a/backend/proteins/models/microscope.py b/backend/proteins/models/microscope.py index 899cda9f7..6c73caddb 100644 --- a/backend/proteins/models/microscope.py +++ b/backend/proteins/models/microscope.py @@ -1,5 +1,6 @@ import json import urllib.parse +from typing import TYPE_CHECKING from django.contrib.postgres.fields import ArrayField from django.core.cache import cache @@ -10,11 +11,10 @@ from django.utils.functional import cached_property from fpbase.cache_utils import OPTICAL_CONFIG_CACHE_KEY - -from ..util.efficiency import spectral_product -from ..util.helpers import shortuuid -from .collection import OwnedCollection -from .spectrum import Camera, Filter, Light, sorted_ex2em +from proteins.models.collection import OwnedCollection +from proteins.models.spectrum import Camera, Filter, Light, sorted_ex2em +from proteins.util.efficiency import spectral_product +from proteins.util.helpers import shortuuid class Microscope(OwnedCollection): @@ -218,17 +218,32 @@ def get_cached_optical_configs() -> str: class OpticalConfig(OwnedCollection): """A a single optical configuration comprising a set of filters""" - microscope = models.ForeignKey("Microscope", related_name="optical_configs", on_delete=models.CASCADE) + microscope_id: int + microscope = models.ForeignKey["Microscope"]( + "Microscope", + related_name="optical_configs", + on_delete=models.CASCADE, + ) comments = models.CharField(max_length=256, blank=True) - filters = models.ManyToManyField("Filter", related_name="optical_configs", blank=True, through="FilterPlacement") - light = models.ForeignKey( + if TYPE_CHECKING: + filters = models.ManyToManyField["Filter", "OpticalConfig"] + else: + filters = models.ManyToManyField( + "Filter", + related_name="optical_configs", + blank=True, + through="FilterPlacement", + ) + light_id: int | None + light = models.ForeignKey["Light"]( "Light", null=True, blank=True, related_name="optical_configs", on_delete=models.SET_NULL, ) - camera = models.ForeignKey( + camera_id: int | None + camera = models.ForeignKey["Camera"]( "Camera", null=True, blank=True, diff --git a/backend/proteins/models/mixins.py b/backend/proteins/models/mixins.py index d5261bee5..ca5491258 100644 --- a/backend/proteins/models/mixins.py +++ b/backend/proteins/models/mixins.py @@ -16,14 +16,16 @@ def get_admin_url(self): class Authorable(models.Model): - created_by = models.ForeignKey( + created_by_id: int | None + created_by = models.ForeignKey["User | None"]( User, blank=True, null=True, related_name="%(class)s_author", on_delete=models.SET_NULL, ) - updated_by = models.ForeignKey( + updated_by_id: int | None + updated_by = models.ForeignKey["User | None"]( User, blank=True, null=True, diff --git a/backend/proteins/models/organism.py b/backend/proteins/models/organism.py index b4e3a7205..d1b97b1fd 100644 --- a/backend/proteins/models/organism.py +++ b/backend/proteins/models/organism.py @@ -3,8 +3,7 @@ from model_utils.models import TimeStampedModel from proteins.extrest import entrez - -from .mixins import Authorable +from proteins.models.mixins import Authorable class Organism(Authorable, TimeStampedModel): diff --git a/backend/proteins/models/oser.py b/backend/proteins/models/oser.py index e25cdc735..cbfff823a 100644 --- a/backend/proteins/models/oser.py +++ b/backend/proteins/models/oser.py @@ -1,10 +1,14 @@ +from typing import TYPE_CHECKING + from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from model_utils.models import TimeStampedModel +from proteins.models.mixins import Authorable from references.models import Reference -from .mixins import Authorable +if TYPE_CHECKING: + from proteins.models import Protein # noqa F401 class OSERMeasurement(Authorable, TimeStampedModel): @@ -56,7 +60,8 @@ class OSERMeasurement(Authorable, TimeStampedModel): ) temp = models.FloatField(null=True, blank=True, verbose_name="Temperature") - reference = models.ForeignKey( + reference_id: int | None + reference = models.ForeignKey["Reference | None"]( Reference, related_name="oser_measurements", verbose_name="Measurement Reference", @@ -65,7 +70,8 @@ class OSERMeasurement(Authorable, TimeStampedModel): on_delete=models.SET_NULL, help_text="Reference where the measurement was made", ) # usually, the original paper that published the protein - protein = models.ForeignKey( + protein_id: int + protein = models.ForeignKey["Protein"]( "Protein", related_name="oser_measurements", verbose_name="Protein", diff --git a/backend/proteins/models/protein.py b/backend/proteins/models/protein.py index 30512c759..531bc070f 100644 --- a/backend/proteins/models/protein.py +++ b/backend/proteins/models/protein.py @@ -35,6 +35,8 @@ from references.models import Reference if TYPE_CHECKING: + from typing import Self + from django.db.models.manager import RelatedManager from reversion.models import VersionQuerySet @@ -80,7 +82,9 @@ def to_tree(self, output="clw"): return result.stdout, newick -class _ProteinManager(models.Manager): +class _ProteinManager[T: models.Model](models.Manager): + _queryset_class: type[models.QuerySet[T]] + def get_queryset(self): return _ProteinQuerySet(self.model, using=self._db) @@ -257,7 +261,7 @@ class CofactorChoices(models.TextChoices): ) # managers - objects = _ProteinManager() + objects: "_ProteinManager[Self]" = _ProteinManager() visible = QueryManager(~Q(status="hidden")) def mutations_from_root(self): @@ -372,7 +376,7 @@ def d3_spectra(self): return json.dumps(spectra) def spectra_img(self, fmt="svg", output=None, **kwargs): - spectra = list(Spectrum.objects.filter(owner_state__protein=self).exclude(subtype="2p")) + spectra = list(Spectrum.objects.filter(owner_fluor__state__protein=self).exclude(subtype="2p")) title = self.name if kwargs.pop("title", False) else None if kwargs.get("twitter", False): title = self.name @@ -564,4 +568,10 @@ class State(Fluorophore): # TODO: rename to ProteinState def save(self, *args, **kwargs) -> None: self.entity_type = self.EntityTypes.PROTEIN + # Set label to protein name (used in API/UI for display) + if self.protein_id: + self.label = self.protein.name super().save(*args, **kwargs) + + def get_absolute_url(self): + return self.protein.get_absolute_url() diff --git a/backend/proteins/models/spectrum.py b/backend/proteins/models/spectrum.py index fda7fa2fe..1a4bfde67 100644 --- a/backend/proteins/models/spectrum.py +++ b/backend/proteins/models/spectrum.py @@ -2,7 +2,7 @@ import json import logging from functools import cached_property -from typing import Any, Literal +from typing import TYPE_CHECKING, Any, Literal import django.forms import numpy as np @@ -13,7 +13,6 @@ from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.db.models import Case, CharField, F, IntegerField, QuerySet, Value, When -from django.db.models.functions import Concat, NullIf from django.urls import reverse from django.utils.text import slugify from model_utils import Choices @@ -21,11 +20,13 @@ from model_utils.models import StatusModel, TimeStampedModel from fpbase.cache_utils import SPECTRA_CACHE_KEY +from proteins.models.mixins import AdminURLMixin, Authorable, Product +from proteins.util.helpers import spectra_fig, wave_to_hex +from proteins.util.spectra import interp_linear, norm2one, norm2P, step_size from references.models import Reference -from ..util.helpers import spectra_fig, wave_to_hex -from ..util.spectra import interp_linear, norm2one, norm2P, step_size -from .mixins import AdminURLMixin, Authorable, Product +if TYPE_CHECKING: + from proteins.models import Fluorophore # noqa: F401 logger = logging.getLogger(__name__) @@ -96,7 +97,7 @@ def get_spectra_list(query_set: QuerySet | None = None, **filters: str) -> list[ else: qs = query_set - owners = ["state", "dye", "filter", "light", "camera"] + owners = ["fluor", "filter", "light", "camera"] # Helper to create CASE statement for polymorphic owner fields def owner_case(field: str, **owner_fields) -> Case: @@ -110,18 +111,20 @@ def owner_case(field: str, **owner_fields) -> Case: # Fetch only needed fields and annotate with owner info qs = ( qs.only("id", "category", "subtype", *(f"owner_{o}_id" for o in owners)) - .select_related("owner_state__protein", "owner_dye", "owner_filter", "owner_light", "owner_camera") + .select_related("owner_fluor", "owner_filter", "owner_light", "owner_camera") .annotate( owner_id=owner_case("id"), owner_slug=owner_case("slug"), + # Fluorophore has 'label', others have 'name' owner_name=owner_case( "name", - state=Case( - When(owner_state__name="default", then=F("owner_state__protein__name")), - default=Concat(F("owner_state__protein__name"), Value(" ("), F("owner_state__name"), Value(")")), - ), + fluor=F("owner_fluor__label"), + ), + # Fluorophore doesn't have URL, others do + owner_url=owner_case( + "url", + fluor=Value(""), # Fluorophore has no URL field ), - owner_url=NullIf(owner_case("url", state=F("owner_state__protein__slug")), Value("")), ) .order_by("owner_name") .values("id", "category", "subtype", "owner_id", "owner_slug", "owner_name", "owner_url") @@ -146,69 +149,53 @@ def get_queryset(self): def all_objects(self): return super().get_queryset() - def state_slugs(self): - L = ( + def fluor_slugs(self): + """Get all fluorophore (State + DyeState) slugs.""" + return ( self.get_queryset() - .exclude(owner_state=None) - .values_list("owner_state__slug", "owner_state__protein__name", "owner_state__name") + .exclude(owner_fluor=None) + .values_list("owner_fluor__slug", "owner_fluor__label") .distinct() ) - return [(slug, prot if state == "default" else f"{prot} ({state})") for slug, prot, state in L] - - def dye_slugs(self): - return ( - self.get_queryset().filter(category=self.DYE).values_list("owner_dye__slug", "owner_dye__name").distinct() - ) - # FIXME: Stupid dumb dumb - def fluorlist(self, withdyes=True): + def fluorlist(self): + """Get list of all fluorophores (States + DyeStates) with spectra.""" vallist = [ "category", "subtype", - "owner_state__protein__name", - "owner_state__slug", - "owner_state__name", + "owner_fluor__slug", + "owner_fluor__label", ] - distinct = ["owner_state__slug"] - if withdyes: - vallist += ["owner_dye__slug", "owner_dye__name"] - distinct += ["owner_dye__slug"] Q = ( self.get_queryset() .filter(models.Q(category=Spectrum.DYE) | models.Q(category=Spectrum.PROTEIN)) + .exclude(owner_fluor=None) .values(*vallist) - .distinct(*distinct) + .distinct("owner_fluor__slug") ) out = [] for v in Q: - slug = v.get("owner_state__slug") or v.get("owner_dye__slug") - name = v.get("owner_dye__name", None) - if not name: - prot = v["owner_state__protein__name"] - state = v["owner_state__name"] - name = prot if state == "default" else f"{prot} ({state})" out.append( { "category": v["category"], "subtype": v["subtype"], - "slug": slug, - "name": name, + "slug": v["owner_fluor__slug"], + "name": v["owner_fluor__label"], } ) return sorted(out, key=lambda k: k["name"]) def filter_owner(self, slug): qs = self.none() - A = ("owner_state", "owner_dye", "owner_filter", "owner_light", "owner_camera") + A = ("owner_fluor", "owner_filter", "owner_light", "owner_camera") for ownerclass in A: qs = qs | self.get_queryset().filter(**{ownerclass + "__slug": slug}) return qs def find_similar_owners(self, query, threshold=0.4): A = ( - "owner_state__protein__name", - "owner_dye__name", + "owner_fluor__label", # Fluorophore.label for both State and DyeState "owner_filter__name", "owner_light__name", "owner_camera__name", @@ -346,30 +333,40 @@ class Spectrum(Authorable, StatusModel, TimeStampedModel, AdminURLMixin): # I was swayed to avoid Generic Foreign Keys by this article # https://lukeplant.me.uk/blog/posts/avoid-django-genericforeignkey/ - owner_state = models.ForeignKey("State", null=True, blank=True, on_delete=models.CASCADE, related_name="spectra") - owner_dye = models.ForeignKey("Dye", null=True, blank=True, on_delete=models.CASCADE, related_name="spectra") - owner_filter = models.OneToOneField( + # Fluorophore encompasses both State (ProteinState) and DyeState via MTI + owner_fluor_id: int | None + owner_fluor = models.ForeignKey["Fluorophore | None"]( + "Fluorophore", + null=True, + blank=True, + on_delete=models.CASCADE, + related_name="spectra", + ) + owner_filter_id: int | None + owner_filter = models.OneToOneField["Filter| None"]( "Filter", null=True, blank=True, on_delete=models.CASCADE, related_name="spectrum", ) - owner_light = models.OneToOneField( + owner_light_id: int | None + owner_light = models.OneToOneField["Light | None"]( "Light", null=True, blank=True, on_delete=models.CASCADE, related_name="spectrum", ) - owner_camera = models.OneToOneField( + owner_camera_id: int | None + owner_camera = models.OneToOneField["Camera | None"]( "Camera", null=True, blank=True, on_delete=models.CASCADE, related_name="spectrum", ) - reference = models.ForeignKey( + reference = models.ForeignKey["Reference | None"]( Reference, null=True, blank=True, @@ -389,15 +386,15 @@ class Spectrum(Authorable, StatusModel, TimeStampedModel, AdminURLMixin): class Meta: verbose_name_plural = "spectra" indexes = [ - # Composite index for the most common query pattern: filtering by state and status - models.Index(fields=["owner_state_id", "status"], name="spectrum_state_status_idx"), + # Composite index for the most common query pattern: filtering by fluorophore and status + models.Index(fields=["owner_fluor_id", "status"], name="spectrum_fluor_status_idx"), # Index on status for queries that only filter by approval status models.Index(fields=["status"], name="spectrum_status_idx"), ] def __str__(self): - if self.owner_state: - return "{} {}".format(self.owner_state if self.owner_state else "unowned", self.subtype) + if self.owner_fluor: + return f"{self.owner_fluor} {self.subtype}" else: return self.name @@ -462,8 +459,7 @@ def clean(self): @property def owner_set(self): return [ - self.owner_state, - self.owner_dye, + self.owner_fluor, self.owner_filter, self.owner_light, self.owner_camera, @@ -477,13 +473,16 @@ def owner(self): @property def name(self): # this method allows the protein name to have changed in the meantime - if self.owner_state: - if self.owner_state.name == "default": - return f"{self.owner_state.protein} {self.subtype}" + if self.owner_fluor: + # Check if it's a State (ProteinState) by checking for protein attribute + if hasattr(self.owner_fluor, "protein"): + if self.owner_fluor.name == "default": + return f"{self.owner_fluor.protein} {self.subtype}" + else: + return f"{self.owner_fluor} {self.subtype}" else: - return f"{self.owner_state} {self.subtype}" - elif self.owner_dye: - return f"{self.owner} {self.subtype}" + # It's a DyeState + return f"{self.owner} {self.subtype}" elif self.owner_filter: return str(self.owner) else: diff --git a/backend/proteins/models/transition.py b/backend/proteins/models/transition.py index 8cc381229..7c8028abd 100644 --- a/backend/proteins/models/transition.py +++ b/backend/proteins/models/transition.py @@ -1,9 +1,14 @@ +from typing import TYPE_CHECKING + from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from model_utils.models import TimeStampedModel -from .mixins import Authorable +from proteins.models.mixins import Authorable + +if TYPE_CHECKING: + from proteins.models import Protein, State # noqa F401 class StateTransition(Authorable, TimeStampedModel): @@ -14,21 +19,24 @@ class StateTransition(Authorable, TimeStampedModel): help_text="Wavelength required", validators=[MinValueValidator(300), MaxValueValidator(1000)], ) - protein = models.ForeignKey( + protein_id: int + protein = models.ForeignKey["Protein"]( "Protein", related_name="transitions", verbose_name="Protein Transitioning", help_text="The protein that demonstrates this transition", on_delete=models.CASCADE, ) - from_state = models.ForeignKey( + from_state_id: int + from_state = models.ForeignKey["State"]( "State", related_name="transitions_from", verbose_name="From state", help_text="The initial state ", on_delete=models.CASCADE, ) - to_state = models.ForeignKey( + to_state_id: int + to_state = models.ForeignKey["State"]( "State", related_name="transitions_to", verbose_name="To state", diff --git a/backend/proteins/schema/query.py b/backend/proteins/schema/query.py index 185b8c784..7f45ea60d 100644 --- a/backend/proteins/schema/query.py +++ b/backend/proteins/schema/query.py @@ -19,9 +19,7 @@ def get_cached_spectrum(id, timeout=60 * 60 * 24): spectrum = ( models.Spectrum.objects.filter(id=id) .select_related( - "owner_state", - "owner_state__protein", - "owner_dye", + "owner_fluor", "owner_camera", "owner_filter", "owner_light", diff --git a/backend/proteins/schema/types.py b/backend/proteins/schema/types.py index 5b2bdf161..8e665d9a9 100644 --- a/backend/proteins/schema/types.py +++ b/backend/proteins/schema/types.py @@ -150,11 +150,19 @@ class Meta: class Dye(DjangoObjectType): class Meta: - interfaces = (SpectrumOwnerInterface, FluorophoreInterface) model = models.Dye fields = "__all__" +class DyeState(DjangoObjectType): + """Fluorophore representing a dye in a specific environment.""" + + class Meta: + interfaces = (SpectrumOwnerInterface, FluorophoreInterface) + model = models.dye.DyeState + fields = "__all__" + + class Filter(DjangoObjectType): class Meta: interfaces = (SpectrumOwnerInterface,) @@ -202,15 +210,13 @@ class Meta: @gdo.resolver_hints( select_related=( - "owner_state", - "owner_dye", + "owner_fluor", "owner_camera", "owner_filter", "owner_light", ), only=( - "owner_state", - "owner_dye", + "owner_fluor", "owner_camera", "owner_filter", "owner_light", diff --git a/backend/proteins/tasks.py b/backend/proteins/tasks.py index a81b79ba9..6e99a084b 100644 --- a/backend/proteins/tasks.py +++ b/backend/proteins/tasks.py @@ -13,14 +13,14 @@ def calc_fret(): def calculate_scope_report(self, scope_id, outdated_ids=None, fluor_collection=None): import gc - from proteins.models import Dye, Microscope, OcFluorEff, State + from proteins.models import DyeState, Microscope, OcFluorEff, State # Initialize state_ids and dye_ids if not fluor_collection: # Use iterator to avoid loading all objects into memory at once # Build list of IDs instead of full objects state_ids = list(State.objects.with_spectra().values_list("id", flat=True)) - dye_ids = list(Dye.objects.with_spectra().values_list("id", flat=True)) + dye_ids = list(DyeState.objects.with_spectra().values_list("id", flat=True)) else: # fluor_collection is not currently implemented, but initialize to empty lists # to prevent potential errors if this parameter is used in the future @@ -72,7 +72,7 @@ def calculate_scope_report(self, scope_id, outdated_ids=None, fluor_collection=N # Process Dye objects in batches for start in range(0, len(dye_ids), batch_size): batch_ids = dye_ids[start : start + batch_size] - for dye in Dye.objects.filter(id__in=batch_ids).iterator(): + for dye in DyeState.objects.filter(id__in=batch_ids).iterator(): i += 1 self.update_state(state="PROGRESS", meta={"current": i, "total": total}) try: diff --git a/backend/proteins/util/helpers.py b/backend/proteins/util/helpers.py index 6dca20d2b..7ea107372 100644 --- a/backend/proteins/util/helpers.py +++ b/backend/proteins/util/helpers.py @@ -342,7 +342,7 @@ def forster_list(): # Fetch protein IDs first to reduce memory protein_ids = list( Protein.objects.with_spectra() - .filter(agg=Protein.MONOMER, switch_type=Protein.BASIC) + .filter(agg=Protein.AggChoices.MONOMER, switch_type=Protein.SwitchingChoices.BASIC) .values_list("id", flat=True) ) diff --git a/backend/proteins/util/maintain.py b/backend/proteins/util/maintain.py index 6b2174a76..97c3ce9f7 100644 --- a/backend/proteins/util/maintain.py +++ b/backend/proteins/util/maintain.py @@ -1,6 +1,7 @@ from typing import TYPE_CHECKING, cast from fpseq.mutations import Mutation +from proteins.models.protein import Protein if TYPE_CHECKING: from fpseq import FPSeq @@ -20,7 +21,7 @@ def check_node_sequence_mutation_consistent(node, correct_offset=False): return ms -def suggested_switch_type(protein): +def suggested_switch_type(protein: Protein) -> str | None: """return the "apparent" switch type based on states and transitions for best performance, pre-annotate the protein with ndark and nfrom: @@ -31,7 +32,7 @@ def suggested_switch_type(protein): if not nstates: return None if nstates == 1: - return protein.BASIC + return protein.SwitchingChoices.BASIC # 2 or more states... n_transitions = protein.transitions.count() if hasattr(protein, "ndark"): @@ -39,7 +40,7 @@ def suggested_switch_type(protein): else: darkstates = protein.states.filter(is_dark=True).count() if not n_transitions: - return protein.OTHER + return protein.SwitchingChoices.OTHER elif nstates == 2: # 2 transitions with unique from_states if hasattr(protein, "nfrom"): @@ -47,18 +48,18 @@ def suggested_switch_type(protein): else: nfrom = len(set(protein.transitions.values_list("from_state", flat=True))) if nfrom >= 2: - return protein.PHOTOSWITCHABLE + return protein.SwitchingChoices.PHOTOSWITCHABLE if darkstates == 0: - return protein.PHOTOCONVERTIBLE + return protein.SwitchingChoices.PHOTOCONVERTIBLE if darkstates == 1: - return protein.PHOTOACTIVATABLE + return protein.SwitchingChoices.PHOTOACTIVATABLE if darkstates > 1: return None elif nstates > 2: - return protein.MULTIPHOTOCHROMIC + return protein.SwitchingChoices.MULTIPHOTOCHROMIC -def validate_switch_type(protein): +def validate_switch_type(protein: Protein) -> bool: """returns False if the protein has an unusual switch type for its states & transitions. """ diff --git a/backend/proteins/views/ajax.py b/backend/proteins/views/ajax.py index 2571a79e0..851de6956 100644 --- a/backend/proteins/views/ajax.py +++ b/backend/proteins/views/ajax.py @@ -16,7 +16,7 @@ from fpbase.util import uncache_protein_page from proteins.util.maintain import validate_node -from ..models import Dye, Fluorophore, Lineage, Organism, Protein, Spectrum, State +from ..models import Fluorophore, Lineage, Organism, Protein, Spectrum, State from ..models.spectrum import Camera, Filter, Light logger = logging.getLogger(__name__) @@ -117,18 +117,15 @@ def similar_spectrum_owners(request): similars = Spectrum.objects.find_similar_owners(name, 0.3)[:4] # Group similars by type and fetch with proper prefetching to avoid N+1 queries - # State, Dye, Filter, Light, Camera are the possible types - state_ids = [] - dye_ids = [] + # Fluorophore (State + DyeState), Filter, Light, Camera are the possible types + fluor_ids = [] filter_ids = [] light_ids = [] camera_ids = [] for s in similars: - if isinstance(s, State): - state_ids.append(s.id) - elif isinstance(s, Dye): - dye_ids.append(s.id) + if isinstance(s, Fluorophore): + fluor_ids.append(s.id) elif isinstance(s, Filter): filter_ids.append(s.id) elif isinstance(s, Light): @@ -137,28 +134,38 @@ def similar_spectrum_owners(request): camera_ids.append(s.id) # Fetch each type with appropriate prefetching - # Fluorophores (State, Dye) have 'spectra' (plural), others have 'spectrum' (singular) - states = State.objects.filter(id__in=state_ids).select_related("protein").prefetch_related("spectra") - dyes = Dye.objects.filter(id__in=dye_ids).prefetch_related("spectra") + # For Fluorophores, we need to query State and DyeState separately to get concrete subclasses + # (Fluorophore.objects returns base class instances without get_absolute_url) + from proteins.models.dye import DyeState + + states = State.objects.filter(id__in=fluor_ids).select_related("protein").prefetch_related("spectra") + dye_states = DyeState.objects.filter(id__in=fluor_ids).select_related("dye").prefetch_related("spectra") filters = Filter.objects.filter(id__in=filter_ids).select_related("spectrum") lights = Light.objects.filter(id__in=light_ids).select_related("spectrum") cameras = Camera.objects.filter(id__in=camera_ids).select_related("spectrum") # Combine all objects maintaining order + # For Fluorophores (State/DyeState), use ID-only as key since original `similars` has base "Fluorophore" class similars_dict = {} - for item in [*states, *dyes, *filters, *lights, *cameras]: - similars_dict[(item.__class__.__name__, item.id)] = item + for item in [*states, *dye_states, *filters, *lights, *cameras]: + if isinstance(item, Fluorophore): + similars_dict[item.id] = item # Fluorophores: use ID only + else: + similars_dict[(item.__class__.__name__, item.id)] = item # Others: use (class, ID) similars_optimized = [] for s in similars: - key = (s.__class__.__name__, s.id) + if isinstance(s, Fluorophore): + key = s.id # For Fluorophores, look up by ID only + else: + key = (s.__class__.__name__, s.id) similars_optimized.append(similars_dict.get(key, s)) data = { "similars": [ { "slug": s.slug, - "name": s.protein.name if hasattr(s, "protein") else s.name, + "name": s.label if isinstance(s, Fluorophore) else s.name, "url": s.get_absolute_url(), "spectra": ( [sp.get_subtype_display() for sp in s.spectra.all()] diff --git a/backend/proteins/views/spectra.py b/backend/proteins/views/spectra.py index 38b2733b5..7ccc43678 100644 --- a/backend/proteins/views/spectra.py +++ b/backend/proteins/views/spectra.py @@ -73,7 +73,7 @@ def get_initial(self): if self.kwargs.get("slug", False): with contextlib.suppress(Exception): self.protein = Protein.objects.get(slug=self.kwargs.get("slug")) - init["owner_state"] = self.protein.default_state + init["owner_fluor"] = self.protein.default_state init["category"] = Spectrum.PROTEIN return init @@ -88,7 +88,7 @@ def get_form(self, form_class=None): if self.kwargs.get("slug", False): with contextlib.suppress(Exception): - form.fields["owner_state"] = forms.ModelChoiceField( + form.fields["owner_fluor"] = forms.ModelChoiceField( required=True, label="Protein (state)", empty_label=None, @@ -105,7 +105,9 @@ def form_valid(self, form): # It should return an HttpResponse. response = super().form_valid(form) with contextlib.suppress(Exception): - uncache_protein_page(self.object.owner_state.protein.slug, self.request) + # Uncache if this is a State (Protein) spectrum + if hasattr(self.object.owner_fluor, "protein"): + uncache_protein_page(self.object.owner_fluor.protein.slug, self.request) if not self.request.user.is_staff: body = f""" @@ -138,8 +140,7 @@ def spectra_csv(request): try: idlist = [int(x) for x in request.GET.get("q", "").split(",") if x] spectralist = Spectrum.objects.filter(id__in=idlist).select_related( - "owner_state__protein", - "owner_dye", + "owner_fluor", "owner_filter", "owner_light", "owner_camera", @@ -332,8 +333,7 @@ def pending_spectra_dashboard(request): .filter(status=Spectrum.STATUS.pending) .select_related( "created_by", - "owner_state__protein", - "owner_dye", + "owner_fluor", "owner_filter", "owner_camera", "owner_light", @@ -401,7 +401,7 @@ def pending_spectrum_action(request): spectra = ( Spectrum.objects.all_objects() .filter(id__in=spectrum_ids, status=Spectrum.STATUS.pending) - .select_related("owner_state__protein") + .select_related("owner_fluor") ) if not spectra.exists(): @@ -416,8 +416,9 @@ def pending_spectrum_action(request): # Clear cache for affected protein pages for spectrum in spectra: with contextlib.suppress(Exception): - if spectrum.owner_state: - uncache_protein_page(spectrum.owner_state.protein.slug, request) + # Uncache if this is a State (Protein) spectrum + if spectrum.owner_fluor and hasattr(spectrum.owner_fluor, "protein"): + uncache_protein_page(spectrum.owner_fluor.protein.slug, request) message = f"Accepted {count} spectrum(s)" elif action == "reject": diff --git a/backend/tests/test_fpbase/test_schema.py b/backend/tests/test_fpbase/test_graphql_schema.py similarity index 94% rename from backend/tests/test_fpbase/test_schema.py rename to backend/tests/test_fpbase/test_graphql_schema.py index e7ce77f85..62199d9dc 100644 --- a/backend/tests/test_fpbase/test_schema.py +++ b/backend/tests/test_fpbase/test_graphql_schema.py @@ -80,11 +80,17 @@ def setUp(self): self.microscope = models.Microscope.objects.create() self.protein = models.Protein.objects.create(name="test") self.optical_config = models.OpticalConfig.objects.create(microscope=self.microscope) - self.dye = models.Dye.objects.get_or_create(name="test-dye")[0] + self.dye = models.Dye.objects.get_or_create(name="test-dye", slug="test-dye")[0] + # Create a DyeState (Fluorophore) for the dye + from proteins.models.dye import DyeState + + self.dye_state = DyeState.objects.create( + dye=self.dye, name="test state", slug="test-dye-state", label="test-dye" + ) self.spectrum = models.Spectrum.objects.get_or_create( category=models.Spectrum.DYE, subtype=models.Spectrum.EM, - owner_dye=self.dye, + owner_fluor=self.dye_state, data=[[0, 1], [1, 1]], )[0] diff --git a/backend/tests/test_proteins/test_ajax_views.py b/backend/tests/test_proteins/test_ajax_views.py index bc3b5eb91..ac92935d5 100644 --- a/backend/tests/test_proteins/test_ajax_views.py +++ b/backend/tests/test_proteins/test_ajax_views.py @@ -21,6 +21,7 @@ StateFactory, ) from proteins.models import Spectrum +from proteins.models.dye import DyeState User = get_user_model() @@ -40,17 +41,28 @@ def setUpTestData(cls): cls.proteins.append(protein) cls.states.append(state) # Add spectra to states - SpectrumFactory(owner_state=state, category=Spectrum.PROTEIN, subtype=Spectrum.EX) - SpectrumFactory(owner_state=state, category=Spectrum.PROTEIN, subtype=Spectrum.EM) + SpectrumFactory(owner_fluor=state, category=Spectrum.PROTEIN, subtype=Spectrum.EX) + SpectrumFactory(owner_fluor=state, category=Spectrum.PROTEIN, subtype=Spectrum.EM) - # Create dyes (Fluorophores without protein) + # Create dyes - note: DyeState is the fluorophore that owns spectra, not Dye itself cls.dyes = [] + cls.dye_states = [] for i in range(3): dye = DyeFactory(name=f"SimilarDye{i}") cls.dyes.append(dye) - # Add spectra to dyes - SpectrumFactory(owner_dye=dye, category=Spectrum.DYE, subtype=Spectrum.EX) - SpectrumFactory(owner_dye=dye, category=Spectrum.DYE, subtype=Spectrum.EM) + # Create DyeState as the actual fluorophore owner of spectra + dye_state = DyeState.objects.create( + dye=dye, + name=f"SimilarDye{i} state", + slug=f"similardye{i}-state", + label=f"SimilarDye{i}", + ex_max=488, + em_max=520, + ) + cls.dye_states.append(dye_state) + # Add spectra to dye states + SpectrumFactory(owner_fluor=dye_state, category=Spectrum.DYE, subtype=Spectrum.EX) + SpectrumFactory(owner_fluor=dye_state, category=Spectrum.DYE, subtype=Spectrum.EM) # Create filters (factories automatically create spectrum) cls.filters = [] @@ -265,18 +277,18 @@ def test_similar_spectrum_owners_state_includes_protein_name(self): self.assertIn(self.proteins[0].name, names) def test_similar_spectrum_owners_dye_uses_own_name(self): - """Test that Dyes (Fluorophores without protein) use their own name.""" + """Test that DyeStates (Fluorophores without protein) use their label.""" response = self.client.post( "/ajax/validate_spectrumownername/", - {"owner": self.dyes[0].name}, + {"owner": self.dye_states[0].label}, headers={"X-Requested-With": "XMLHttpRequest"}, ) data = response.json() self.assertGreater(len(data["similars"]), 0) - # At least one should be the dye we searched for + # At least one should be the dye state we searched for names = [s["name"] for s in data["similars"]] - self.assertIn(self.dyes[0].name, names) + self.assertIn(self.dye_states[0].label, names) def test_similar_spectrum_owners_works_without_ajax_header(self): """Test that the endpoint works without the X-Requested-With header.""" diff --git a/backend/tests/test_proteins/test_forms.py b/backend/tests/test_proteins/test_forms.py index 5a432e17d..791d7a9e9 100644 --- a/backend/tests/test_proteins/test_forms.py +++ b/backend/tests/test_proteins/test_forms.py @@ -245,7 +245,7 @@ def test_spectrum_form_manual_data_valid(self): form_data = { "category": Spectrum.PROTEIN, "subtype": Spectrum.EX, - "owner_state": self.state.id, + "owner_fluor": self.state.id, "data": ( "[[400, 0.1], [401, 0.2], [402, 0.3], [403, 0.5], [404, 0.8], " "[405, 1.0], [406, 0.8], [407, 0.5], [408, 0.3], [409, 0.1]]" @@ -261,7 +261,7 @@ def test_spectrum_form_manual_data_missing(self): form_data = { "category": Spectrum.PROTEIN, "subtype": Spectrum.EX, - "owner_state": self.state.id, + "owner_fluor": self.state.id, "data": "", # Empty manual data "data_source": "manual", "confirmation": True, @@ -280,7 +280,7 @@ def test_spectrum_form_file_data_valid(self): form_data = { "category": Spectrum.PROTEIN, "subtype": Spectrum.EX, - "owner_state": self.state.id, + "owner_fluor": self.state.id, "data": "", # Empty manual data "data_source": "file", "confirmation": True, @@ -294,7 +294,7 @@ def test_spectrum_form_file_data_missing(self): form_data = { "category": Spectrum.PROTEIN, "subtype": Spectrum.EX, - "owner_state": self.state.id, + "owner_fluor": self.state.id, "data": "", "data_source": "file", "confirmation": True, @@ -309,7 +309,7 @@ def test_spectrum_form_default_data_source(self): form_data = { "category": Spectrum.PROTEIN, "subtype": Spectrum.EX, - "owner_state": self.state.id, + "owner_fluor": self.state.id, "data": "", "confirmation": True, # No data_source provided - should default to "file" @@ -329,7 +329,7 @@ def test_spectrum_form_with_interpolation(self): form_data = { "category": Spectrum.PROTEIN, "subtype": Spectrum.EX, - "owner_state": self.state.id, + "owner_fluor": self.state.id, "data": ("[[400, 0.1], [405, 0.5], [410, 1.0], [415, 0.8], [420, 0.5], [425, 0.3], [430, 0.1]]"), "data_source": "manual", "confirmation": True, diff --git a/backend/tests/test_proteins/test_models.py b/backend/tests/test_proteins/test_models.py index afaae6c5f..6f1f5ca72 100644 --- a/backend/tests/test_proteins/test_models.py +++ b/backend/tests/test_proteins/test_models.py @@ -21,13 +21,13 @@ def setUpTestData(cls): Spectrum.objects.create( category=Spectrum.PROTEIN, subtype=Spectrum.EX, - owner_state=state, + owner_fluor=state, data=[[431.0, 0.0039], [432.0, 0.0038], [433.0, 0.0042]], ) Spectrum.objects.create( category=Spectrum.PROTEIN, subtype=Spectrum.EM, - owner_state=state, + owner_fluor=state, data=[[550.0, 0.0012], [551.0, 0.0014], [552.0, 0.0015]], ) diff --git a/backend/tests/test_proteins/test_spectra_views.py b/backend/tests/test_proteins/test_spectra_views.py index 20aa87333..dddde6229 100644 --- a/backend/tests/test_proteins/test_spectra_views.py +++ b/backend/tests/test_proteins/test_spectra_views.py @@ -20,7 +20,7 @@ def setUpTestData(cls): for i in range(10): state = StateFactory() spectrum = SpectrumFactory( - owner_state=state, + owner_fluor=state, subtype=Spectrum.EX if i % 2 == 0 else Spectrum.EM, category=Spectrum.PROTEIN, ) diff --git a/backend/tests/test_proteins/test_tasks.py b/backend/tests/test_proteins/test_tasks.py index 0eb071398..b13ef1e58 100644 --- a/backend/tests/test_proteins/test_tasks.py +++ b/backend/tests/test_proteins/test_tasks.py @@ -110,7 +110,7 @@ def test_calculate_scope_report_uses_values_list(self): """Test that calculate_scope_report uses values_list for memory efficiency.""" with ( patch("proteins.models.State") as mock_state, - patch("proteins.models.Dye") as mock_dye, + patch("proteins.models.DyeState") as mock_dye, patch("proteins.models.Microscope") as mock_microscope, ): # Mock the with_spectra().values_list() chain diff --git a/backend/tests/test_proteins/test_views.py b/backend/tests/test_proteins/test_views.py index 3c884c576..866c8ec9b 100644 --- a/backend/tests/test_proteins/test_views.py +++ b/backend/tests/test_proteins/test_views.py @@ -106,7 +106,7 @@ def test_spectrum_preview_manual_data_success(self): post_data = { "category": Spectrum.PROTEIN, "subtype": Spectrum.EX, - "owner_state": self.state.id, + "owner_fluor": self.state.id, "data": ( "[[400, 0.1], [401, 0.2], [402, 0.3], [403, 0.5], [404, 0.8], " "[405, 1.0], [406, 0.8], [407, 0.5], [408, 0.3], [409, 0.1]]" @@ -140,7 +140,7 @@ def test_spectrum_preview_file_upload_success(self): data={ "category": Spectrum.PROTEIN, "subtype": Spectrum.EX, - "owner_state": self.state.id, + "owner_fluor": self.state.id, "data": "", "data_source": "file", "confirmation": True, @@ -163,7 +163,7 @@ def test_spectrum_preview_validation_failure_manual(self): post_data = { "category": Spectrum.PROTEIN, "subtype": Spectrum.EX, - "owner_state": self.state.id, + "owner_fluor": self.state.id, "data": "", # Empty manual data "data_source": "manual", "confirmation": True, @@ -185,7 +185,7 @@ def test_spectrum_preview_validation_failure_file(self): post_data = { "category": Spectrum.PROTEIN, "subtype": Spectrum.EX, - "owner_state": self.state.id, + "owner_fluor": self.state.id, "data": "", "data_source": "file", "confirmation": True, @@ -207,7 +207,7 @@ def test_spectrum_preview_invalid_spectrum_data(self): post_data = { "category": Spectrum.PROTEIN, "subtype": Spectrum.EX, - "owner_state": self.state.id, + "owner_fluor": self.state.id, "data": "invalid data format", # Invalid spectrum data "data_source": "manual", "confirmation": True, @@ -226,7 +226,7 @@ def test_spectrum_preview_requires_authentication(self): post_data = { "category": Spectrum.PROTEIN, "subtype": Spectrum.EX, - "owner_state": self.state.id, + "owner_fluor": self.state.id, "data": ( "[[400, 0.1], [401, 0.2], [402, 0.3], [403, 0.5], [404, 0.8], " "[405, 1.0], [406, 0.8], [407, 0.5], [408, 0.3], [409, 0.1]]" @@ -249,7 +249,7 @@ def test_spectrum_preview_data_source_defaults_to_file(self): post_data = { "category": Spectrum.PROTEIN, "subtype": Spectrum.EX, - "owner_state": self.state.id, + "owner_fluor": self.state.id, "data": "", "confirmation": True, # No data_source provided From 23b6b34535fe08aa4eb253c5a9c30d44dcfcabd6 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 21 Nov 2025 07:59:00 -0500 Subject: [PATCH 07/57] fix label save --- backend/fpbase/cache_utils.py | 1 + backend/proteins/models/protein.py | 5 +++++ .../tests/{test_fpbase => test_api}/test_graphql_schema.py | 0 3 files changed, 6 insertions(+) rename backend/tests/{test_fpbase => test_api}/test_graphql_schema.py (100%) diff --git a/backend/fpbase/cache_utils.py b/backend/fpbase/cache_utils.py index 50599bc3f..18ca188f7 100644 --- a/backend/fpbase/cache_utils.py +++ b/backend/fpbase/cache_utils.py @@ -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", diff --git a/backend/proteins/models/protein.py b/backend/proteins/models/protein.py index 531bc070f..56d4ebd51 100644 --- a/backend/proteins/models/protein.py +++ b/backend/proteins/models/protein.py @@ -417,6 +417,11 @@ def save(self, *args, **kwargs): if self.set_default_state(): super().save() + # Update label on all states when protein name changes + # The label field is cached on Fluorophore for query performance + if self.pk: + self.states.update(label=self.name) + class Meta: ordering = ["name"] indexes = [ diff --git a/backend/tests/test_fpbase/test_graphql_schema.py b/backend/tests/test_api/test_graphql_schema.py similarity index 100% rename from backend/tests/test_fpbase/test_graphql_schema.py rename to backend/tests/test_api/test_graphql_schema.py From 0ccac8fb5e5707f7c40a607f782e99e08b465184 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 21 Nov 2025 18:32:52 -0500 Subject: [PATCH 08/57] up pnpm --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 347ee80a1..ef4b7ce46 100644 --- a/package.json +++ b/package.json @@ -33,5 +33,5 @@ "engines": { "node": "22.x" }, - "packageManager": "pnpm@10.22.0" + "packageManager": "pnpm@10.23.0" } From 3cf13abf01fe449e150cea1a5b585884477a8e10 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 21 Nov 2025 18:54:53 -0500 Subject: [PATCH 09/57] more test fixes --- backend/fpbase/views.py | 2 +- backend/proteins/api/views.py | 2 +- backend/proteins/factories.py | 2 +- backend/proteins/models/fluorophore.py | 15 ++++++++++++++- backend/proteins/schema/query.py | 6 +++--- backend/proteins/util/efficiency.py | 3 ++- backend/proteins/views/fret.py | 5 +++-- backend/proteins/views/microscope.py | 8 ++++---- backend/tests/test_api/test_graphql_schema.py | 2 +- backend/tests/test_proteins/test_tasks.py | 2 +- backend/tests_e2e/test_e2e.py | 4 ++-- 11 files changed, 33 insertions(+), 18 deletions(-) diff --git a/backend/fpbase/views.py b/backend/fpbase/views.py index 53f46eb2d..0d5fbcdc8 100644 --- a/backend/fpbase/views.py +++ b/backend/fpbase/views.py @@ -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 diff --git a/backend/proteins/api/views.py b/backend/proteins/api/views.py index 11b019d92..a5ab9e7f0 100644 --- a/backend/proteins/api/views.py +++ b/backend/proteins/api/views.py @@ -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 diff --git a/backend/proteins/factories.py b/backend/proteins/factories.py index 88582e243..72da98574 100644 --- a/backend/proteins/factories.py +++ b/backend/proteins/factories.py @@ -365,7 +365,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"), diff --git a/backend/proteins/models/fluorophore.py b/backend/proteins/models/fluorophore.py index 835bec852..e5b4d7e61 100644 --- a/backend/proteins/models/fluorophore.py +++ b/backend/proteins/models/fluorophore.py @@ -10,7 +10,7 @@ from django.db.models.manager import RelatedManager - from proteins.models import FluorescenceMeasurement, Spectrum # noqa: F401 + from proteins.models import Dye, FluorescenceMeasurement, Protein, Spectrum # noqa: F401 class FluorophoreManager[T: models.Model](models.Manager): @@ -200,3 +200,16 @@ def within_em_band(self, value, height=0.7) -> bool: def d3_dicts(self) -> list[dict]: return [spect.d3dict() for spect in self.spectra.all()] + + def get_absolute_url(self) -> str | None: + # return the absolute url for the protein or dye that owns this fluorophore + if owner := self._owner(): + return owner.get_absolute_url() + return None + + def _owner(self) -> "Dye | Protein | None": + if hasattr(self, "dye"): + return self.dye + if hasattr(self, "protein"): + return self.protein + return None diff --git a/backend/proteins/schema/query.py b/backend/proteins/schema/query.py index 7f45ea60d..856107264 100644 --- a/backend/proteins/schema/query.py +++ b/backend/proteins/schema/query.py @@ -145,14 +145,14 @@ def resolve_opticalConfig(self, info, **kwargs): dye = graphene.Field(types.Dye, id=graphene.Int(), name=graphene.String()) def resolve_dyes(self, info, **kwargs): - return gdo.query(models.Dye.objects.all(), info) + return gdo.query(models.DyeState.objects.all(), info) def resolve_dye(self, info, **kwargs): name = kwargs.get("name") if name is not None: slug = slugify(name) - return gdo.query(models.Dye.objects.filter(slug=slug), info).get() + return gdo.query(models.DyeState.objects.filter(slug=slug), info).get() _id = kwargs.get("id") if _id is not None: - return gdo.query(models.Dye.objects.filter(id=_id), info).get() + return gdo.query(models.DyeState.objects.filter(id=_id), info).get() return None diff --git a/backend/proteins/util/efficiency.py b/backend/proteins/util/efficiency.py index 9c1833615..27d2af97a 100644 --- a/backend/proteins/util/efficiency.py +++ b/backend/proteins/util/efficiency.py @@ -1,6 +1,7 @@ import numpy as np from proteins.models.dye import Dye as Dye +from proteins.models.dye import DyeState from proteins.models.protein import State as State @@ -51,7 +52,7 @@ def oclist_efficiency_report(oclist, fluor_collection=None, include_dyes=True): if fluor_collection is None: fluor_collection = list(State.objects.with_spectra()) if include_dyes: - fluor_collection += list(Dye.objects.with_spectra()) + fluor_collection += list(DyeState.objects.with_spectra()) D = {} for oc in oclist: D[oc.name] = oc_efficiency_report(oc, fluor_collection) diff --git a/backend/proteins/views/fret.py b/backend/proteins/views/fret.py index 6fde8a97b..5580fb08a 100644 --- a/backend/proteins/views/fret.py +++ b/backend/proteins/views/fret.py @@ -4,8 +4,9 @@ from fpbase.celery import app from fpbase.util import is_ajax +from proteins.models.dye import DyeState -from ..models import Dye, State +from ..models import State from ..tasks import calc_fret @@ -47,7 +48,7 @@ def fret_chart(request): ] good_dyes = ( - Dye.objects.exclude(ext_coeff=None).exclude(qy=None).filter(spectra__subtype__in=("ex", "ab")) + DyeState.objects.exclude(ext_coeff=None).exclude(qy=None).filter(spectra__subtype__in=("ex", "ab")) ).values("slug", "name", "spectra__category", "spectra__subtype") slugs += [ diff --git a/backend/proteins/views/microscope.py b/backend/proteins/views/microscope.py index 8252f0100..28506d012 100644 --- a/backend/proteins/views/microscope.py +++ b/backend/proteins/views/microscope.py @@ -31,11 +31,11 @@ from fpbase.celery import app from fpbase.util import is_ajax +from proteins.models.dye import DyeState from ..forms import MicroscopeForm, OpticalConfigFormSet from ..models import ( Camera, - Dye, Light, Microscope, OpticalConfig, @@ -98,7 +98,7 @@ def scope_report_json(request, pk): oclist = microscope.optical_configs.values_list("id") state_ct = ContentType.objects.get_for_model(State) - dye_ct = ContentType.objects.get_for_model(Dye) + dye_ct = ContentType.objects.get_for_model(DyeState) effs = list( OcFluorEff.objects.exclude(ex_eff=None) @@ -184,7 +184,7 @@ def scope_report_json(request, pk): "uuid", ) } - dyes = Dye.objects.with_spectra() + dyes = DyeState.objects.with_spectra() fluors.update( { i["slug"]: i @@ -227,7 +227,7 @@ def post(self, request, *args, **kwargs): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - probe_count = State.objects.with_spectra().count() + Dye.objects.with_spectra().count() + probe_count = State.objects.with_spectra().count() + DyeState.objects.with_spectra().count() ids = self.object.optical_configs.all().values_list("id", flat=True) effs = OcFluorEff.objects.filter(oc__in=ids) context["outdated"] = list(effs.outdated().values_list("id", flat=True)) diff --git a/backend/tests/test_api/test_graphql_schema.py b/backend/tests/test_api/test_graphql_schema.py index 62199d9dc..2fba979da 100644 --- a/backend/tests/test_api/test_graphql_schema.py +++ b/backend/tests/test_api/test_graphql_schema.py @@ -80,7 +80,7 @@ def setUp(self): self.microscope = models.Microscope.objects.create() self.protein = models.Protein.objects.create(name="test") self.optical_config = models.OpticalConfig.objects.create(microscope=self.microscope) - self.dye = models.Dye.objects.get_or_create(name="test-dye", slug="test-dye")[0] + self.dye = models.DyeState.objects.get_or_create(name="test-dye", slug="test-dye")[0] # Create a DyeState (Fluorophore) for the dye from proteins.models.dye import DyeState diff --git a/backend/tests/test_proteins/test_tasks.py b/backend/tests/test_proteins/test_tasks.py index b13ef1e58..5a4c9fd36 100644 --- a/backend/tests/test_proteins/test_tasks.py +++ b/backend/tests/test_proteins/test_tasks.py @@ -139,7 +139,7 @@ def test_calculate_scope_report_signature(self): # This is a smoke test to ensure the function signature hasn't changed with ( patch("proteins.models.State") as mock_state, - patch("proteins.models.Dye") as mock_dye, + patch("proteins.models.DyeState") as mock_dye, patch("proteins.models.Microscope") as mock_microscope, patch("proteins.models.OcFluorEff") as mock_eff_model, ): diff --git a/backend/tests_e2e/test_e2e.py b/backend/tests_e2e/test_e2e.py index 140851eb7..d3b5a2c1d 100644 --- a/backend/tests_e2e/test_e2e.py +++ b/backend/tests_e2e/test_e2e.py @@ -114,7 +114,7 @@ def test_spectrum_submission_preview_manual_data( auth_page.locator("#id_confirmation").check() # Select2 autocomplete for protein - _select2_enter("#div_id_owner_state [role='combobox']", protein.name, auth_page) + _select2_enter("#div_id_owner_fluor [role='combobox']", protein.name, auth_page) # Switch to manual data tab and enter data auth_page.locator("#manual-tab").click() @@ -162,7 +162,7 @@ def test_spectrum_submission_tab_switching( auth_page.locator("#id_subtype").select_option(Spectrum.EX) auth_page.locator("#id_confirmation").check() - _select2_enter("#div_id_owner_state [role='combobox']", protein.name, auth_page) + _select2_enter("#div_id_owner_fluor [role='combobox']", protein.name, auth_page) # Test tab switching file_tab = auth_page.locator("#file-tab") From 06a228e0ef8eba957a96f544926ad1ab11f90c5a Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 21 Nov 2025 19:48:02 -0500 Subject: [PATCH 10/57] update oc_eff stuff --- ...0002_update_ocfluoreff_to_use_direct_fk.py | 77 +++++++++++++++++++ backend/proteins/models/efficiency.py | 20 +---- backend/proteins/models/fluorophore.py | 3 +- backend/proteins/models/protein.py | 2 - backend/proteins/tasks.py | 54 +++++-------- backend/proteins/views/microscope.py | 64 +++++++-------- 6 files changed, 128 insertions(+), 92 deletions(-) create mode 100644 backend/proteins/migrations/0002_update_ocfluoreff_to_use_direct_fk.py diff --git a/backend/proteins/migrations/0002_update_ocfluoreff_to_use_direct_fk.py b/backend/proteins/migrations/0002_update_ocfluoreff_to_use_direct_fk.py new file mode 100644 index 000000000..c8ad52699 --- /dev/null +++ b/backend/proteins/migrations/0002_update_ocfluoreff_to_use_direct_fk.py @@ -0,0 +1,77 @@ +from django.db import migrations, models +import django.db.models.deletion + + +def migrate_generic_fk_to_direct_fk(apps, schema_editor): + """Migrate data from GenericForeignKey (content_type/object_id) to direct ForeignKey (fluor).""" + OcFluorEff = apps.get_model("proteins", "OcFluorEff") + ContentType = apps.get_model("contenttypes", "ContentType") + + # Get content types for State and DyeState + try: + state_ct = ContentType.objects.get(app_label="proteins", model="state") + dyestate_ct = ContentType.objects.get(app_label="proteins", model="dyestate") + except ContentType.DoesNotExist: + # If content types don't exist, there's no data to migrate + return + + # Migrate all OcFluorEff records + for eff in OcFluorEff.objects.all(): + # The object_id already points to the correct Fluorophore ID + # because State and DyeState inherit from Fluorophore via MTI + if eff.content_type_id in (state_ct.id, dyestate_ct.id): + eff.fluor_id = eff.object_id + eff.save(update_fields=['fluor_id']) + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0001_initial"), + ("contenttypes", "0002_remove_content_type_name"), + ] + + operations = [ + # Step 1: Remove old unique_together constraint first (before removing fields) + migrations.AlterUniqueTogether( + name="ocfluoreff", + unique_together=set(), + ), + # Step 2: Add fluor field as nullable + migrations.AddField( + model_name="ocfluoreff", + name="fluor", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="proteins.fluorophore", + ), + ), + # Step 3: Migrate data + migrations.RunPython( + migrate_generic_fk_to_direct_fk, + reverse_code=migrations.RunPython.noop, + ), + # Step 4: Make fluor non-nullable + migrations.AlterField( + model_name="ocfluoreff", + name="fluor", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="proteins.fluorophore", + ), + ), + # Step 5: Remove content_type and object_id fields + migrations.RemoveField( + model_name="ocfluoreff", + name="content_type", + ), + migrations.RemoveField( + model_name="ocfluoreff", + name="object_id", + ), + # Step 6: Add new unique_together constraint + migrations.AlterUniqueTogether( + name="ocfluoreff", + unique_together={("oc", "fluor")}, + ), + ] diff --git a/backend/proteins/models/efficiency.py b/backend/proteins/models/efficiency.py index 93daf67bd..1a3b2e313 100644 --- a/backend/proteins/models/efficiency.py +++ b/backend/proteins/models/efficiency.py @@ -1,8 +1,5 @@ from typing import TYPE_CHECKING -from django.contrib.contenttypes.fields import GenericForeignKey -from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.db.models import F, Max, OuterRef, Q, Subquery @@ -17,7 +14,7 @@ class OcFluorEffQuerySet(models.QuerySet): def outdated(self): - fluor_objs = Fluorophore.objects.filter(id=OuterRef("object_id")) + fluor_objs = Fluorophore.objects.filter(id=OuterRef("fluor_id")) spectra_mod = fluor_objs.annotate(latest_spec=Max("spectra__modified")).values("latest_spec")[:1] fluor_mod = fluor_objs.values("modified")[:1] @@ -31,10 +28,8 @@ def outdated(self): class OcFluorEff(TimeStampedModel): oc = models.ForeignKey["OpticalConfig"]("OpticalConfig", on_delete=models.CASCADE) - limit = models.Q(app_label="proteins", model__in=("state", "dyestate")) - content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, limit_choices_to=limit) - object_id = models.PositiveIntegerField() - fluor = GenericForeignKey("content_type", "object_id") + fluor_id: int + fluor = models.ForeignKey[Fluorophore](Fluorophore, on_delete=models.CASCADE, related_name="oc_effs") fluor_name = models.CharField(max_length=100, blank=True) ex_eff = models.FloatField( null=True, @@ -58,14 +53,7 @@ class OcFluorEff(TimeStampedModel): objects = OcFluorEffQuerySet.as_manager() class Meta: - unique_together = ("oc", "content_type", "object_id") - - def clean(self): - allowed = list(ContentType.objects.filter(self.limit).values_list("id", "model")) - allowed_ids = {item[0] for item in allowed} - if self.content_type_id not in allowed_ids: - models_list = ", ".join(sorted({item[1] for item in allowed})) - raise ValidationError(f"ContentType for OcFluorEff.fluor must be in: {models_list}") + unique_together = ("oc", "fluor") def update_effs(self): rep = oc_efficiency_report(self.oc, [self.fluor]).get(self.fluor.slug, {}) diff --git a/backend/proteins/models/fluorophore.py b/backend/proteins/models/fluorophore.py index e5b4d7e61..0d441c50b 100644 --- a/backend/proteins/models/fluorophore.py +++ b/backend/proteins/models/fluorophore.py @@ -10,7 +10,7 @@ from django.db.models.manager import RelatedManager - from proteins.models import Dye, FluorescenceMeasurement, Protein, Spectrum # noqa: F401 + from proteins.models import Dye, FluorescenceMeasurement, OcFluorEff, Protein, Spectrum # noqa: F401 class FluorophoreManager[T: models.Model](models.Manager): @@ -57,6 +57,7 @@ class EntityTypes(models.TextChoices): if TYPE_CHECKING: spectra = RelatedManager["Spectrum"]() measurements = RelatedManager["FluorescenceMeasurement"]() + oc_effs = RelatedManager["OcFluorEff"]() class Meta: indexes = [ diff --git a/backend/proteins/models/protein.py b/backend/proteins/models/protein.py index 56d4ebd51..8825b5a06 100644 --- a/backend/proteins/models/protein.py +++ b/backend/proteins/models/protein.py @@ -9,7 +9,6 @@ from subprocess import PIPE, run from typing import TYPE_CHECKING, cast -from django.contrib.contenttypes.fields import GenericRelation from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.search import TrigramSimilarity from django.core.exceptions import ObjectDoesNotExist, ValidationError @@ -558,7 +557,6 @@ class State(Fluorophore): # TODO: rename to ProteinState help_text="Maturation time (min)", # maturation half-life in min validators=[MinValueValidator(0), MaxValueValidator(1600)], ) - oc_eff = GenericRelation("OcFluorEff", related_query_name="state") if TYPE_CHECKING: transitions = models.ManyToManyField["State", "State"] diff --git a/backend/proteins/tasks.py b/backend/proteins/tasks.py index 6e99a084b..3dd5107e8 100644 --- a/backend/proteins/tasks.py +++ b/backend/proteins/tasks.py @@ -45,44 +45,28 @@ def calculate_scope_report(self, scope_id, outdated_ids=None, fluor_collection=N return # Process states and dyes separately in batches to reduce memory usage + # Note: We query concrete subclasses (State, DyeState) instead of Fluorophore + # because MTI requires this to access child-specific fields oc_count = m.optical_configs.count() total = oc_count * (len(state_ids) + len(dye_ids)) - # Process states in batches batch_size = 50 for oc in m.optical_configs.all(): - # Process State objects in batches - for start in range(0, len(state_ids), batch_size): - batch_ids = state_ids[start : start + batch_size] - for state in State.objects.filter(id__in=batch_ids).iterator(): - i += 1 - self.update_state(state="PROGRESS", meta={"current": i, "total": total}) - try: - obj = OcFluorEff.objects.get(oc=oc, state=state) - if obj.outdated: - obj.save() - updated.append((oc, state)) - except OcFluorEff.DoesNotExist: - try: - OcFluorEff.objects.create(oc=oc, fluor=state) - except Exception as e: - capture_exception(e) - gc.collect() - - # Process Dye objects in batches - for start in range(0, len(dye_ids), batch_size): - batch_ids = dye_ids[start : start + batch_size] - for dye in DyeState.objects.filter(id__in=batch_ids).iterator(): - i += 1 - self.update_state(state="PROGRESS", meta={"current": i, "total": total}) - try: - obj = OcFluorEff.objects.get(oc=oc, dye=dye) - if obj.outdated: - obj.save() - updated.append((oc, dye)) - except OcFluorEff.DoesNotExist: + # Process both State and DyeState with the same logic + for model_class, fluor_ids in [(State, state_ids), (DyeState, dye_ids)]: + for start in range(0, len(fluor_ids), batch_size): + batch_ids = fluor_ids[start : start + batch_size] + for fluor in model_class.objects.filter(id__in=batch_ids).iterator(): + i += 1 + self.update_state(state="PROGRESS", meta={"current": i, "total": total}) try: - OcFluorEff.objects.create(oc=oc, fluor=dye) - except Exception as e: - capture_exception(e) - gc.collect() + obj = OcFluorEff.objects.get(oc=oc, fluor=fluor) + if obj.outdated: + obj.save() + updated.append((oc, fluor)) + except OcFluorEff.DoesNotExist: + try: + OcFluorEff.objects.create(oc=oc, fluor=fluor) + except Exception as e: + capture_exception(e) + gc.collect() diff --git a/backend/proteins/views/microscope.py b/backend/proteins/views/microscope.py index 28506d012..4fa6eb552 100644 --- a/backend/proteins/views/microscope.py +++ b/backend/proteins/views/microscope.py @@ -4,14 +4,13 @@ from django.contrib import messages from django.contrib.auth import get_user_model -from django.contrib.contenttypes.models import ContentType from django.contrib.messages.views import SuccessMessageMixin from django.contrib.postgres.aggregates import ArrayAgg from django.core.exceptions import PermissionDenied from django.core.mail import mail_admins from django.db import transaction -from django.db.models import CharField, Count, F, Q, Value -from django.db.models.functions import Lower +from django.db.models import Case, CharField, Count, F, Q, Value, When +from django.db.models.functions import Cast, Lower from django.http import ( Http404, HttpResponseNotAllowed, @@ -97,21 +96,33 @@ def scope_report_json(request, pk): microscope = Microscope.objects.get(id=pk) oclist = microscope.optical_configs.values_list("id") - state_ct = ContentType.objects.get_for_model(State) - dye_ct = ContentType.objects.get_for_model(DyeState) - + # Query all efficiency records for this microscope's optical configs + # Use Case/When to determine the correct ID field based on entity_type effs = list( OcFluorEff.objects.exclude(ex_eff=None) - .filter(content_type=state_ct, oc__in=oclist) + .filter(oc__in=oclist) + .select_related("fluor") .annotate( - fluor_id=F("state__protein__uuid"), - fluor_slug=F("state__slug"), - type=Value("p", CharField()), + # For proteins, use the protein's UUID; for dyes, use the dye's ID (cast to string) + owner_id=Case( + When(fluor__entity_type="protein", then=F("fluor__state__protein__uuid")), + When(fluor__entity_type="dye", then=Cast(F("fluor__dyestate__dye__id"), CharField())), + default=Value(None), + output_field=CharField(), + ), + owner_slug=F("fluor__slug"), + # Map entity_type to the old 'p'/'d' format + type=Case( + When(fluor__entity_type="protein", then=Value("p")), + When(fluor__entity_type="dye", then=Value("d")), + default=Value("p"), + output_field=CharField(), + ), ) .values( - "fluor_id", + "owner_id", "fluor_name", - "fluor_slug", + "owner_slug", "ex_eff", "em_eff", "ex_eff_broad", @@ -121,29 +132,6 @@ def scope_report_json(request, pk): ) ) - effs.extend( - list( - OcFluorEff.objects.exclude(ex_eff=None) - .filter(content_type=dye_ct, oc__in=oclist) - .annotate( - fluor_id=F("dye__id"), - fluor_slug=F("dye__slug"), - type=Value("d", CharField()), - ) - .values( - "fluor_id", - "fluor_name", - "fluor_slug", - "ex_eff", - "em_eff", - "ex_eff_broad", - "brightness", - "type", - "oc__name", - ) - ) - ) - data = defaultdict(list) for item in effs: if not (item["ex_eff"] and item["em_eff"]): @@ -151,15 +139,15 @@ def scope_report_json(request, pk): data[item["oc__name"]].append( { "fluor": item["fluor_name"], - "fluor_slug": item["fluor_slug"], - "fluor_id": item["fluor_id"], + "fluor_slug": item["owner_slug"], + "fluor_id": item["owner_id"], "ex_eff": item["ex_eff"], "ex_eff_broad": item["ex_eff_broad"], "em_eff": item["em_eff"], "brightness": item["brightness"] or None, "shape": "circle" if item["type"] == "p" else "square", "url": microscope.get_absolute_url() - + "?c={}&p={}".format(quote(item["oc__name"]), quote(item["fluor_slug"])), + + "?c={}&p={}".format(quote(item["oc__name"]), quote(item["owner_slug"])), } ) From a0abf207a4262fbd30ed64666ff99312c57a972a Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 21 Nov 2025 21:00:32 -0500 Subject: [PATCH 11/57] typing --- backend/favit/models.py | 13 +++++- backend/fpbase/users/models.py | 21 ++++++++- backend/proteins/models/bleach.py | 4 ++ backend/proteins/models/collection.py | 13 ++++-- backend/proteins/models/dye.py | 7 +++ backend/proteins/models/efficiency.py | 1 + backend/proteins/models/fluorescence_data.py | 2 + backend/proteins/models/fluorophore.py | 8 +++- backend/proteins/models/lineage.py | 16 ++++++- backend/proteins/models/microscope.py | 36 +++++++++++++--- backend/proteins/models/organism.py | 10 +++++ backend/proteins/models/protein.py | 29 +++++++++++-- backend/proteins/models/snapgene.py | 8 ++++ backend/proteins/models/spectrum.py | 19 ++++++++- backend/proteins/util/maintain.py | 10 ++--- backend/references/models.py | 45 +++++++++++++++++--- 16 files changed, 210 insertions(+), 32 deletions(-) diff --git a/backend/favit/models.py b/backend/favit/models.py index c0113c3a4..38ec2a619 100644 --- a/backend/favit/models.py +++ b/backend/favit/models.py @@ -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 @@ -6,14 +10,19 @@ 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) diff --git a/backend/fpbase/users/models.py b/backend/fpbase/users/models.py index 809ba9b30..b09885ccb 100644 --- a/backend/fpbase/users/models.py +++ b/backend/fpbase/users/models.py @@ -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 @@ -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 @@ -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: diff --git a/backend/proteins/models/bleach.py b/backend/proteins/models/bleach.py index 453cb2ae1..cb6cb60f9 100644 --- a/backend/proteins/models/bleach.py +++ b/backend/proteins/models/bleach.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import TYPE_CHECKING from django.core.validators import MaxValueValidator, MinValueValidator @@ -92,6 +94,7 @@ class BleachMeasurement(Authorable, TimeStampedModel): help_text="protein expressed in living cells", ) cell_type = models.CharField(max_length=60, blank=True, verbose_name="Cell Type", help_text="e.g. HeLa") + reference_id: int | None reference = models.ForeignKey[Reference | None]( Reference, related_name="bleach_measurements", @@ -101,6 +104,7 @@ class BleachMeasurement(Authorable, TimeStampedModel): on_delete=models.SET_NULL, help_text="Reference where the measurement was made", ) # usually, the original paper that published the protein + state_id: int state = models.ForeignKey["State"]( "State", related_name="bleach_measurements", diff --git a/backend/proteins/models/collection.py b/backend/proteins/models/collection.py index 178bc0110..10166444f 100644 --- a/backend/proteins/models/collection.py +++ b/backend/proteins/models/collection.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import TYPE_CHECKING from django.contrib.auth import get_user_model @@ -7,7 +9,7 @@ from model_utils.models import TimeStampedModel if TYPE_CHECKING: - from proteins.models import Protein + from proteins.models import Dye, Microscope, Protein # noqa: F401 User = get_user_model() @@ -44,7 +46,8 @@ class Meta: class ProteinCollection(OwnedCollection): if TYPE_CHECKING: - proteins: models.ManyToManyField["Protein", "ProteinCollection"] + proteins: models.ManyToManyField[Protein, ProteinCollection] + on_scope: models.QuerySet[Microscope] else: proteins = models.ManyToManyField("Protein", related_name="collection_memberships") private = models.BooleanField( @@ -61,7 +64,11 @@ class Meta: class FluorophoreCollection(ProteinCollection): - dyes = models.ManyToManyField("Dye", blank=True, related_name="collection_memberships") + if TYPE_CHECKING: + dyes = models.ManyToManyField["Dye", "FluorophoreCollection"] + fluor_on_scope: models.QuerySet[Microscope] + else: + dyes = models.ManyToManyField("Dye", blank=True, related_name="collection_memberships") def get_absolute_url(self): return reverse("proteins:fluor-collection-detail", args=[self.id]) diff --git a/backend/proteins/models/dye.py b/backend/proteins/models/dye.py index 9ed5de7cd..6977a9364 100644 --- a/backend/proteins/models/dye.py +++ b/backend/proteins/models/dye.py @@ -8,6 +8,7 @@ from proteins.models.mixins import Authorable, Product if TYPE_CHECKING: + from proteins.models import DyeState, ReactiveDerivative from references.models import Reference # noqa: F401 @@ -63,6 +64,12 @@ class Dye(Authorable, TimeStampedModel, Product): # TODO: rename to SmallMolecu help_text="Equilibrium constant between non-fluorescent lactone and fluorescent zwitterion.", ) + if TYPE_CHECKING: + isomers: models.QuerySet["Dye"] + states: models.QuerySet[DyeState] + derivatives: models.QuerySet[ReactiveDerivative] + collection_memberships: models.QuerySet + class Meta: # Enforce uniqueness only on defined structures to allow multiple proprietary entries constraints = [ diff --git a/backend/proteins/models/efficiency.py b/backend/proteins/models/efficiency.py index 1a3b2e313..a80ba13e4 100644 --- a/backend/proteins/models/efficiency.py +++ b/backend/proteins/models/efficiency.py @@ -27,6 +27,7 @@ def outdated(self): class OcFluorEff(TimeStampedModel): + oc_id: int oc = models.ForeignKey["OpticalConfig"]("OpticalConfig", on_delete=models.CASCADE) fluor_id: int fluor = models.ForeignKey[Fluorophore](Fluorophore, on_delete=models.CASCADE, related_name="oc_effs") diff --git a/backend/proteins/models/fluorescence_data.py b/backend/proteins/models/fluorescence_data.py index 5891516ff..95948918f 100644 --- a/backend/proteins/models/fluorescence_data.py +++ b/backend/proteins/models/fluorescence_data.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from model_utils.models import TimeStampedModel diff --git a/backend/proteins/models/fluorophore.py b/backend/proteins/models/fluorophore.py index 0d441c50b..fd94f3cd4 100644 --- a/backend/proteins/models/fluorophore.py +++ b/backend/proteins/models/fluorophore.py @@ -10,7 +10,13 @@ from django.db.models.manager import RelatedManager - from proteins.models import Dye, FluorescenceMeasurement, OcFluorEff, Protein, Spectrum # noqa: F401 + from proteins.models import ( # noqa: F401 + Dye, + FluorescenceMeasurement, + OcFluorEff, + Protein, + Spectrum, + ) class FluorophoreManager[T: models.Model](models.Manager): diff --git a/backend/proteins/models/lineage.py b/backend/proteins/models/lineage.py index 3ba1a9dc4..886370593 100644 --- a/backend/proteins/models/lineage.py +++ b/backend/proteins/models/lineage.py @@ -1,3 +1,7 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from django.core.exceptions import ValidationError from django.db import models from django.utils.translation import gettext_lazy as _ @@ -34,9 +38,12 @@ def get_prep_value(self, value): class Lineage(MPTTModel, TimeStampedModel, Authorable): + protein_id: int protein = models.OneToOneField["Protein"]("Protein", on_delete=models.CASCADE, related_name="lineage") + parent_id: int | None parent = TreeForeignKey("self", on_delete=models.CASCADE, null=True, blank=True, related_name="children") - reference = models.ForeignKey( + reference_id: int | None + reference = models.ForeignKey[Reference | None]( Reference, on_delete=models.CASCADE, null=True, @@ -45,7 +52,8 @@ class Lineage(MPTTModel, TimeStampedModel, Authorable): ) mutation = MutationSetField(max_length=400, blank=True) rootmut = models.CharField(max_length=400, blank=True) - root_node = models.ForeignKey( + root_node_id: int | None + root_node = models.ForeignKey["Lineage | None"]( "self", null=True, on_delete=models.CASCADE, @@ -53,6 +61,10 @@ class Lineage(MPTTModel, TimeStampedModel, Authorable): verbose_name="Root Node", ) + if TYPE_CHECKING: + children: models.QuerySet[Lineage] + descendants: models.QuerySet[Lineage] + class MPTTMeta: order_insertion_by = ["protein"] diff --git a/backend/proteins/models/microscope.py b/backend/proteins/models/microscope.py index 6c73caddb..fd0317864 100644 --- a/backend/proteins/models/microscope.py +++ b/backend/proteins/models/microscope.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json import urllib.parse from typing import TYPE_CHECKING @@ -16,6 +18,11 @@ from proteins.util.efficiency import spectral_product from proteins.util.helpers import shortuuid +if TYPE_CHECKING: + from django.db.models.manager import RelatedManager + + from proteins.models.spectrum import Spectrum + class Microscope(OwnedCollection): """A microscope or other collection of optical configurations @@ -28,21 +35,27 @@ class Microscope(OwnedCollection): """ id = models.CharField(primary_key=True, max_length=22, default=shortuuid, editable=False) - extra_lights = models.ManyToManyField("Light", blank=True, related_name="microscopes") - extra_cameras = models.ManyToManyField("Camera", blank=True, related_name="microscopes") + if TYPE_CHECKING: + extra_lights = models.ManyToManyField["Light", "Microscope"] + extra_cameras = models.ManyToManyField["Camera", "Microscope"] + else: + extra_lights = models.ManyToManyField("Light", blank=True, related_name="microscopes") + extra_cameras = models.ManyToManyField("Camera", blank=True, related_name="microscopes") extra_lasers = ArrayField( models.PositiveSmallIntegerField(validators=[MinValueValidator(300), MaxValueValidator(1600)]), default=list, blank=True, ) - collection = models.ForeignKey( + collection_id: int | None + collection = models.ForeignKey["ProteinCollection | None"]( "ProteinCollection", blank=True, null=True, related_name="on_scope", on_delete=models.CASCADE, ) - fluors = models.ForeignKey( + fluors_id: int | None + fluors = models.ForeignKey["FluorophoreCollection | None"]( "FluorophoreCollection", blank=True, null=True, @@ -73,6 +86,9 @@ class Microscope(OwnedCollection): help_text="Enable pan and zoom on spectra plot.", ) + if TYPE_CHECKING: + optical_configs = RelatedManager["OpticalConfig"]() + class Meta: ordering = ["created"] @@ -160,7 +176,7 @@ def bs_filters(self): ) @cached_property - def spectra(self): + def spectra(self) -> list[Spectrum]: spectra = [] for f in ( self.ex_filters, @@ -226,7 +242,11 @@ class OpticalConfig(OwnedCollection): ) comments = models.CharField(max_length=256, blank=True) if TYPE_CHECKING: + from proteins.models import OcFluorEff + filters = models.ManyToManyField["Filter", "OpticalConfig"] + filterplacement_set: models.QuerySet[FilterPlacement] + ocfluoreff_set: models.QuerySet[OcFluorEff] else: filters = models.ManyToManyField( "Filter", @@ -342,8 +362,10 @@ class FilterPlacement(models.Model): BS = "bs" PATH_CHOICES = ((EX, "Excitation Path"), (EM, "Emission Path"), (BS, "Both Paths")) - filter = models.ForeignKey("Filter", on_delete=models.CASCADE) - config = models.ForeignKey("OpticalConfig", on_delete=models.CASCADE) + filter_id: int + filter = models.ForeignKey[Filter]("Filter", on_delete=models.CASCADE) + config_id: int + config = models.ForeignKey["OpticalConfig"]("OpticalConfig", on_delete=models.CASCADE) path = models.CharField(max_length=2, choices=PATH_CHOICES, verbose_name="Ex/Bs/Em Path") # when path == BS, reflects refers to the emission path reflects = models.BooleanField(default=False, help_text="Filter reflects emission (if BS or EM filter)") diff --git a/backend/proteins/models/organism.py b/backend/proteins/models/organism.py index d1b97b1fd..ea7173006 100644 --- a/backend/proteins/models/organism.py +++ b/backend/proteins/models/organism.py @@ -1,3 +1,7 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from django.db import models from django.urls import reverse from model_utils.models import TimeStampedModel @@ -5,6 +9,9 @@ from proteins.extrest import entrez from proteins.models.mixins import Authorable +if TYPE_CHECKING: + from proteins.models import Protein + class Organism(Authorable, TimeStampedModel): """A class for the parental organism (species) from which the protein has been engineered""" @@ -20,6 +27,9 @@ class Organism(Authorable, TimeStampedModel): genus = models.CharField(max_length=128, blank=True) rank = models.CharField(max_length=128, blank=True) + if TYPE_CHECKING: + proteins: models.QuerySet[Protein] + def __str__(self): return self.scientific_name diff --git a/backend/proteins/models/protein.py b/backend/proteins/models/protein.py index 8825b5a06..742d34a34 100644 --- a/backend/proteins/models/protein.py +++ b/backend/proteins/models/protein.py @@ -39,7 +39,16 @@ from django.db.models.manager import RelatedManager from reversion.models import VersionQuerySet - from proteins.models import Lineage, Organism, SnapGenePlasmid # noqa: F401 + from proteins.models import ( # noqa: F401 + BleachMeasurement, + Excerpt, + Lineage, + Organism, + OSERMeasurement, + ProteinCollection, + SnapGenePlasmid, + StateTransition, + ) # this is a hack to allow for reversions of proteins to work with Null chromophores @@ -216,7 +225,7 @@ class CofactorChoices(models.TextChoices): # Relations parent_organism_id: int | None - parent_organism = models.ForeignKey["Organism"]( + parent_organism = models.ForeignKey["Organism | None"]( "Organism", related_name="proteins", verbose_name="Parental organism", @@ -227,7 +236,7 @@ class CofactorChoices(models.TextChoices): ) primary_reference_id: int | None - primary_reference = models.ForeignKey["Reference"]( + primary_reference = models.ForeignKey["Reference | None"]( Reference, related_name="primary_proteins", verbose_name="Primary Reference", @@ -236,7 +245,10 @@ class CofactorChoices(models.TextChoices): on_delete=models.SET_NULL, help_text="Preferably the publication that introduced the protein", ) - references = models.ManyToManyField(Reference, related_name="proteins", blank=True) + if TYPE_CHECKING: + references = models.ManyToManyField["Reference", "Protein"] + else: + references = models.ManyToManyField(Reference, related_name="proteins", blank=True) default_state_id: int | None default_state = models.ForeignKey["State | None"]( @@ -251,6 +263,11 @@ class CofactorChoices(models.TextChoices): states = RelatedManager["State"]() lineage = RelatedManager["Lineage"]() snapgene_plasmids = models.ManyToManyField["SnapGenePlasmid", "Protein"] + default_for: models.QuerySet["State"] + transitions: models.QuerySet[StateTransition] + oser_measurements: models.QuerySet[OSERMeasurement] + collection_memberships: models.QuerySet[ProteinCollection] + excerpts: models.QuerySet[Excerpt] else: snapgene_plasmids = models.ManyToManyField( "SnapGenePlasmid", @@ -560,6 +577,10 @@ class State(Fluorophore): # TODO: rename to ProteinState if TYPE_CHECKING: transitions = models.ManyToManyField["State", "State"] + transition_state: models.QuerySet["State"] + transitions_from: models.QuerySet[StateTransition] + transitions_to: models.QuerySet[StateTransition] + bleach_measurements: models.QuerySet[BleachMeasurement] else: transitions = models.ManyToManyField( "State", diff --git a/backend/proteins/models/snapgene.py b/backend/proteins/models/snapgene.py index 9ae61aec0..54083c392 100644 --- a/backend/proteins/models/snapgene.py +++ b/backend/proteins/models/snapgene.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from django.db import models +if TYPE_CHECKING: + from proteins.models import Protein + class SnapGenePlasmid(models.Model): """Represents a plasmid from SnapGene's database. @@ -17,6 +22,9 @@ class SnapGenePlasmid(models.Model): size = models.IntegerField(null=True, blank=True, help_text="Size in base pairs") topology = models.CharField(max_length=50, blank=True) + if TYPE_CHECKING: + proteins: models.QuerySet[Protein] + class Meta: ordering = ["name"] verbose_name = "SnapGene Plasmid" diff --git a/backend/proteins/models/spectrum.py b/backend/proteins/models/spectrum.py index 1a4bfde67..0340f3387 100644 --- a/backend/proteins/models/spectrum.py +++ b/backend/proteins/models/spectrum.py @@ -61,10 +61,20 @@ def d3_dicts(self): class Camera(SpectrumOwner, Product): manufacturer = models.CharField(max_length=128, blank=True) + if TYPE_CHECKING: + spectrum: "Spectrum" + microscopes: models.QuerySet + optical_configs: models.QuerySet + class Light(SpectrumOwner, Product): manufacturer = models.CharField(max_length=128, blank=True) + if TYPE_CHECKING: + spectrum: "Spectrum" + microscopes: models.QuerySet + optical_configs: models.QuerySet + def sorted_ex2em(filterset): def _sort(stype): @@ -343,7 +353,7 @@ class Spectrum(Authorable, StatusModel, TimeStampedModel, AdminURLMixin): related_name="spectra", ) owner_filter_id: int | None - owner_filter = models.OneToOneField["Filter| None"]( + owner_filter = models.OneToOneField["Filter | None"]( "Filter", null=True, blank=True, @@ -366,6 +376,7 @@ class Spectrum(Authorable, StatusModel, TimeStampedModel, AdminURLMixin): on_delete=models.CASCADE, related_name="spectrum", ) + reference_id: int | None reference = models.ForeignKey["Reference | None"]( Reference, null=True, @@ -653,6 +664,12 @@ class Filter(SpectrumOwner, Product): blank=True, null=True, validators=[MinValueValidator(0), MaxValueValidator(90)] ) + if TYPE_CHECKING: + from proteins.models import OpticalConfig + + spectrum: "Spectrum" + optical_configs: models.QuerySet[OpticalConfig] + class Meta: ordering = ["bandcenter"] diff --git a/backend/proteins/util/maintain.py b/backend/proteins/util/maintain.py index 97c3ce9f7..d7cf694be 100644 --- a/backend/proteins/util/maintain.py +++ b/backend/proteins/util/maintain.py @@ -40,7 +40,7 @@ def suggested_switch_type(protein: Protein) -> str | None: else: darkstates = protein.states.filter(is_dark=True).count() if not n_transitions: - return protein.SwitchingChoices.OTHER + return str(protein.SwitchingChoices.OTHER) elif nstates == 2: # 2 transitions with unique from_states if hasattr(protein, "nfrom"): @@ -48,15 +48,15 @@ def suggested_switch_type(protein: Protein) -> str | None: else: nfrom = len(set(protein.transitions.values_list("from_state", flat=True))) if nfrom >= 2: - return protein.SwitchingChoices.PHOTOSWITCHABLE + return str(protein.SwitchingChoices.PHOTOSWITCHABLE) if darkstates == 0: - return protein.SwitchingChoices.PHOTOCONVERTIBLE + return str(protein.SwitchingChoices.PHOTOCONVERTIBLE) if darkstates == 1: - return protein.SwitchingChoices.PHOTOACTIVATABLE + return str(protein.SwitchingChoices.PHOTOACTIVATABLE) if darkstates > 1: return None elif nstates > 2: - return protein.SwitchingChoices.MULTIPHOTOCHROMIC + return str(protein.SwitchingChoices.MULTIPHOTOCHROMIC) def validate_switch_type(protein: Protein) -> bool: diff --git a/backend/references/models.py b/backend/references/models.py index 7fcf54914..19b0eb44e 100644 --- a/backend/references/models.py +++ b/backend/references/models.py @@ -1,5 +1,8 @@ +from __future__ import annotations + import re from datetime import UTC, datetime +from typing import TYPE_CHECKING from django.contrib.auth import get_user_model from django.core.exceptions import ObjectDoesNotExist @@ -27,7 +30,11 @@ class Author(TimeStampedModel): family = models.CharField(max_length=80) given = models.CharField(max_length=80) initials = models.CharField(max_length=10) - publications = models.ManyToManyField("Reference", through="ReferenceAuthor") + if TYPE_CHECKING: + publications = models.ManyToManyField["Reference", "Author"] + referenceauthor_set: models.QuerySet[ReferenceAuthor] + else: + publications = models.ManyToManyField("Reference", through="ReferenceAuthor") @property def protein_contributions(self): @@ -90,17 +97,41 @@ class Reference(TimeStampedModel): ], help_text="YYYY", ) - authors = models.ManyToManyField("Author", through="ReferenceAuthor") + if TYPE_CHECKING: + from proteins.models import ( + BleachMeasurement, + Excerpt, + FluorescenceMeasurement, + Lineage, + OSERMeasurement, + Protein, + Spectrum, + ) + + authors = models.ManyToManyField["Author", "Reference"] + referenceauthor_set: models.QuerySet[ReferenceAuthor] + primary_proteins: models.QuerySet[Protein] + proteins: models.QuerySet[Protein] + excerpts: models.QuerySet[Excerpt] + spectra: models.QuerySet[Spectrum] + bleach_measurements: models.QuerySet[BleachMeasurement] + oser_measurements: models.QuerySet[OSERMeasurement] + lineages: models.QuerySet[Lineage] + fluorescencemeasurement_set: models.QuerySet[FluorescenceMeasurement] + else: + authors = models.ManyToManyField("Author", through="ReferenceAuthor") summary = models.CharField(max_length=512, blank=True, help_text="Brief summary of findings") - created_by = models.ForeignKey( + created_by_id: int | None + created_by = models.ForeignKey[User | None]( User, related_name="reference_author", blank=True, null=True, on_delete=models.CASCADE, ) - updated_by = models.ForeignKey( + updated_by_id: int | None + updated_by = models.ForeignKey[User | None]( User, related_name="reference_modifier", blank=True, @@ -199,8 +230,10 @@ def url(self): class ReferenceAuthor(models.Model): - reference = models.ForeignKey(Reference, on_delete=models.CASCADE) - author = models.ForeignKey(Author, on_delete=models.CASCADE) + reference_id: int + reference = models.ForeignKey[Reference](Reference, on_delete=models.CASCADE) + author_id: int + author = models.ForeignKey[Author](Author, on_delete=models.CASCADE) author_idx = models.PositiveSmallIntegerField() class Meta: From 4f95c3bf52f742f8246f2700f6106fbc89dd4162 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 21 Nov 2025 23:02:05 -0500 Subject: [PATCH 12/57] fix graphql --- backend/proteins/models/dye.py | 3 ++ backend/proteins/models/fluorophore.py | 13 +++++ backend/proteins/models/protein.py | 3 +- backend/proteins/models/spectrum.py | 8 +-- backend/proteins/schema/types.py | 53 ++++++++++++++++++- backend/tests/test_api/test_graphql_schema.py | 13 +++-- 6 files changed, 80 insertions(+), 13 deletions(-) diff --git a/backend/proteins/models/dye.py b/backend/proteins/models/dye.py index 6977a9364..152c207cf 100644 --- a/backend/proteins/models/dye.py +++ b/backend/proteins/models/dye.py @@ -112,6 +112,9 @@ class DyeState(Fluorophore): default=False, help_text="If True, this is the default state shown on the dye summary card." ) + if TYPE_CHECKING: + fluorophore_ptr: Fluorophore # added by Django MTI + def save(self, *args, **kwargs): self.entity_type = "dye" super().save(*args, **kwargs) diff --git a/backend/proteins/models/fluorophore.py b/backend/proteins/models/fluorophore.py index fd94f3cd4..4c66e9945 100644 --- a/backend/proteins/models/fluorophore.py +++ b/backend/proteins/models/fluorophore.py @@ -12,10 +12,12 @@ from proteins.models import ( # noqa: F401 Dye, + DyeState, FluorescenceMeasurement, OcFluorEff, Protein, Spectrum, + State, ) @@ -65,6 +67,10 @@ class EntityTypes(models.TextChoices): measurements = RelatedManager["FluorescenceMeasurement"]() oc_effs = RelatedManager["OcFluorEff"]() + # these are not *guaranteed* to exist, they come from Django MTI + dyestate: "DyeState" + state: "State" + class Meta: indexes = [ models.Index(fields=["ex_max"]), @@ -74,6 +80,13 @@ class Meta: def __str__(self): return self.label + def as_subclass(self) -> "Self": + """Downcast to the specific subclass instance.""" + for subclass_name in ["dyestate", "state"]: + if hasattr(self, subclass_name): + return getattr(self, subclass_name) + return self # Fallback to parent if no child found + def rebuild_attributes(self): """The Compositing Engine. diff --git a/backend/proteins/models/protein.py b/backend/proteins/models/protein.py index 742d34a34..82c7d0605 100644 --- a/backend/proteins/models/protein.py +++ b/backend/proteins/models/protein.py @@ -262,12 +262,12 @@ class CofactorChoices(models.TextChoices): if TYPE_CHECKING: states = RelatedManager["State"]() lineage = RelatedManager["Lineage"]() - snapgene_plasmids = models.ManyToManyField["SnapGenePlasmid", "Protein"] default_for: models.QuerySet["State"] transitions: models.QuerySet[StateTransition] oser_measurements: models.QuerySet[OSERMeasurement] collection_memberships: models.QuerySet[ProteinCollection] excerpts: models.QuerySet[Excerpt] + snapgene_plasmids = models.ManyToManyField["SnapGenePlasmid", "Protein"] else: snapgene_plasmids = models.ManyToManyField( "SnapGenePlasmid", @@ -581,6 +581,7 @@ class State(Fluorophore): # TODO: rename to ProteinState transitions_from: models.QuerySet[StateTransition] transitions_to: models.QuerySet[StateTransition] bleach_measurements: models.QuerySet[BleachMeasurement] + fluorophore_ptr: Fluorophore # added by Django MTI else: transitions = models.ManyToManyField( "State", diff --git a/backend/proteins/models/spectrum.py b/backend/proteins/models/spectrum.py index 0340f3387..86474ae69 100644 --- a/backend/proteins/models/spectrum.py +++ b/backend/proteins/models/spectrum.py @@ -26,7 +26,7 @@ from references.models import Reference if TYPE_CHECKING: - from proteins.models import Fluorophore # noqa: F401 + from proteins.models import Fluorophore logger = logging.getLogger(__name__) @@ -468,7 +468,7 @@ def clean(self): raise ValidationError(errors) @property - def owner_set(self): + def owner_set(self) -> list["Fluorophore | Filter | Light | Camera | None"]: return [ self.owner_fluor, self.owner_filter, @@ -477,12 +477,12 @@ def owner_set(self): ] @property - def owner(self): + def owner(self) -> "Fluorophore | Filter | Light | Camera | None": return next((x for x in self.owner_set if x), None) # raise AssertionError("No owner is set") @property - def name(self): + def name(self) -> str: # this method allows the protein name to have changed in the meantime if self.owner_fluor: # Check if it's a State (ProteinState) by checking for protein attribute diff --git a/backend/proteins/schema/types.py b/backend/proteins/schema/types.py index 8e665d9a9..3568b0f48 100644 --- a/backend/proteins/schema/types.py +++ b/backend/proteins/schema/types.py @@ -134,6 +134,23 @@ class SpectrumOwnerInterface(graphene.Interface): typ = graphene.String() slug = graphene.String() + @classmethod + def resolve_type(cls, instance, info): + from proteins.models import dye as dye_models + + # Check DyeState first since it's a Fluorophore + if isinstance(instance, dye_models.DyeState): + return DyeState + elif isinstance(instance, models.State): + return State + elif isinstance(instance, models.Camera): + return Camera + elif isinstance(instance, models.Filter): + return Filter + elif isinstance(instance, models.Light): + return Light + return None + def resolve_typ(self, info): return self.__class__.__name__.lower() @@ -147,6 +164,10 @@ class Meta: model = models.Camera fields = "__all__" + @classmethod + def is_type_of(cls, root, info): + return isinstance(root, models.Camera) + class Dye(DjangoObjectType): class Meta: @@ -162,6 +183,12 @@ class Meta: model = models.dye.DyeState fields = "__all__" + @classmethod + def is_type_of(cls, root, info): + from proteins.models import dye as dye_models + + return isinstance(root, dye_models.DyeState) + class Filter(DjangoObjectType): class Meta: @@ -169,6 +196,10 @@ class Meta: model = models.Filter fields = "__all__" + @classmethod + def is_type_of(cls, root, info): + return isinstance(root, models.Filter) + class Light(DjangoObjectType): class Meta: @@ -176,6 +207,10 @@ class Meta: model = models.Light fields = "__all__" + @classmethod + def is_type_of(cls, root, info): + return isinstance(root, models.Light) + class State(gdo.OptimizedDjangoObjectType): protein = graphene.Field(Protein) @@ -185,6 +220,10 @@ class Meta: model = models.State fields = "__all__" + @classmethod + def is_type_of(cls, root, info): + return isinstance(root, models.State) + @gdo.resolver_hints(select_related=("protein",), only=("protein",)) def resolve_protein(self, info, **kwargs): return self.protein @@ -223,7 +262,19 @@ class Meta: ), ) def resolve_owner(self, info, **kwargs): - return self.owner + owner = self.owner + if owner is None: + return None + + # Handle Django MTI - downcast Fluorophore to its specific subclass + # Check if it's a DyeState or State by trying to access the child instance + if hasattr(owner, "dyestate"): + return owner.dyestate + elif hasattr(owner, "state"): + return owner.state + + # For non-Fluorophore owners (Camera, Filter, Light), return as-is + return owner def resolve_color(self, info, **kwargs): return self.color() diff --git a/backend/tests/test_api/test_graphql_schema.py b/backend/tests/test_api/test_graphql_schema.py index 2fba979da..be1e07da9 100644 --- a/backend/tests/test_api/test_graphql_schema.py +++ b/backend/tests/test_api/test_graphql_schema.py @@ -55,7 +55,7 @@ ... on State { ...FluorophoreParts } - ... on Dye { + ... on DyeState { ...FluorophoreParts } } @@ -80,13 +80,12 @@ def setUp(self): self.microscope = models.Microscope.objects.create() self.protein = models.Protein.objects.create(name="test") self.optical_config = models.OpticalConfig.objects.create(microscope=self.microscope) - self.dye = models.DyeState.objects.get_or_create(name="test-dye", slug="test-dye")[0] + # Create a Dye (parent entity) first + self.dye = models.Dye.objects.get_or_create(name="test-dye", slug="test-dye")[0] # Create a DyeState (Fluorophore) for the dye from proteins.models.dye import DyeState - self.dye_state = DyeState.objects.create( - dye=self.dye, name="test state", slug="test-dye-state", label="test-dye" - ) + self.dye_state = DyeState.objects.create(dye=self.dye, name="test state", label="test-dye") self.spectrum = models.Spectrum.objects.get_or_create( category=models.Spectrum.DYE, subtype=models.Spectrum.EM, @@ -143,7 +142,7 @@ def test_spectrum(self): response = self.query(SPECTRUM, op_name="Spectrum", variables={"id": self.spectrum.id}) self.assertResponseNoErrors(response) content = json.loads(response.content) - self.assertEqual(content["data"]["spectrum"]["owner"]["id"], str(self.dye.id)) + self.assertEqual(content["data"]["spectrum"]["owner"]["id"], str(self.dye_state.id)) self.assertEqual( content["data"]["spectrum"]["category"].upper(), str(self.spectrum.category.upper()), @@ -170,7 +169,7 @@ def test_spectra(self): content = json.loads(response.content) last_spectrum = content["data"]["spectra"][-1] self.assertEqual(last_spectrum["id"], str(self.spectrum.id)) - self.assertEqual(last_spectrum["owner"]["name"], self.dye.name) + self.assertEqual(last_spectrum["owner"]["name"], self.dye_state.label) def test_protein(self): response = self.query( From c5bb4f667537c3b906da95891ce6451837d62b5c Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 21 Nov 2025 23:03:14 -0500 Subject: [PATCH 13/57] just test update --- justfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/justfile b/justfile index b322adfca..ea258087d 100644 --- a/justfile +++ b/justfile @@ -58,7 +58,7 @@ snapshots-update: snapshots-test: uv run pytest backend/tests_e2e/ --visual-snapshots -n 4 -test: test-py test-e2e +test: test-py # clean up all virtual environments, caches, and build artifacts clean-static: From e9c343c2283110baa334152d141acfe80d85e709 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 21 Nov 2025 23:04:00 -0500 Subject: [PATCH 14/57] remove old --- backend/proteins/models/_old_protein.py | 637 ------------------------ backend/proteins/models/_old_state.py | 243 --------- 2 files changed, 880 deletions(-) delete mode 100644 backend/proteins/models/_old_protein.py delete mode 100644 backend/proteins/models/_old_state.py diff --git a/backend/proteins/models/_old_protein.py b/backend/proteins/models/_old_protein.py deleted file mode 100644 index 1d8f732ce..000000000 --- a/backend/proteins/models/_old_protein.py +++ /dev/null @@ -1,637 +0,0 @@ -import contextlib -import datetime -import io -import json -import os -import re -import sys -from random import choices -from subprocess import PIPE, run -from typing import TYPE_CHECKING - -from django.contrib.auth import get_user_model -from django.contrib.postgres.fields import ArrayField -from django.contrib.postgres.search import TrigramSimilarity -from django.core.exceptions import ObjectDoesNotExist, ValidationError -from django.db import models -from django.db.models import Count, F, Func, Q, Value -from django.urls import reverse -from django.utils.text import slugify -from model_utils import Choices -from model_utils.managers import QueryManager -from model_utils.models import StatusModel, TimeStampedModel -from reversion.models import Version - -from favit.models import Favorite -from fpseq import FPSeq -from references.models import Reference - -from .. import util -from ..util.helpers import get_base_name, get_color_group, mless, spectra_fig -from ..validators import protein_sequence_validator, validate_uniprot -from .collection import ProteinCollection -from .mixins import Authorable -from .spectrum import Spectrum - -if TYPE_CHECKING: - from proteins.models import SnapGenePlasmid, State # noqa: F401 - -User = get_user_model() - - -def prot_uuid(k=5, opts="ABCDEFGHJKLMNOPQRSTUVWXYZ123456789"): - i = "".join(choices(opts, k=k)) - try: - Protein.objects.get(uuid=i) - except Protein.DoesNotExist: - return i - else: - return prot_uuid(k, opts) - - -def findname(name): - queries = [ - {"name__iexact": name}, - {"aliases__icontains": name}, - # {'name__icontains': name}, - {"name__iexact": re.sub(r" \((Before|Planar|wild).*", "", name)}, - {"aliases__icontains": re.sub(r" \((Before|Planar|wild).*", "", name)}, - # {'name__icontains': re.sub(r' \((Before|Planar).*', '', name)}, - {"name__iexact": name.strip("1")}, - # {'name__icontains': name.strip('1')}, - ] - for query in queries: - with contextlib.suppress(Exception): - return Protein.objects.get(**query) - return None - - -class ProteinQuerySet(models.QuerySet): - def fasta(self): - seqs = list(self.exclude(seq__isnull=True).values("uuid", "name", "seq")) - for s in seqs: - s["name"] = s["name"].replace("\u03b1", "-alpha").replace("β", "-beta") - return io.StringIO("\n".join([">{uuid} {name}\n{seq}".format(**s) for s in seqs])) - - def to_tree(self, output="clw"): - fasta = self.fasta() - binary = "bin/muscle_" + ("osx" if sys.platform == "darwin" else "nix") - cmd = [binary] - # faster - cmd += ["-maxiters", "2", "-diags", "-quiet", f"-{output}"] - # make tree - cmd += ["-cluster", "neighborjoining", "-tree2", "tree.phy"] - result = run(cmd, input=fasta.read(), stdout=PIPE, encoding="ascii") - with open("tree.phy") as handle: - newick = handle.read().replace("\n", "") - os.remove("tree.phy") - return result.stdout, newick - - -class ProteinManager(models.Manager): - def deep_get(self, name): - slug = slugify(name) - with contextlib.suppress(Protein.DoesNotExist): - return self.get(slug=slug) - aliases_lower = Func(Func(F("aliases"), function="unnest"), function="LOWER") - remove_space = Func(aliases_lower, Value(" "), Value("-"), function="replace") - final = Func(remove_space, Value("."), Value(""), function="replace") - D = dict(Protein.objects.annotate(aka=final).values_list("aka", "id")) - if slug in D: - return self.get(id=D[slug]) - else: - raise Protein.DoesNotExist("Protein matching query does not exist.") - - def get_queryset(self): - return ProteinQuerySet(self.model, using=self._db) - - def with_counts(self): - from django.db import connection - - with connection.cursor() as cursor: - cursor.execute( - """ - SELECT p.id, p.name, p.slug, COUNT(*) - FROM proteins_protein p, proteins_state s - WHERE p.id = s.protein_id - GROUP BY p.id, p.name, p.slug - ORDER BY COUNT(*) DESC""" - ) - result_list = [] - for row in cursor.fetchall(): - p = self.model(id=row[0], name=row[1], slug=row[2]) - p.num_states = row[3] - result_list.append(p) - return result_list - - def annotated(self): - return self.get_queryset().annotate(Count("states"), Count("transitions")) - - def with_spectra(self, twoponly=False): - qs = self.get_queryset().filter(states__spectra__isnull=False).distinct() - if not twoponly: - # hacky way to remove 2p only spectra - qs = qs.annotate(stypes=Count("states__spectra__subtype")).filter(stypes__gt=1) - return qs - - def find_similar(self, name, similarity=0.2): - return ( - self.get_queryset() - .annotate(similarity=TrigramSimilarity("name", name)) - .filter(similarity__gt=similarity) - .order_by("-similarity") - ) - - -class SequenceField(models.CharField): - def __init__(self, *args, **kwargs): - kwargs["max_length"] = 1024 - kwargs["validators"] = [protein_sequence_validator] - super().__init__(*args, **kwargs) - - def deconstruct(self): - name, path, args, kwargs = super().deconstruct() - del kwargs["max_length"] - del kwargs["validators"] - return name, path, args, kwargs - - def from_db_value(self, value, expression, connection): - # Skip validation for database values - they're already validated - return FPSeq(value, validate=False) if value else None - - def to_python(self, value): - if isinstance(value, FPSeq): - return value - # New values should still be validated - return FPSeq(value, validate=True) if value else None - - def get_prep_value(self, value): - return str(value) if value else None - - -# this is a hack to allow for reversions of proteins to work with Null chromophores -# this makes sure that a None value is converted to an empty string -class _NonNullChar(models.CharField): - def to_python(self, value): - return "" if value is None else super().to_python(value) - - -class Protein(Authorable, StatusModel, TimeStampedModel): - """Protein class to store individual proteins, each with a unique AA sequence and name""" - - STATUS = Choices("pending", "approved", "hidden") - - MONOMER = "m" - DIMER = "d" - TANDEM_DIMER = "td" - WEAK_DIMER = "wd" - TETRAMER = "t" - AGG_CHOICES = ( - (MONOMER, "Monomer"), - (DIMER, "Dimer"), - (TANDEM_DIMER, "Tandem dimer"), - (WEAK_DIMER, "Weak dimer"), - (TETRAMER, "Tetramer"), - ) - - BASIC = "b" - PHOTOACTIVATABLE = "pa" - PHOTOSWITCHABLE = "ps" - PHOTOCONVERTIBLE = "pc" - MULTIPHOTOCHROMIC = "mp" - TIMER = "t" - OTHER = "o" - SWITCHING_CHOICES = ( - (BASIC, "Basic"), - (PHOTOACTIVATABLE, "Photoactivatable"), - (PHOTOSWITCHABLE, "Photoswitchable"), - (PHOTOCONVERTIBLE, "Photoconvertible"), - (MULTIPHOTOCHROMIC, "Multi-photochromic"), # both convertible and switchable - (OTHER, "Multistate"), - (TIMER, "Timer"), - ) - - BILIRUBIN = "br" - BILIVERDIN = "bv" - FLAVIN = "fl" - PHYCOCYANOBILIN = "pc" - RIBITYL_LUMAZINE = "rl" - COFACTOR_CHOICES = ( - (BILIRUBIN, "Bilirubin"), - (BILIVERDIN, "Biliverdin"), - (FLAVIN, "Flavin"), - (PHYCOCYANOBILIN, "Phycocyanobilin"), - (RIBITYL_LUMAZINE, "ribityl-lumazine"), - ) - - # Attributes - # uuid = models.UUIDField(default=uuid_lib.uuid4, editable=False, unique=True) # for API - uuid = models.CharField( - max_length=5, - default=prot_uuid, - editable=False, - unique=True, - db_index=True, - verbose_name="FPbase ID", - ) - name = models.CharField(max_length=128, help_text="Name of the fluorescent protein", db_index=True) - slug = models.SlugField(max_length=64, unique=True, help_text="URL slug for the protein") # for generating urls - base_name = models.CharField(max_length=128) # easily searchable "family" name - aliases = ArrayField(models.CharField(max_length=200), blank=True, null=True) - chromophore = _NonNullChar(max_length=5, blank=True, default="") - seq_validated = models.BooleanField(default=False, help_text="Sequence has been validated by a moderator") - # seq must be nullable because of uniqueness contraints - seq = SequenceField( - unique=True, - blank=True, - null=True, - verbose_name="Sequence", - help_text="Amino acid sequence (IPG ID is preferred)", - ) - seq_comment = models.CharField( - max_length=512, - blank=True, - help_text="if necessary, comment on source of sequence", - ) - - pdb = ArrayField( - models.CharField(max_length=4), - blank=True, - null=True, - verbose_name="Protein DataBank IDs", - ) - genbank = models.CharField( - max_length=12, - null=True, - blank=True, - unique=True, - verbose_name="Genbank Accession", - help_text="NCBI Genbank Accession", - ) - uniprot = models.CharField( - max_length=10, - null=True, - blank=True, - unique=True, - verbose_name="UniProtKB Accession", - validators=[validate_uniprot], - ) - ipg_id = models.CharField( - max_length=12, - null=True, - blank=True, - unique=True, - verbose_name="IPG ID", - help_text="Identical Protein Group ID at Pubmed", - ) # identical protein group uid - mw = models.FloatField(null=True, blank=True, help_text="Molecular Weight") # molecular weight - agg = models.CharField( - max_length=2, - choices=AGG_CHOICES, - blank=True, - verbose_name="Oligomerization", - help_text="Oligomerization tendency", - ) - oser = models.FloatField(null=True, blank=True, help_text="OSER score") # molecular weight - switch_type = models.CharField( - max_length=2, - choices=SWITCHING_CHOICES, - blank=True, - default="b", - verbose_name="Switching Type", - help_text="Photoswitching type (basic if none)", - ) - blurb = models.TextField(max_length=512, blank=True, help_text="Brief descriptive blurb") - cofactor = models.CharField( - max_length=2, - choices=COFACTOR_CHOICES, - blank=True, - help_text="Required for fluorescence", - ) - - # Relations - parent_organism = models.ForeignKey( - "Organism", - related_name="proteins", - verbose_name="Parental organism", - on_delete=models.SET_NULL, - blank=True, - null=True, - help_text="Organism from which the protein was engineered", - ) - primary_reference = models.ForeignKey( - Reference, - related_name="primary_proteins", - verbose_name="Primary Reference", - blank=True, - null=True, - on_delete=models.SET_NULL, - help_text="Preferably the publication that introduced the protein", - ) # usually, the original paper that published the protein - references = models.ManyToManyField( - Reference, related_name="proteins", blank=True - ) # all papers that reference the protein - # FRET_partner = models.ManyToManyField('self', symmetrical=False, through='FRETpair', blank=True) - - default_state_id: int | None - default_state = models.ForeignKey["State | None"]( - "State", - related_name="default_for", - blank=True, - null=True, - on_delete=models.SET_NULL, - ) - - if TYPE_CHECKING: - snapgene_plasmids = models.ManyToManyField["SnapGenePlasmid", "Protein"] - else: - snapgene_plasmids = models.ManyToManyField( - "SnapGenePlasmid", - related_name="proteins", - blank=True, - help_text="Associated SnapGene plasmids", - ) - - # __original_ipg_id = None - - # managers - objects = ProteinManager() - visible = QueryManager(~Q(status="hidden")) - - def mutations_from_root(self): - try: - root = self.lineage.get_root() - if root.protein.seq and self.seq: - return root.protein.seq.mutations_to(self.seq) - except ObjectDoesNotExist: - return None - - @property - def mless(self): - return mless(self.name) - - @property - def description(self): - return util.long_blurb(self) - - @property - def _base_name(self): - '''return core name of protein, stripping prefixes like "m" or "Tag"''' - return get_base_name(self.name) - - @property - def versions(self): - return Version.objects.get_for_object(self) - - def last_approved_version(self): - if self.status == "approved": - return self - try: - return ( - Version.objects.get_for_object(self).filter(serialized_data__contains='"status": "approved"').first() - ) - except Exception: - return None - - @property - def additional_references(self): - return self.references.exclude(id=self.primary_reference_id).order_by("-year") - - @property - def em_css(self): - if self.states.count() > 1: - from collections import OrderedDict - - stops = OrderedDict({st.emhex: "" for st in self.states.all()}) - bgs = [] - stepsize = int(100 / (len(stops) + 1)) - sub = 0 - for i, _hex in enumerate(stops): - if _hex == "#000": - sub = 18 - bgs.append(f"{_hex} {(i + 1) * stepsize - sub}%") - return f"linear-gradient(90deg, {', '.join(bgs)})" - elif self.default_state: - return self.default_state.emhex - else: - return "repeating-linear-gradient(-45deg,#333,#333 8px,#444 8px,#444 16px);" - - @property - def em_svg(self): - if self.states.count() <= 1: - return self.default_state.emhex if self.default_state else "?" - stops = [st.emhex for st in self.states.all()] - stepsize = int(100 / (len(stops) + 1)) - svgdef = "linear:" - for i, color in enumerate(stops): - perc = (i + 1.0) * stepsize - if color == "#000": - perc *= 0.2 - svgdef += f'' - return svgdef - - @property - def color(self): - try: - return get_color_group(self.default_state.ex_max, self.default_state.em_max)[0] # pyright: ignore - except Exception: - return "" - - # Methods - def __str__(self): - return self.name - - def get_absolute_url(self): - return reverse("proteins:protein-detail", args=[self.slug]) - - def has_default(self): - return bool(self.default_state) - - def mutations_to(self, other, **kwargs): - if isinstance(other, Protein): - other = other.seq - return self.seq.mutations_to(other, **kwargs) if self.seq and other else None - - def mutations_from(self, other, **kwargs): - if isinstance(other, Protein): - other = other.seq - return other.seq.mutations_to(self.seq, **kwargs) if (self.seq and other) else None - - def has_spectra(self): - return any(state.has_spectra() for state in self.states.all()) - - def has_bleach_measurements(self): - return self.states.filter(bleach_measurements__isnull=False).exists() - - def d3_spectra(self): - spectra = [] - for state in self.states.all(): - spectra.extend(state.d3_dicts()) - return json.dumps(spectra) - - def spectra_img(self, fmt="svg", output=None, **kwargs): - spectra = list(Spectrum.objects.filter(owner_state__protein=self).exclude(subtype="2p")) - title = self.name if kwargs.pop("title", False) else None - if kwargs.get("twitter", False): - title = self.name - info = "" - if self.default_state: - info += f"Ex/Em λ: {self.default_state.ex_max}/{self.default_state.em_max}" - info += f"\nEC: {self.default_state.ext_coeff} QY: {self.default_state.qy}" - return spectra_fig(spectra, fmt, output, title=title, info=info, **kwargs) - - def set_default_state(self) -> bool: - # FIXME: should allow control of default states in form - # if only 1 state, make it the default state - if not self.default_state or self.default_state.is_dark: - if self.states.count() == 1 and not self.states.first().is_dark: - self.default_state = self.states.first() - # otherwise use farthest red non-dark state - elif self.states.count() > 1: - self.default_state = self.states.exclude(is_dark=True).order_by("-em_max").first() - return True - return False - - def clean(self): - errors = {} - # Don't allow basic switch_types to have more than one state. - # if self.switch_type == 'b' and self.states.count() > 1: - # errors.update({'switch_type': 'Basic (non photoconvertible) proteins ' - # 'cannot have more than one state.'}) - if self.pdb: - self.pdb = list(set(self.pdb)) - for item in self.pdb: - if Protein.objects.exclude(id=self.id).filter(pdb__contains=[item]).exists(): - p = Protein.objects.filter(pdb__contains=[item]).first() - errors["pdb"] = f"PDB ID {item} is already in use by protein {p.name}" - - if errors: - raise ValidationError(errors) - - def save(self, *args, **kwargs): - self.slug = slugify(self.name) - self.base_name = self._base_name - super().save(*args, **kwargs) - if self.set_default_state(): - super().save() - # self.__original_ipg_id = self.ipg_id - - # Meta - class Meta: - ordering = ["name"] - indexes = [ - models.Index(fields=["status"], name="protein_status_idx"), - ] - - def history(self, ignoreKeys=()): - from proteins.util.history import get_history - - return get_history(self, ignoreKeys) - - # ################################## - # for algolia index - - def is_visible(self): - return self.status != "hidden" - - def img_url(self): - if self.has_spectra(): - return ( - "https://www.fpbase.org" - + reverse("proteins:spectra-img", args=[self.slug]) - + ".png?xlabels=0&xlim=400,800" - ) - else: - return None - - def tags(self): - tags = [self.get_switch_type_display(), self.get_agg_display(), self.color] - return [i for i in tags if i] - - def date_published(self, norm=False): - d = self.primary_reference.date if self.primary_reference else None - if norm: - return (d.year - 1992) / (datetime.datetime.now(datetime.UTC).year - 1992) if d else 0 - return datetime.datetime.combine(d, datetime.datetime.min.time()) if d else None - - def n_faves(self, norm=False): - nf = Favorite.objects.for_model(Protein).filter(target_object_id=self.id).count() - if norm: - from collections import Counter - - mx = Counter(Favorite.objects.for_model(Protein).values_list("target_object_id", flat=True)).most_common(1) - mx = mx[0][1] if mx else 1 - return nf / mx - return nf - - def n_cols(self): - return ProteinCollection.objects.filter(proteins=self.id).count() - - def ga_views(self, period="month", norm=False): - from proteins.extrest.ga import cached_ga_popular - - try: - hits = cached_ga_popular()[period] - return next( - ( - rating / max(list(zip(*hits))[2]) if norm else rating - for slug, _name, rating in hits - if slug == self.slug - ), - 0, - ) - except Exception: - return 0 - - def switchType(self): - return self.get_switch_type_display() - - def _agg(self): - return self.get_agg_display() - - def url(self): - return self.get_absolute_url() - - def ex(self): - if not self.states.exists(): - return None - ex = [s.ex_max for s in self.states.all()] - return ex[0] if len(ex) == 1 else ex - - def em(self): - if not self.states.exists(): - return None - em = [s.em_max for s in self.states.all()] - return em[0] if len(em) == 1 else em - - def pka(self): - if not self.states.exists(): - return None - n = [s.pka for s in self.states.all()] - return n[0] if len(n) == 1 else n - - def ec(self): - if not self.states.exists(): - return None - n = [s.ext_coeff for s in self.states.all()] - return n[0] if len(n) == 1 else n - - def qy(self): - if not self.states.exists(): - return None - n = [s.qy for s in self.states.all()] - return n[0] if len(n) == 1 else n - - def rank(self): - # max rank is 1 - return ( - 0.5 * self.date_published(norm=True) + 0.6 * self.ga_views(norm=True) + 1.0 * self.n_faves(norm=True) - ) / 2.5 - - def local_brightness(self): - if self.states.exists(): - return max(s.local_brightness for s in self.states.all()) - - def first_author(self): - if self.primary_reference and self.primary_reference.first_author: - return self.primary_reference.first_author.family diff --git a/backend/proteins/models/_old_state.py b/backend/proteins/models/_old_state.py deleted file mode 100644 index 9b143c5f5..000000000 --- a/backend/proteins/models/_old_state.py +++ /dev/null @@ -1,243 +0,0 @@ -from django.contrib.contenttypes.fields import GenericRelation -from django.core.validators import MaxValueValidator, MinValueValidator -from django.db import models -from django.db.models import Avg -from django.utils.text import slugify - -from ..util.helpers import wave_to_hex -from .mixins import Product -from .spectrum import SpectrumOwner - - -class Fluorophore(SpectrumOwner): - ex_max = models.PositiveSmallIntegerField( - blank=True, - null=True, - validators=[MinValueValidator(300), MaxValueValidator(900)], - db_index=True, - ) - em_max = models.PositiveSmallIntegerField( - blank=True, - null=True, - validators=[MinValueValidator(300), MaxValueValidator(1000)], - db_index=True, - ) - - twop_ex_max = models.PositiveSmallIntegerField( - blank=True, - null=True, - verbose_name="Peak 2P excitation", - validators=[MinValueValidator(700), MaxValueValidator(1600)], - db_index=True, - ) - ext_coeff = models.IntegerField( - blank=True, - null=True, - verbose_name="Extinction Coefficient", - validators=[MinValueValidator(0), MaxValueValidator(300000)], - ) # extinction coefficient - twop_peakGM = models.FloatField( - null=True, - blank=True, - verbose_name="Peak 2P cross-section of S0->S1 (GM)", - validators=[MinValueValidator(0), MaxValueValidator(200)], - ) - qy = models.FloatField( - null=True, - blank=True, - verbose_name="Quantum Yield", - validators=[MinValueValidator(0), MaxValueValidator(1)], - ) # quantum yield - twop_qy = models.FloatField( - null=True, - blank=True, - verbose_name="2P Quantum Yield", - validators=[MinValueValidator(0), MaxValueValidator(1)], - ) # quantum yield - brightness = models.FloatField(null=True, blank=True, editable=False) - pka = models.FloatField( - null=True, - blank=True, - verbose_name="pKa", - validators=[MinValueValidator(2), MaxValueValidator(12)], - ) # pKa acid dissociation constant - lifetime = models.FloatField( - null=True, - blank=True, - help_text="Lifetime (ns)", - validators=[MinValueValidator(0), MaxValueValidator(20)], - ) # fluorescence lifetime in nanoseconds - emhex = models.CharField(max_length=7, blank=True) - exhex = models.CharField(max_length=7, blank=True) - is_dark = models.BooleanField( - default=False, - verbose_name="Dark State", - help_text="This state does not fluorescence", - ) - - class Meta: - abstract = True - - def save(self, *args, **kwargs): - if self.qy and self.ext_coeff: - self.brightness = float(round(self.ext_coeff * self.qy / 1000, 2)) - - self.emhex = "#000" if self.is_dark else wave_to_hex(self.em_max) - self.exhex = wave_to_hex(self.ex_max) - - super().save(*args, **kwargs) - - @property - def fluor_name(self): - if hasattr(self, "protein"): - return self.protein.name - return self.name - - @property - def abs_spectrum(self): - spect = [f for f in self.spectra.all() if f.subtype == "ab"] - if len(spect) > 1: - raise AssertionError(f"multiple ex spectra found for {self}") - if len(spect): - return spect[0] - return None - - @property - def ex_spectrum(self): - spect = [f for f in self.spectra.all() if f.subtype == "ex"] - if len(spect) > 1: - raise AssertionError(f"multiple ex spectra found for {self}") - if len(spect): - return spect[0] - return self.abs_spectrum - - @property - def em_spectrum(self): - spect = [f for f in self.spectra.all() if f.subtype == "em"] - if len(spect) > 1: - raise AssertionError(f"multiple em spectra found for {self}") - if len(spect): - return spect[0] - return self.abs_spectrum - - @property - def twop_spectrum(self): - spect = [f for f in self.spectra.all() if f.subtype == "2p"] - if len(spect) > 1: - raise AssertionError("multiple 2p spectra found") - if len(spect): - return spect[0] - return None - - @property - def bright_rel_egfp(self): - if self.brightness: - return self.brightness / 0.336 - return None - - @property - def stokes(self): - try: - return self.em_max - self.ex_max - except TypeError: - return None - - def has_spectra(self): - if any([self.ex_spectrum, self.em_spectrum]): - return True - return False - - def ex_band(self, height=0.7): - return self.ex_spectrum.width(height) - - def em_band(self, height=0.7): - return self.em_spectrum.width(height) - - def within_ex_band(self, value, height=0.7): - if self.has_spectra(): - minRange, maxRange = self.ex_band(height) - if minRange < value < maxRange: - return True - return False - - def within_em_band(self, value, height=0.7): - if self.has_spectra(): - minRange, maxRange = self.em_band(height) - if minRange < value < maxRange: - return True - return False - - def d3_dicts(self): - return [spect.d3dict() for spect in self.spectra.all()] - - -class FluorophoreManager(models.Manager): - def notdark(self): - return self.filter(is_dark=False) - - def with_spectra(self): - return self.get_queryset().filter(spectra__isnull=False).distinct() - - -class Dye(Fluorophore, Product): - objects = FluorophoreManager() - oc_eff = GenericRelation("OcFluorEff", related_query_name="dye") - - -class State(Fluorophore): - DEFAULT_NAME = "default" - - """ A class for the states that a given protein can be in - (including spectra and other state-dependent properties) """ - name = models.CharField(max_length=64, default=DEFAULT_NAME) # required - maturation = models.FloatField( - null=True, - blank=True, - help_text="Maturation time (min)", # maturation half-life in min - validators=[MinValueValidator(0), MaxValueValidator(1600)], - ) - # Relations - transitions = models.ManyToManyField( - "State", - related_name="transition_state", - verbose_name="State Transitions", - blank=True, - through="StateTransition", - ) # any additional papers that reference the protein - protein = models.ForeignKey( - "Protein", - related_name="states", - help_text="The protein to which this state belongs", - on_delete=models.CASCADE, - ) - oc_eff = GenericRelation("OcFluorEff", related_query_name="state") - - # Managers - objects = FluorophoreManager() - - class Meta: - verbose_name = "State" - unique_together = (("protein", "ex_max", "em_max", "ext_coeff", "qy"),) - - def __str__(self): - if self.name in (self.DEFAULT_NAME, "default"): - return str(self.protein.name) - return f"{self.protein.name} ({self.name})" - - def get_absolute_url(self): - return self.protein.get_absolute_url() - - def makeslug(self): - return f"{self.protein.slug}_{slugify(self.name)}" - - @property - def local_brightness(self): - """brightness relative to spectral neighbors. 1 = average""" - if not (self.em_max and self.brightness): - return 1 - B = State.objects.exclude(id=self.id).filter(em_max__around=self.em_max).aggregate(Avg("brightness")) - try: - v = round(self.brightness / B["brightness__avg"], 4) - except TypeError: - v = 1 - return v From bbdbf9d433b6b76efbeba48e72621b02fc3bb00b Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sat, 22 Nov 2025 11:38:01 -0500 Subject: [PATCH 15/57] migration and tests working --- backend/proteins/migrations/0001_initial.py | 1226 ++++++++++------- .../0002_auto_20180312_0156.py | 0 ...0002_update_ocfluoreff_to_use_direct_fk.py | 77 -- .../0003_protein_oser.py | 0 .../0004_auto_20180315_1323.py | 0 .../0005_auto_20180328_1614.py | 0 ...8_1614_squashed_0010_auto_20180501_1547.py | 0 .../0006_auto_20180401_2009.py | 0 .../0006_auto_20180512_0058.py | 0 .../0007_auto_20180403_0140.py | 0 .../0007_auto_20180513_2346.py | 0 .../0008_auto_20180430_0308.py | 0 .../0008_auto_20180515_1659.py | 0 .../0009_auto_20180430_0335.py | 0 .../0009_auto_20180525_1640.py | 0 .../0010_auto_20180501_1547.py | 0 .../0010_auto_20180525_1840.py | 0 .../0011_auto_20180525_2349.py | 0 .../0012_auto_20180708_1811.py | 0 .../0013_auto_20180718_1717.py | 0 .../0014_auto_20180718_2340.py | 0 .../0015_auto_20180720_1921.py | 0 .../0016_auto_20180722_1314.py | 0 .../0017_auto_20180722_1626.py | 0 .../0018_protein_cofactor.py | 0 .../0019_auto_20180723_2200.py | 0 .../0020_auto_20180729_0234.py | 0 .../0021_auto_20180804_0203.py | 0 .../0022_osermeasurement.py | 0 .../0023_spectrum_reference.py | 0 .../0024_auto_20181011_1659.py | 0 .../0025_auto_20181011_1715.py | 0 .../0026_bleachmeasurement_cell_type.py | 0 .../0027_auto_20181011_1754.py | 0 .../0028_auto_20181012_2011.py | 0 .../0029_auto_20181014_1241.py | 0 .../0030_lineage.py | 0 .../0031_auto_20181103_1531.py | 0 .../0032_auto_20181107_2015.py | 0 .../0033_auto_20181107_2119.py | 0 .../0034_lineage_rootmut.py | 0 .../0035_auto_20181110_0103.py | 0 .../0036_lineage_root_node.py | 0 .../0037_auto_20181205_2035.py | 0 .../0038_auto_20181205_2044.py | 0 .../0039_auto_20181206_0009.py | 0 .../0040_auto_20181210_0345.py | 0 .../0041_auto_20181216_1743.py | 0 .../0042_auto_20181216_1744.py | 0 .../0043_remove_excerpt_protein.py | 0 .../0044_auto_20181218_1310.py | 0 .../0045_dye_is_dark.py | 0 .../0046_auto_20190121_1341.py | 0 .../0047_auto_20190319_1525.py | 0 .../0048_change_protein_uuid.py | 0 .../0049_auto_20190323_1947.py | 0 .../0050_auto_20190714_1318.py | 0 ...r_bleachmeasurement_created_by_and_more.py | 0 .../0052_alter_protein_chromophore.py | 0 .../0053_alter_protein_chromophore.py | 0 ...microscope_cfg_calc_efficiency_and_more.py | 0 ...spectrum_status_spectrum_status_changed.py | 0 ...trum_spectrum_state_status_idx_and_more.py | 0 .../0057_add_status_index.py | 0 ...apgeneplasmid_protein_snapgene_plasmids.py | 0 .../0059_add_fluorophore_and_new_models.py | 212 +++ .../0060_migrate_data_from_old_schema.py | 348 +++++ .../migrations/0061_cleanup_old_schema.py | 67 + .../proteins/migrations_old/0001_initial.py | 799 ----------- 69 files changed, 1327 insertions(+), 1402 deletions(-) rename backend/proteins/{migrations_old => migrations}/0002_auto_20180312_0156.py (100%) delete mode 100644 backend/proteins/migrations/0002_update_ocfluoreff_to_use_direct_fk.py rename backend/proteins/{migrations_old => migrations}/0003_protein_oser.py (100%) rename backend/proteins/{migrations_old => migrations}/0004_auto_20180315_1323.py (100%) rename backend/proteins/{migrations_old => migrations}/0005_auto_20180328_1614.py (100%) rename backend/proteins/{migrations_old => migrations}/0005_auto_20180328_1614_squashed_0010_auto_20180501_1547.py (100%) rename backend/proteins/{migrations_old => migrations}/0006_auto_20180401_2009.py (100%) rename backend/proteins/{migrations_old => migrations}/0006_auto_20180512_0058.py (100%) rename backend/proteins/{migrations_old => migrations}/0007_auto_20180403_0140.py (100%) rename backend/proteins/{migrations_old => migrations}/0007_auto_20180513_2346.py (100%) rename backend/proteins/{migrations_old => migrations}/0008_auto_20180430_0308.py (100%) rename backend/proteins/{migrations_old => migrations}/0008_auto_20180515_1659.py (100%) rename backend/proteins/{migrations_old => migrations}/0009_auto_20180430_0335.py (100%) rename backend/proteins/{migrations_old => migrations}/0009_auto_20180525_1640.py (100%) rename backend/proteins/{migrations_old => migrations}/0010_auto_20180501_1547.py (100%) rename backend/proteins/{migrations_old => migrations}/0010_auto_20180525_1840.py (100%) rename backend/proteins/{migrations_old => migrations}/0011_auto_20180525_2349.py (100%) rename backend/proteins/{migrations_old => migrations}/0012_auto_20180708_1811.py (100%) rename backend/proteins/{migrations_old => migrations}/0013_auto_20180718_1717.py (100%) rename backend/proteins/{migrations_old => migrations}/0014_auto_20180718_2340.py (100%) rename backend/proteins/{migrations_old => migrations}/0015_auto_20180720_1921.py (100%) rename backend/proteins/{migrations_old => migrations}/0016_auto_20180722_1314.py (100%) rename backend/proteins/{migrations_old => migrations}/0017_auto_20180722_1626.py (100%) rename backend/proteins/{migrations_old => migrations}/0018_protein_cofactor.py (100%) rename backend/proteins/{migrations_old => migrations}/0019_auto_20180723_2200.py (100%) rename backend/proteins/{migrations_old => migrations}/0020_auto_20180729_0234.py (100%) rename backend/proteins/{migrations_old => migrations}/0021_auto_20180804_0203.py (100%) rename backend/proteins/{migrations_old => migrations}/0022_osermeasurement.py (100%) rename backend/proteins/{migrations_old => migrations}/0023_spectrum_reference.py (100%) rename backend/proteins/{migrations_old => migrations}/0024_auto_20181011_1659.py (100%) rename backend/proteins/{migrations_old => migrations}/0025_auto_20181011_1715.py (100%) rename backend/proteins/{migrations_old => migrations}/0026_bleachmeasurement_cell_type.py (100%) rename backend/proteins/{migrations_old => migrations}/0027_auto_20181011_1754.py (100%) rename backend/proteins/{migrations_old => migrations}/0028_auto_20181012_2011.py (100%) rename backend/proteins/{migrations_old => migrations}/0029_auto_20181014_1241.py (100%) rename backend/proteins/{migrations_old => migrations}/0030_lineage.py (100%) rename backend/proteins/{migrations_old => migrations}/0031_auto_20181103_1531.py (100%) rename backend/proteins/{migrations_old => migrations}/0032_auto_20181107_2015.py (100%) rename backend/proteins/{migrations_old => migrations}/0033_auto_20181107_2119.py (100%) rename backend/proteins/{migrations_old => migrations}/0034_lineage_rootmut.py (100%) rename backend/proteins/{migrations_old => migrations}/0035_auto_20181110_0103.py (100%) rename backend/proteins/{migrations_old => migrations}/0036_lineage_root_node.py (100%) rename backend/proteins/{migrations_old => migrations}/0037_auto_20181205_2035.py (100%) rename backend/proteins/{migrations_old => migrations}/0038_auto_20181205_2044.py (100%) rename backend/proteins/{migrations_old => migrations}/0039_auto_20181206_0009.py (100%) rename backend/proteins/{migrations_old => migrations}/0040_auto_20181210_0345.py (100%) rename backend/proteins/{migrations_old => migrations}/0041_auto_20181216_1743.py (100%) rename backend/proteins/{migrations_old => migrations}/0042_auto_20181216_1744.py (100%) rename backend/proteins/{migrations_old => migrations}/0043_remove_excerpt_protein.py (100%) rename backend/proteins/{migrations_old => migrations}/0044_auto_20181218_1310.py (100%) rename backend/proteins/{migrations_old => migrations}/0045_dye_is_dark.py (100%) rename backend/proteins/{migrations_old => migrations}/0046_auto_20190121_1341.py (100%) rename backend/proteins/{migrations_old => migrations}/0047_auto_20190319_1525.py (100%) rename backend/proteins/{migrations_old => migrations}/0048_change_protein_uuid.py (100%) rename backend/proteins/{migrations_old => migrations}/0049_auto_20190323_1947.py (100%) rename backend/proteins/{migrations_old => migrations}/0050_auto_20190714_1318.py (100%) rename backend/proteins/{migrations_old => migrations}/0051_alter_bleachmeasurement_created_by_and_more.py (100%) rename backend/proteins/{migrations_old => migrations}/0052_alter_protein_chromophore.py (100%) rename backend/proteins/{migrations_old => migrations}/0053_alter_protein_chromophore.py (100%) rename backend/proteins/{migrations_old => migrations}/0054_microscope_cfg_calc_efficiency_and_more.py (100%) rename backend/proteins/{migrations_old => migrations}/0055_spectrum_status_spectrum_status_changed.py (100%) rename backend/proteins/{migrations_old => migrations}/0056_spectrum_spectrum_state_status_idx_and_more.py (100%) rename backend/proteins/{migrations_old => migrations}/0057_add_status_index.py (100%) rename backend/proteins/{migrations_old => migrations}/0058_snapgeneplasmid_protein_snapgene_plasmids.py (100%) create mode 100644 backend/proteins/migrations/0059_add_fluorophore_and_new_models.py create mode 100644 backend/proteins/migrations/0060_migrate_data_from_old_schema.py create mode 100644 backend/proteins/migrations/0061_cleanup_old_schema.py delete mode 100644 backend/proteins/migrations_old/0001_initial.py diff --git a/backend/proteins/migrations/0001_initial.py b/backend/proteins/migrations/0001_initial.py index 304f1b81c..48d52dce1 100644 --- a/backend/proteins/migrations/0001_initial.py +++ b/backend/proteins/migrations/0001_initial.py @@ -1,625 +1,799 @@ -# Generated by Django 5.2.8 on 2025-11-21 02:22 +# Generated by Django 1.11.9 on 2018-02-13 01:08 + +import uuid import django.contrib.postgres.fields import django.core.validators import django.db.models.deletion import django.utils.timezone import model_utils.fields -import mptt.fields -import proteins.models._sequence_field -import proteins.models.lineage -import proteins.models.mixins -import proteins.models.protein -import proteins.models.spectrum -import proteins.util.helpers from django.conf import settings from django.contrib.postgres.operations import TrigramExtension from django.db import migrations, models +import proteins.fields +import proteins.validators -class Migration(migrations.Migration): +class Migration(migrations.Migration): initial = True dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - ('references', '0008_alter_reference_year'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("references", "0001_initial"), ] operations = [ TrigramExtension(), migrations.CreateModel( - name='Fluorophore', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('ex_max', models.PositiveSmallIntegerField(blank=True, db_index=True, help_text='Excitation maximum (nm)', null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(900)])), - ('em_max', models.PositiveSmallIntegerField(blank=True, db_index=True, help_text='Emission maximum (nm)', null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1000)])), - ('emhex', models.CharField(blank=True, max_length=7)), - ('exhex', models.CharField(blank=True, max_length=7)), - ('ext_coeff', models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(300000)], verbose_name='Extinction Coefficient (M-1 cm-1)')), - ('qy', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='Quantum Yield')), - ('brightness', models.FloatField(blank=True, editable=False, null=True)), - ('lifetime', models.FloatField(blank=True, help_text='Lifetime (ns)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(20)])), - ('pka', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(2), django.core.validators.MaxValueValidator(12)], verbose_name='pKa')), - ('twop_ex_max', models.PositiveSmallIntegerField(blank=True, db_index=True, null=True, validators=[django.core.validators.MinValueValidator(700), django.core.validators.MaxValueValidator(1600)], verbose_name='Peak 2P excitation')), - ('twop_peakGM', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(200)], verbose_name='Peak 2P cross-section of S0->S1 (GM)')), - ('twop_qy', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='2P Quantum Yield')), - ('is_dark', models.BooleanField(default=False, help_text='This state does not fluorescence', verbose_name='Dark State')), - ('label', models.CharField(db_index=True, max_length=255)), - ('slug', models.SlugField(unique=True)), - ('entity_type', models.CharField(choices=[('protein', 'Protein'), ('dye', 'Dye')], db_index=True, max_length=10)), - ('source_map', models.JSONField(blank=True, default=dict)), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), - ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.CreateModel( - name='ProteinCollection', + name="BleachMeasurement", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('name', models.CharField(max_length=100)), - ('description', models.CharField(blank=True, max_length=512)), - ('managers', django.contrib.postgres.fields.ArrayField(base_field=models.EmailField(max_length=254), blank=True, default=list, size=None)), - ('private', models.BooleanField(default=False, help_text='Private collections can not be seen by or shared with other users', verbose_name='Private Collection')), - ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)ss', to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.CreateModel( - name='Protein', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('status', model_utils.fields.StatusField(choices=[('pending', 'pending'), ('approved', 'approved'), ('hidden', 'hidden')], default='pending', max_length=100, no_check_for_status=True, verbose_name='status')), - ('status_changed', model_utils.fields.MonitorField(default=django.utils.timezone.now, monitor='status', verbose_name='status changed')), - ('uuid', models.CharField(db_index=True, default=proteins.models.protein.prot_uuid, editable=False, max_length=5, unique=True, verbose_name='FPbase ID')), - ('name', models.CharField(db_index=True, help_text='Name of the fluorescent protein', max_length=128)), - ('slug', models.SlugField(help_text='URL slug for the protein', max_length=64, unique=True)), - ('base_name', models.CharField(max_length=128)), - ('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=200), blank=True, null=True, size=None)), - ('chromophore', proteins.models.protein._NonNullChar(blank=True, default='', max_length=5)), - ('seq_validated', models.BooleanField(default=False, help_text='Sequence has been validated by a moderator')), - ('seq', proteins.models._sequence_field.SequenceField(blank=True, help_text='Amino acid sequence (IPG ID is preferred)', null=True, unique=True, verbose_name='Sequence')), - ('seq_comment', models.CharField(blank=True, help_text='if necessary, comment on source of sequence', max_length=512)), - ('pdb', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=4), blank=True, null=True, size=None, verbose_name='Protein DataBank IDs')), - ('genbank', models.CharField(blank=True, help_text='NCBI Genbank Accession', max_length=12, null=True, unique=True, verbose_name='Genbank Accession')), - ('uniprot', models.CharField(blank=True, max_length=10, null=True, unique=True, validators=[django.core.validators.RegexValidator('[OPQ][0-9][A-Z0-9]{3}[0-9]|[A-NR-Z][0-9]([A-Z][A-Z0-9]{2}[0-9]){1,2}', 'Not a valid UniProt Accession')], verbose_name='UniProtKB Accession')), - ('ipg_id', models.CharField(blank=True, help_text='Identical Protein Group ID at Pubmed', max_length=12, null=True, unique=True, verbose_name='IPG ID')), - ('mw', models.FloatField(blank=True, help_text='Molecular Weight', null=True)), - ('agg', models.CharField(blank=True, choices=[('m', 'Monomer'), ('d', 'Dimer'), ('td', 'Tandem dimer'), ('wd', 'Weak dimer'), ('t', 'Tetramer')], help_text='Oligomerization tendency', max_length=2, verbose_name='Oligomerization')), - ('oser', models.FloatField(blank=True, help_text='OSER score', null=True)), - ('switch_type', models.CharField(blank=True, choices=[('b', 'Basic'), ('pa', 'Photoactivatable'), ('ps', 'Photoswitchable'), ('pc', 'Photoconvertible'), ('mp', 'Multi-photochromic'), ('t', 'Multistate'), ('o', 'Timer')], default='b', help_text='Photoswitching type (basic if none)', max_length=2, verbose_name='Switching Type')), - ('blurb', models.TextField(blank=True, help_text='Brief descriptive blurb', max_length=512)), - ('cofactor', models.CharField(blank=True, choices=[('br', 'Bilirubin'), ('bv', 'Biliverdin'), ('fl', 'Flavin'), ('pc', 'Phycocyanobilin'), ('rl', 'ribityl-lumazine')], help_text='Required for fluorescence', max_length=2)), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), + ( + "rate", + models.FloatField( + help_text="Photobleaching half-life (s)", + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(3000), + ], + verbose_name="Bleach Rate", + ), + ), + ( + "power", + models.FloatField( + blank=True, + help_text="If not reported, use '-1'", + null=True, + validators=[django.core.validators.MinValueValidator(-1)], + verbose_name="Illumination Power", + ), + ), + ( + "units", + models.CharField(blank=True, help_text="e.g. W/cm2", max_length=100, verbose_name="Power Unit"), + ), + ( + "light", + models.CharField( + blank=True, + choices=[("a", "Arc-lamp"), ("la", "Laser"), ("le", "LED"), ("o", "Other")], + max_length=2, + verbose_name="Light Source", + ), + ), + ( + "modality", + models.CharField( + blank=True, + choices=[ + ("wf", "Widefield"), + ("ps", "Point Scanning Confocal"), + ("sd", "Spinning Disc Confocal"), + ("s", "Spectrophotometer"), + ("t", "TIRF"), + ("o", "Other"), + ], + max_length=2, + verbose_name="Imaging Modality", + ), + ), + ("temp", models.FloatField(blank=True, null=True, verbose_name="Temperature")), + ( + "fusion", + models.CharField( + blank=True, help_text="(if applicable)", max_length=60, verbose_name="Fusion Protein" + ), + ), + ( + "in_cell", + models.IntegerField( + blank=True, + choices=[(-1, "Unkown"), (0, "No"), (1, "Yes")], + default=-1, + help_text="protein expressed in living cells", + verbose_name="In cells?", + ), + ), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="bleachmeasurement_author", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "reference", + models.ForeignKey( + blank=True, + help_text="Reference where the measurement was made", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="bleach_measurements", + to="references.Reference", + verbose_name="Measurement Reference", + ), + ), ], options={ - 'ordering': ['name'], + "abstract": False, }, ), migrations.CreateModel( - name='SnapGenePlasmid', + name="FRETpair", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('plasmid_id', models.CharField(db_index=True, max_length=100, unique=True)), - ('name', models.CharField(max_length=200)), - ('description', models.TextField(blank=True)), - ('author', models.CharField(blank=True, max_length=200)), - ('size', models.IntegerField(blank=True, help_text='Size in base pairs', null=True)), - ('topology', models.CharField(blank=True, max_length=50)), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), + ("radius", models.FloatField(blank=True, null=True)), ], options={ - 'verbose_name': 'SnapGene Plasmid', - 'verbose_name_plural': 'SnapGene Plasmids', - 'ordering': ['name'], + "verbose_name": "FRET Pair", }, ), migrations.CreateModel( - name='BleachMeasurement', + name="Mutation", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('rate', models.FloatField(help_text='Photobleaching half-life (s)', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(3000)], verbose_name='Bleach Rate')), - ('power', models.FloatField(blank=True, help_text="If not reported, use '-1'", null=True, validators=[django.core.validators.MinValueValidator(-1)], verbose_name='Illumination Power')), - ('units', models.CharField(blank=True, help_text='e.g. W/cm2', max_length=100, verbose_name='Power Units')), - ('light', models.CharField(blank=True, choices=[('a', 'Arc-lamp'), ('la', 'Laser'), ('le', 'LED'), ('o', 'Other')], max_length=2, verbose_name='Light Source')), - ('bandcenter', models.PositiveSmallIntegerField(blank=True, help_text='Band center of excitation light filter', null=True, validators=[django.core.validators.MinValueValidator(200), django.core.validators.MaxValueValidator(1600)], verbose_name='Band Center (nm)')), - ('bandwidth', models.PositiveSmallIntegerField(blank=True, help_text='Bandwidth of excitation light filter', null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(1000)], verbose_name='Bandwidth (nm)')), - ('modality', models.CharField(blank=True, choices=[('wf', 'Widefield'), ('ps', 'Point Scanning Confocal'), ('sd', 'Spinning Disc Confocal'), ('s', 'Spectrophotometer'), ('t', 'TIRF'), ('o', 'Other')], max_length=2, verbose_name='Imaging Modality')), - ('temp', models.FloatField(blank=True, null=True, verbose_name='Temp (˚C)')), - ('fusion', models.CharField(blank=True, help_text='(if applicable)', max_length=60, verbose_name='Fusion Protein')), - ('in_cell', models.IntegerField(blank=True, choices=[(-1, 'Unkown'), (0, 'No'), (1, 'Yes')], default=-1, help_text='protein expressed in living cells', verbose_name='In cells?')), - ('cell_type', models.CharField(blank=True, help_text='e.g. HeLa', max_length=60, verbose_name='Cell Type')), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), - ('reference', models.ForeignKey(blank=True, help_text='Reference where the measurement was made', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bleach_measurements', to='references.reference', verbose_name='Measurement Reference')), - ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL)), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "mutations", + django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=5), + size=None, + validators=[ + django.core.validators.RegexValidator( + "^[ACDEFGHIKLMNPQRSTVWY-][1-9][0-9]{0,2}[ACDEFGHIKLMNPQRSTVWY]$", + "not a valid mutation code: eg S65T", + ) + ], + ), + ), ], - options={ - 'abstract': False, - }, ), migrations.CreateModel( - name='Camera', + name="Organism", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('part', models.CharField(blank=True, max_length=128)), - ('url', models.URLField(blank=True)), - ('name', models.CharField(max_length=100)), - ('slug', models.SlugField(help_text='Unique slug for the %(class)', max_length=128, unique=True)), - ('manufacturer', models.CharField(blank=True, max_length=128)), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), - ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL)), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), + ( + "id", + models.PositiveIntegerField( + help_text="NCBI Taxonomy ID", primary_key=True, serialize=False, verbose_name="Taxonomy ID" + ), + ), + ("scientific_name", models.CharField(blank=True, max_length=128)), + ("division", models.CharField(blank=True, max_length=128)), + ("common_name", models.CharField(blank=True, max_length=128)), + ("species", models.CharField(blank=True, max_length=128)), + ("genus", models.CharField(blank=True, max_length=128)), + ("rank", models.CharField(blank=True, max_length=128)), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="organism_author", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "updated_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="organism_modifier", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'ordering': ['name'], - 'abstract': False, + "verbose_name": "Organism", + "ordering": ["scientific_name"], }, ), migrations.CreateModel( - name='Dye', + name="Protein", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('manufacturer', models.CharField(blank=True, max_length=128)), - ('part', models.CharField(blank=True, max_length=128)), - ('url', models.URLField(blank=True)), - ('name', models.CharField(db_index=True, max_length=255)), - ('slug', models.SlugField(unique=True)), - ('synonyms', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, size=None)), - ('structural_status', models.CharField(choices=[('DEFINED', 'Defined Structure'), ('PROPRIETARY', 'Proprietary / Unknown Structure')], default='DEFINED', max_length=20)), - ('canonical_smiles', models.TextField(blank=True)), - ('inchi', models.TextField(blank=True)), - ('inchikey', models.CharField(blank=True, db_index=True, max_length=27)), - ('molblock', models.TextField(blank=True, help_text='V3000 Molfile for precise rendering')), - ('chemical_class', models.CharField(blank=True, db_index=True, max_length=100)), - ('equilibrium_constant_klz', models.FloatField(blank=True, help_text='Equilibrium constant between non-fluorescent lactone and fluorescent zwitterion.', null=True)), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), - ('parent_mixture', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='isomers', to='proteins.dye')), - ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.CreateModel( - name='DyeState', - fields=[ - ('fluorophore_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='proteins.fluorophore')), - ('name', models.CharField(help_text="e.g., 'Bound to DNA' or 'In Methanol'", max_length=255)), - ('solvent', models.CharField(default='PBS', max_length=100)), - ('ph', models.FloatField(default=7.4)), - ('environment', models.CharField(choices=[], default='FREE', max_length=20)), - ('is_reference', models.BooleanField(default=False, help_text='If True, this is the default state shown on the dye summary card.')), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), + ( + "status", + model_utils.fields.StatusField( + choices=[("pending", "pending"), ("approved", "approved"), ("hidden", "hidden")], + default="pending", + max_length=100, + no_check_for_status=True, + verbose_name="status", + ), + ), + ( + "status_changed", + model_utils.fields.MonitorField( + default=django.utils.timezone.now, monitor="status", verbose_name="status changed" + ), + ), + ("uuid", models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ("name", models.CharField(db_index=True, help_text="Name of the fluorescent protein", max_length=128)), + ("slug", models.SlugField(help_text="URL slug for the protein", max_length=64, unique=True)), + ("base_name", models.CharField(max_length=128)), + ( + "aliases", + django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=200), blank=True, null=True, size=None + ), + ), + ("chromophore", models.CharField(blank=True, max_length=5, null=True)), + ( + "seq", + models.CharField( + blank=True, + help_text="Amino acid sequence (IPG ID is preferred)", + max_length=1024, + null=True, + unique=True, + validators=[proteins.validators.protein_sequence_validator], + verbose_name="Sequence", + ), + ), + ( + "pdb", + django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=4), + blank=True, + null=True, + size=None, + verbose_name="Protein DataBank ID", + ), + ), + ( + "genbank", + models.CharField( + blank=True, + help_text="NCBI Genbank Accession", + max_length=12, + null=True, + unique=True, + verbose_name="Genbank Accession", + ), + ), + ( + "uniprot", + models.CharField( + blank=True, + max_length=10, + null=True, + unique=True, + validators=[ + django.core.validators.RegexValidator( + "[OPQ][0-9][A-Z0-9]{3}[0-9]|[A-NR-Z][0-9]([A-Z][A-Z0-9]{2}[0-9]){1,2}", + "Not a valid UniProt Accession", + ) + ], + verbose_name="UniProtKB Accession", + ), + ), + ( + "ipg_id", + models.CharField( + blank=True, + help_text="Identical Protein Group ID at Pubmed", + max_length=12, + null=True, + unique=True, + verbose_name="IPG ID", + ), + ), + ("mw", models.FloatField(blank=True, help_text="Molecular Weight", null=True)), + ( + "agg", + models.CharField( + blank=True, + choices=[ + ("m", "Monomer"), + ("d", "Dimer"), + ("td", "Tandem dimer"), + ("wd", "Weak dimer"), + ("t", "Tetramer"), + ], + help_text="Oligomerization tendency", + max_length=2, + ), + ), + ( + "switch_type", + models.CharField( + blank=True, + choices=[ + ("b", "Basic"), + ("pa", "Photoactivatable"), + ("ps", "Photoswitchable"), + ("pc", "Photoconvertible"), + ("t", "Timer"), + ("o", "Multistate"), + ], + help_text="Photoswitching type (basic if none)", + max_length=2, + verbose_name="Type", + ), + ), + ("blurb", models.CharField(blank=True, help_text="Brief descriptive blurb", max_length=512)), + ( + "FRET_partner", + models.ManyToManyField(blank=True, through="proteins.FRETpair", to="proteins.Protein"), + ), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="protein_author", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'abstract': False, + "ordering": ["name"], }, - bases=('proteins.fluorophore',), ), migrations.CreateModel( - name='State', + name="ProteinCollection", fields=[ - ('fluorophore_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='proteins.fluorophore')), - ('name', models.CharField(default='default', max_length=64)), - ('maturation', models.FloatField(blank=True, help_text='Maturation time (min)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1600)])), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), + ("name", models.CharField(max_length=100)), + ("description", models.CharField(blank=True, max_length=512)), + ( + "private", + models.BooleanField( + default=False, + help_text="Private collections can not be seen by or shared with other users", + verbose_name="Private Collection", + ), + ), + ( + "owner", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="collections", + to=settings.AUTH_USER_MODEL, + verbose_name="Protein Collection", + ), + ), + ("proteins", models.ManyToManyField(related_name="collection_memberships", to="proteins.Protein")), ], - options={ - 'abstract': False, - }, - bases=('proteins.fluorophore',), ), migrations.CreateModel( - name='Filter', + name="State", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('manufacturer', models.CharField(blank=True, max_length=128)), - ('part', models.CharField(blank=True, max_length=128)), - ('url', models.URLField(blank=True)), - ('name', models.CharField(max_length=100)), - ('slug', models.SlugField(help_text='Unique slug for the %(class)', max_length=128, unique=True)), - ('bandcenter', models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(200), django.core.validators.MaxValueValidator(1600)])), - ('bandwidth', models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(900)])), - ('edge', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1600)])), - ('tavg', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)])), - ('aoi', models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(90)])), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), - ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL)), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), + ("name", models.CharField(default="default", max_length=64)), + ("slug", models.SlugField(help_text="Unique slug for the state", max_length=128, unique=True)), + ( + "is_dark", + models.BooleanField( + default=False, help_text="This state does not fluorescence", verbose_name="Dark State" + ), + ), + ( + "ex_max", + models.PositiveSmallIntegerField( + blank=True, + db_index=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(300), + django.core.validators.MaxValueValidator(900), + ], + ), + ), + ( + "em_max", + models.PositiveSmallIntegerField( + blank=True, + db_index=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(300), + django.core.validators.MaxValueValidator(1000), + ], + ), + ), + ( + "ex_spectra", + proteins.fields.SpectrumField( + blank=True, help_text="List of [[wavelength, value],...] pairs", null=True + ), + ), + ( + "em_spectra", + proteins.fields.SpectrumField( + blank=True, help_text="List of [[wavelength, value],...] pairs", null=True + ), + ), + ( + "ext_coeff", + models.IntegerField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(300000), + ], + verbose_name="Extinction Coefficient", + ), + ), + ( + "qy", + models.FloatField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(1), + ], + verbose_name="Quantum Yield", + ), + ), + ("brightness", models.FloatField(blank=True, editable=False, null=True)), + ( + "pka", + models.FloatField( + blank=True, + null=True, + validators=[ + django.core.validators.MinValueValidator(2), + django.core.validators.MaxValueValidator(12), + ], + verbose_name="pKa", + ), + ), + ( + "maturation", + models.FloatField( + blank=True, + help_text="Maturation time (min)", + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(1600), + ], + ), + ), + ( + "lifetime", + models.FloatField( + blank=True, + help_text="Lifetime (ns)", + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(20), + ], + ), + ), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="state_author", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "protein", + models.ForeignKey( + help_text="The protein to which this state belongs", + on_delete=django.db.models.deletion.CASCADE, + related_name="states", + to="proteins.Protein", + ), + ), ], options={ - 'ordering': ['bandcenter'], + "verbose_name": "State", }, ), migrations.CreateModel( - name='FilterPlacement', + name="StateTransition", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('path', models.CharField(choices=[('ex', 'Excitation Path'), ('em', 'Emission Path'), ('bs', 'Both Paths')], max_length=2, verbose_name='Ex/Bs/Em Path')), - ('reflects', models.BooleanField(default=False, help_text='Filter reflects emission (if BS or EM filter)')), - ('filter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='proteins.filter')), - ], - ), - migrations.CreateModel( - name='FluorescenceMeasurement', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('ex_max', models.PositiveSmallIntegerField(blank=True, db_index=True, help_text='Excitation maximum (nm)', null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(900)])), - ('em_max', models.PositiveSmallIntegerField(blank=True, db_index=True, help_text='Emission maximum (nm)', null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1000)])), - ('emhex', models.CharField(blank=True, max_length=7)), - ('exhex', models.CharField(blank=True, max_length=7)), - ('ext_coeff', models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(300000)], verbose_name='Extinction Coefficient (M-1 cm-1)')), - ('qy', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='Quantum Yield')), - ('brightness', models.FloatField(blank=True, editable=False, null=True)), - ('lifetime', models.FloatField(blank=True, help_text='Lifetime (ns)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(20)])), - ('pka', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(2), django.core.validators.MaxValueValidator(12)], verbose_name='pKa')), - ('twop_ex_max', models.PositiveSmallIntegerField(blank=True, db_index=True, null=True, validators=[django.core.validators.MinValueValidator(700), django.core.validators.MaxValueValidator(1600)], verbose_name='Peak 2P excitation')), - ('twop_peakGM', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(200)], verbose_name='Peak 2P cross-section of S0->S1 (GM)')), - ('twop_qy', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='2P Quantum Yield')), - ('is_dark', models.BooleanField(default=False, help_text='This state does not fluorescence', verbose_name='Dark State')), - ('date_measured', models.DateField(blank=True, null=True)), - ('conditions', models.TextField(blank=True, help_text='pH, solvent, temp, etc.')), - ('is_trusted', models.BooleanField(default=False, help_text='If True, this measurement overrides others.')), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), - ('fluorophore', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='measurements', to='proteins.fluorophore')), - ('reference', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='references.reference')), - ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL)), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), + ( + "trans_wave", + models.PositiveSmallIntegerField( + blank=True, + help_text="Wavelength required", + null=True, + validators=[ + django.core.validators.MinValueValidator(300), + django.core.validators.MaxValueValidator(1000), + ], + verbose_name="Transition Wavelength", + ), + ), + ( + "from_state", + models.ForeignKey( + help_text="The initial state ", + on_delete=django.db.models.deletion.CASCADE, + related_name="transitions_from", + to="proteins.State", + verbose_name="From state", + ), + ), + ( + "protein", + models.ForeignKey( + help_text="The protein that demonstrates this transition", + on_delete=django.db.models.deletion.CASCADE, + related_name="transitions", + to="proteins.Protein", + verbose_name="Protein Transitioning", + ), + ), + ( + "to_state", + models.ForeignKey( + help_text="The state after transition", + on_delete=django.db.models.deletion.CASCADE, + related_name="transitions_to", + to="proteins.State", + verbose_name="To state", + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), - migrations.CreateModel( - name='FluorophoreCollection', - fields=[ - ('proteincollection_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='proteins.proteincollection')), - ], - options={ - 'abstract': False, - }, - bases=('proteins.proteincollection',), - ), - migrations.CreateModel( - name='Light', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('part', models.CharField(blank=True, max_length=128)), - ('url', models.URLField(blank=True)), - ('name', models.CharField(max_length=100)), - ('slug', models.SlugField(help_text='Unique slug for the %(class)', max_length=128, unique=True)), - ('manufacturer', models.CharField(blank=True, max_length=128)), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), - ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'ordering': ['name'], - 'abstract': False, - }, - ), - migrations.CreateModel( - name='Microscope', - fields=[ - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('name', models.CharField(max_length=100)), - ('description', models.CharField(blank=True, max_length=512)), - ('managers', django.contrib.postgres.fields.ArrayField(base_field=models.EmailField(max_length=254), blank=True, default=list, size=None)), - ('id', models.CharField(default=proteins.util.helpers.shortuuid, editable=False, max_length=22, primary_key=True, serialize=False)), - ('extra_lasers', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1600)]), blank=True, default=list, size=None)), - ('cfg_calc_efficiency', models.BooleanField(default=True, help_text='Calculate efficiency on update.')), - ('cfg_fill_area', models.BooleanField(default=True, help_text='Fill area under spectra.')), - ('cfg_min_wave', models.PositiveSmallIntegerField(default=350, help_text='Minimum wavelength to display on page load.', validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1199)])), - ('cfg_max_wave', models.PositiveSmallIntegerField(default=800, help_text='Maximum wavelength to display on page load.', validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1199)])), - ('cfg_enable_pan_zoom', models.BooleanField(default=True, help_text='Enable pan and zoom on spectra plot.')), - ('collection', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='on_scope', to='proteins.proteincollection')), - ('extra_cameras', models.ManyToManyField(blank=True, related_name='microscopes', to='proteins.camera')), - ('extra_lights', models.ManyToManyField(blank=True, related_name='microscopes', to='proteins.light')), - ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)ss', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'ordering': ['created'], - }, - ), - migrations.CreateModel( - name='OpticalConfig', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('name', models.CharField(max_length=100)), - ('description', models.CharField(blank=True, max_length=512)), - ('managers', django.contrib.postgres.fields.ArrayField(base_field=models.EmailField(max_length=254), blank=True, default=list, size=None)), - ('comments', models.CharField(blank=True, max_length=256)), - ('laser', models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1600)])), - ('camera', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='optical_configs', to='proteins.camera')), - ('filters', models.ManyToManyField(blank=True, related_name='optical_configs', through='proteins.FilterPlacement', to='proteins.filter')), - ('light', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='optical_configs', to='proteins.light')), - ('microscope', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='optical_configs', to='proteins.microscope')), - ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)ss', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'ordering': ['name'], - }, - ), - migrations.CreateModel( - name='OcFluorEff', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('object_id', models.PositiveIntegerField()), - ('fluor_name', models.CharField(blank=True, max_length=100)), - ('ex_eff', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='Excitation Efficiency')), - ('ex_eff_broad', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='Excitation Efficiency (Broadband)')), - ('em_eff', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='Emission Efficiency')), - ('brightness', models.FloatField(blank=True, null=True)), - ('content_type', models.ForeignKey(limit_choices_to=models.Q(models.Q(('app_label', 'proteins'), ('model', 'state')), models.Q(('app_label', 'proteins'), ('model', 'dye')), _connector='OR'), on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), - ('oc', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='proteins.opticalconfig')), - ], - ), migrations.AddField( - model_name='filterplacement', - name='config', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='proteins.opticalconfig'), - ), - migrations.CreateModel( - name='Organism', - fields=[ - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('id', models.PositiveIntegerField(help_text='NCBI Taxonomy ID', primary_key=True, serialize=False, verbose_name='Taxonomy ID')), - ('scientific_name', models.CharField(blank=True, max_length=128)), - ('division', models.CharField(blank=True, max_length=128)), - ('common_name', models.CharField(blank=True, max_length=128)), - ('species', models.CharField(blank=True, max_length=128)), - ('genus', models.CharField(blank=True, max_length=128)), - ('rank', models.CharField(blank=True, max_length=128)), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), - ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'Organism', - 'ordering': ['scientific_name'], - }, + model_name="state", + name="transitions", + field=models.ManyToManyField( + blank=True, + related_name="transition_state", + through="proteins.StateTransition", + to="proteins.State", + verbose_name="State Transitions", + ), ), migrations.AddField( - model_name='proteincollection', - name='proteins', - field=models.ManyToManyField(related_name='collection_memberships', to='proteins.protein'), + model_name="state", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="state_modifier", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='protein', - name='parent_organism', - field=models.ForeignKey(blank=True, help_text='Organism from which the protein was engineered', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='proteins', to='proteins.organism', verbose_name='Parental organism'), + model_name="protein", + name="default_state", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="default_for", + to="proteins.State", + ), ), migrations.AddField( - model_name='protein', - name='primary_reference', - field=models.ForeignKey(blank=True, help_text='Preferably the publication that introduced the protein', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_proteins', to='references.reference', verbose_name='Primary Reference'), + model_name="protein", + name="parent_organism", + field=models.ForeignKey( + blank=True, + help_text="Organism from which the protein was engineered", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="proteins", + to="proteins.Organism", + verbose_name="Parental organism", + ), ), migrations.AddField( - model_name='protein', - name='references', - field=models.ManyToManyField(blank=True, related_name='proteins', to='references.reference'), - ), - migrations.AddField( - model_name='protein', - name='updated_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL), - ), - migrations.CreateModel( - name='OSERMeasurement', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('percent', models.FloatField(blank=True, help_text="Percentage of 'normal' looking cells", null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)], verbose_name='Percent Normal Cells')), - ('percent_stddev', models.FloatField(blank=True, help_text='Standard deviation of percent normal cells (if applicable)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)], verbose_name='StdDev')), - ('percent_ncells', models.IntegerField(blank=True, help_text='Number of cells analyzed in percent normal for this FP', null=True, verbose_name='Number of cells for percent measurement')), - ('oserne', models.FloatField(blank=True, help_text='Ratio of OSER to nuclear envelope (NE) fluorescence intensities', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)], verbose_name='OSER/NE ratio')), - ('oserne_stddev', models.FloatField(blank=True, help_text='Standard deviation of OSER/NE ratio (if applicable)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)], verbose_name='OSER/NE StdDev')), - ('oserne_ncells', models.IntegerField(blank=True, help_text='Number of cells analyzed in OSER/NE this FP', null=True, verbose_name='Number of cells for OSER/NE measurement')), - ('celltype', models.CharField(blank=True, help_text='e.g. COS-7, HeLa', max_length=64, verbose_name='Cell Type')), - ('temp', models.FloatField(blank=True, null=True, verbose_name='Temperature')), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), - ('reference', models.ForeignKey(blank=True, help_text='Reference where the measurement was made', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='oser_measurements', to='references.reference', verbose_name='Measurement Reference')), - ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL)), - ('protein', models.ForeignKey(help_text='The protein on which this measurement was made', on_delete=django.db.models.deletion.CASCADE, related_name='oser_measurements', to='proteins.protein', verbose_name='Protein')), - ], - ), - migrations.CreateModel( - name='Lineage', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('mutation', proteins.models.lineage.MutationSetField(blank=True, max_length=400)), - ('rootmut', models.CharField(blank=True, max_length=400)), - ('lft', models.PositiveIntegerField(editable=False)), - ('rght', models.PositiveIntegerField(editable=False)), - ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)), - ('level', models.PositiveIntegerField(editable=False)), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), - ('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='proteins.lineage')), - ('reference', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='lineages', to='references.reference')), - ('root_node', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='descendants', to='proteins.lineage', verbose_name='Root Node')), - ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL)), - ('protein', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='lineage', to='proteins.protein')), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='Excerpt', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('status', model_utils.fields.StatusField(choices=[('approved', 'approved'), ('flagged', 'flagged'), ('rejected', 'rejected')], default='approved', max_length=100, no_check_for_status=True, verbose_name='status')), - ('status_changed', model_utils.fields.MonitorField(default=django.utils.timezone.now, monitor='status', verbose_name='status changed')), - ('content', models.TextField(help_text='Brief excerpt describing this protein', max_length=1200)), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), - ('reference', models.ForeignKey(help_text='Source of this excerpt', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='excerpts', to='references.reference')), - ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL)), - ('proteins', models.ManyToManyField(blank=True, related_name='excerpts', to='proteins.protein')), - ], - options={ - 'ordering': ['reference__year', 'created'], - }, - ), - migrations.CreateModel( - name='ReactiveDerivative', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('reactive_group', models.CharField(choices=[('NHS_ESTER', 'NHS Ester'), ('HALO_TAG', 'HaloTag Ligand'), ('SNAP_TAG', 'SNAP-Tag Ligand'), ('CLIP_TAG', 'CLIP-Tag Ligand'), ('MALEIMIDE', 'Maleimide'), ('AZIDE', 'Azide'), ('ALKYNE', 'Alkyne'), ('BIOTIN', 'Biotin'), ('OTHER', 'Other')], max_length=10)), - ('full_smiles', models.TextField(blank=True, help_text='Structure of the complete reactive molecule')), - ('molecular_weight', models.FloatField()), - ('vendor', models.CharField(max_length=100)), - ('catalog_number', models.CharField(max_length=100)), - ('core_dye', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='derivatives', to='proteins.dye')), - ], + model_name="protein", + name="primary_reference", + field=models.ForeignKey( + blank=True, + help_text="Preferably the publication that introduced the protein", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="primary_proteins", + to="references.Reference", + verbose_name="Primary Reference", + ), ), migrations.AddField( - model_name='protein', - name='snapgene_plasmids', - field=models.ManyToManyField(blank=True, help_text='Associated SnapGene plasmids', related_name='proteins', to='proteins.snapgeneplasmid'), - ), - migrations.CreateModel( - name='Spectrum', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('status', model_utils.fields.StatusField(choices=[('approved', 'approved'), ('pending', 'pending'), ('rejected', 'rejected')], default='approved', max_length=100, no_check_for_status=True, verbose_name='status')), - ('status_changed', model_utils.fields.MonitorField(default=django.utils.timezone.now, monitor='status', verbose_name='status changed')), - ('data', proteins.models.spectrum.SpectrumData(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(max_length=10), size=2), size=None)), - ('category', models.CharField(choices=[('d', 'Dye'), ('p', 'Protein'), ('l', 'Light Source'), ('f', 'Filter'), ('c', 'Camera')], db_index=True, max_length=1, verbose_name='Spectrum Type')), - ('subtype', models.CharField(choices=[('ex', 'Excitation'), ('ab', 'Absorption'), ('em', 'Emission'), ('2p', 'Two Photon Abs'), ('bp', 'Bandpass'), ('bx', 'Bandpass-Ex'), ('bm', 'Bandpass-Em'), ('sp', 'Shortpass'), ('lp', 'Longpass'), ('bs', 'Beamsplitter'), ('qe', 'Quantum Efficiency'), ('pd', 'Power Distribution')], db_index=True, max_length=2, verbose_name='Spectrum Subtype')), - ('ph', models.FloatField(blank=True, null=True, verbose_name='pH')), - ('solvent', models.CharField(blank=True, max_length=128)), - ('source', models.CharField(blank=True, help_text='Source of the spectra data', max_length=128)), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), - ('owner_camera', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectrum', to='proteins.camera')), - ('owner_filter', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectrum', to='proteins.filter')), - ('owner_fluor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectra', to='proteins.fluorophore')), - ('owner_light', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectrum', to='proteins.light')), - ('reference', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='spectra', to='references.reference')), - ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name_plural': 'spectra', - }, - bases=(models.Model, proteins.models.mixins.AdminURLMixin), - ), - migrations.CreateModel( - name='StateTransition', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('trans_wave', models.PositiveSmallIntegerField(blank=True, help_text='Wavelength required', null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1000)], verbose_name='Transition Wavelength')), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_author', to=settings.AUTH_USER_MODEL)), - ('protein', models.ForeignKey(help_text='The protein that demonstrates this transition', on_delete=django.db.models.deletion.CASCADE, related_name='transitions', to='proteins.protein', verbose_name='Protein Transitioning')), - ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_modifier', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'abstract': False, - }, - ), - migrations.AddConstraint( - model_name='dye', - constraint=models.UniqueConstraint(condition=models.Q(('structural_status', 'DEFINED')), fields=('inchikey',), name='unique_defined_molecule'), + model_name="protein", + name="references", + field=models.ManyToManyField(blank=True, related_name="proteins", to="references.Reference"), ), migrations.AddField( - model_name='dyestate', - name='dye', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='states', to='proteins.dye'), + model_name="protein", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="protein_modifier", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='statetransition', - name='from_state', - field=models.ForeignKey(help_text='The initial state ', on_delete=django.db.models.deletion.CASCADE, related_name='transitions_from', to='proteins.state', verbose_name='From state'), + model_name="mutation", + name="parent", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="proteins", + to="proteins.Protein", + verbose_name="Parent Protein", + ), ), migrations.AddField( - model_name='statetransition', - name='to_state', - field=models.ForeignKey(help_text='The state after transition', on_delete=django.db.models.deletion.CASCADE, related_name='transitions_to', to='proteins.state', verbose_name='To state'), + model_name="fretpair", + name="acceptor", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="FK_FRETacceptor_protein", + to="proteins.Protein", + verbose_name="acceptor", + ), ), migrations.AddField( - model_name='state', - name='protein', - field=models.ForeignKey(help_text='The protein to which this state belongs', on_delete=django.db.models.deletion.CASCADE, related_name='states', to='proteins.protein'), + model_name="fretpair", + name="created_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="fretpair_author", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='state', - name='transitions', - field=models.ManyToManyField(blank=True, related_name='transition_state', through='proteins.StateTransition', to='proteins.state', verbose_name='State Transitions'), + model_name="fretpair", + name="donor", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="FK_FRETdonor_protein", + to="proteins.Protein", + verbose_name="donor", + ), ), migrations.AddField( - model_name='protein', - name='default_state', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_for', to='proteins.state'), + model_name="fretpair", + name="pair_references", + field=models.ManyToManyField(blank=True, related_name="FK_FRETpair_reference", to="references.Reference"), ), migrations.AddField( - model_name='bleachmeasurement', - name='state', - field=models.ForeignKey(help_text='The state on which this measurement was made', on_delete=django.db.models.deletion.CASCADE, related_name='bleach_measurements', to='proteins.state', verbose_name='Protein (state)'), - ), - migrations.AddIndex( - model_name='fluorophore', - index=models.Index(fields=['ex_max'], name='proteins_fl_ex_max_6b4df0_idx'), - ), - migrations.AddIndex( - model_name='fluorophore', - index=models.Index(fields=['em_max'], name='proteins_fl_em_max_a2aef9_idx'), + model_name="fretpair", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="fretpair_modifier", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='microscope', - name='fluors', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='fluor_on_scope', to='proteins.fluorophorecollection'), + model_name="bleachmeasurement", + name="state", + field=models.ForeignKey( + help_text="The state on which this measurement was made", + on_delete=django.db.models.deletion.CASCADE, + related_name="bleach_measurements", + to="proteins.State", + verbose_name="Protein (state)", + ), ), migrations.AddField( - model_name='fluorophorecollection', - name='dyes', - field=models.ManyToManyField(blank=True, related_name='collection_memberships', to='proteins.dye'), - ), - migrations.AlterUniqueTogether( - name='opticalconfig', - unique_together={('name', 'microscope')}, - ), - migrations.AlterUniqueTogether( - name='ocfluoreff', - unique_together={('oc', 'content_type', 'object_id')}, + model_name="bleachmeasurement", + name="updated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="bleachmeasurement_modifier", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterUniqueTogether( - name='proteincollection', - unique_together={('owner', 'name')}, + name="state", + unique_together={("protein", "ex_max", "em_max", "ext_coeff", "qy")}, ), migrations.AlterUniqueTogether( - name='osermeasurement', - unique_together={('protein', 'reference')}, - ), - migrations.AddIndex( - model_name='spectrum', - index=models.Index(fields=['owner_fluor_id', 'status'], name='spectrum_fluor_status_idx'), - ), - migrations.AddIndex( - model_name='spectrum', - index=models.Index(fields=['status'], name='spectrum_status_idx'), - ), - migrations.AddIndex( - model_name='protein', - index=models.Index(fields=['status'], name='protein_status_idx'), + name="proteincollection", + unique_together={("owner", "name")}, ), ] diff --git a/backend/proteins/migrations_old/0002_auto_20180312_0156.py b/backend/proteins/migrations/0002_auto_20180312_0156.py similarity index 100% rename from backend/proteins/migrations_old/0002_auto_20180312_0156.py rename to backend/proteins/migrations/0002_auto_20180312_0156.py diff --git a/backend/proteins/migrations/0002_update_ocfluoreff_to_use_direct_fk.py b/backend/proteins/migrations/0002_update_ocfluoreff_to_use_direct_fk.py deleted file mode 100644 index c8ad52699..000000000 --- a/backend/proteins/migrations/0002_update_ocfluoreff_to_use_direct_fk.py +++ /dev/null @@ -1,77 +0,0 @@ -from django.db import migrations, models -import django.db.models.deletion - - -def migrate_generic_fk_to_direct_fk(apps, schema_editor): - """Migrate data from GenericForeignKey (content_type/object_id) to direct ForeignKey (fluor).""" - OcFluorEff = apps.get_model("proteins", "OcFluorEff") - ContentType = apps.get_model("contenttypes", "ContentType") - - # Get content types for State and DyeState - try: - state_ct = ContentType.objects.get(app_label="proteins", model="state") - dyestate_ct = ContentType.objects.get(app_label="proteins", model="dyestate") - except ContentType.DoesNotExist: - # If content types don't exist, there's no data to migrate - return - - # Migrate all OcFluorEff records - for eff in OcFluorEff.objects.all(): - # The object_id already points to the correct Fluorophore ID - # because State and DyeState inherit from Fluorophore via MTI - if eff.content_type_id in (state_ct.id, dyestate_ct.id): - eff.fluor_id = eff.object_id - eff.save(update_fields=['fluor_id']) - - -class Migration(migrations.Migration): - dependencies = [ - ("proteins", "0001_initial"), - ("contenttypes", "0002_remove_content_type_name"), - ] - - operations = [ - # Step 1: Remove old unique_together constraint first (before removing fields) - migrations.AlterUniqueTogether( - name="ocfluoreff", - unique_together=set(), - ), - # Step 2: Add fluor field as nullable - migrations.AddField( - model_name="ocfluoreff", - name="fluor", - field=models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - to="proteins.fluorophore", - ), - ), - # Step 3: Migrate data - migrations.RunPython( - migrate_generic_fk_to_direct_fk, - reverse_code=migrations.RunPython.noop, - ), - # Step 4: Make fluor non-nullable - migrations.AlterField( - model_name="ocfluoreff", - name="fluor", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="proteins.fluorophore", - ), - ), - # Step 5: Remove content_type and object_id fields - migrations.RemoveField( - model_name="ocfluoreff", - name="content_type", - ), - migrations.RemoveField( - model_name="ocfluoreff", - name="object_id", - ), - # Step 6: Add new unique_together constraint - migrations.AlterUniqueTogether( - name="ocfluoreff", - unique_together={("oc", "fluor")}, - ), - ] diff --git a/backend/proteins/migrations_old/0003_protein_oser.py b/backend/proteins/migrations/0003_protein_oser.py similarity index 100% rename from backend/proteins/migrations_old/0003_protein_oser.py rename to backend/proteins/migrations/0003_protein_oser.py diff --git a/backend/proteins/migrations_old/0004_auto_20180315_1323.py b/backend/proteins/migrations/0004_auto_20180315_1323.py similarity index 100% rename from backend/proteins/migrations_old/0004_auto_20180315_1323.py rename to backend/proteins/migrations/0004_auto_20180315_1323.py diff --git a/backend/proteins/migrations_old/0005_auto_20180328_1614.py b/backend/proteins/migrations/0005_auto_20180328_1614.py similarity index 100% rename from backend/proteins/migrations_old/0005_auto_20180328_1614.py rename to backend/proteins/migrations/0005_auto_20180328_1614.py diff --git a/backend/proteins/migrations_old/0005_auto_20180328_1614_squashed_0010_auto_20180501_1547.py b/backend/proteins/migrations/0005_auto_20180328_1614_squashed_0010_auto_20180501_1547.py similarity index 100% rename from backend/proteins/migrations_old/0005_auto_20180328_1614_squashed_0010_auto_20180501_1547.py rename to backend/proteins/migrations/0005_auto_20180328_1614_squashed_0010_auto_20180501_1547.py diff --git a/backend/proteins/migrations_old/0006_auto_20180401_2009.py b/backend/proteins/migrations/0006_auto_20180401_2009.py similarity index 100% rename from backend/proteins/migrations_old/0006_auto_20180401_2009.py rename to backend/proteins/migrations/0006_auto_20180401_2009.py diff --git a/backend/proteins/migrations_old/0006_auto_20180512_0058.py b/backend/proteins/migrations/0006_auto_20180512_0058.py similarity index 100% rename from backend/proteins/migrations_old/0006_auto_20180512_0058.py rename to backend/proteins/migrations/0006_auto_20180512_0058.py diff --git a/backend/proteins/migrations_old/0007_auto_20180403_0140.py b/backend/proteins/migrations/0007_auto_20180403_0140.py similarity index 100% rename from backend/proteins/migrations_old/0007_auto_20180403_0140.py rename to backend/proteins/migrations/0007_auto_20180403_0140.py diff --git a/backend/proteins/migrations_old/0007_auto_20180513_2346.py b/backend/proteins/migrations/0007_auto_20180513_2346.py similarity index 100% rename from backend/proteins/migrations_old/0007_auto_20180513_2346.py rename to backend/proteins/migrations/0007_auto_20180513_2346.py diff --git a/backend/proteins/migrations_old/0008_auto_20180430_0308.py b/backend/proteins/migrations/0008_auto_20180430_0308.py similarity index 100% rename from backend/proteins/migrations_old/0008_auto_20180430_0308.py rename to backend/proteins/migrations/0008_auto_20180430_0308.py diff --git a/backend/proteins/migrations_old/0008_auto_20180515_1659.py b/backend/proteins/migrations/0008_auto_20180515_1659.py similarity index 100% rename from backend/proteins/migrations_old/0008_auto_20180515_1659.py rename to backend/proteins/migrations/0008_auto_20180515_1659.py diff --git a/backend/proteins/migrations_old/0009_auto_20180430_0335.py b/backend/proteins/migrations/0009_auto_20180430_0335.py similarity index 100% rename from backend/proteins/migrations_old/0009_auto_20180430_0335.py rename to backend/proteins/migrations/0009_auto_20180430_0335.py diff --git a/backend/proteins/migrations_old/0009_auto_20180525_1640.py b/backend/proteins/migrations/0009_auto_20180525_1640.py similarity index 100% rename from backend/proteins/migrations_old/0009_auto_20180525_1640.py rename to backend/proteins/migrations/0009_auto_20180525_1640.py diff --git a/backend/proteins/migrations_old/0010_auto_20180501_1547.py b/backend/proteins/migrations/0010_auto_20180501_1547.py similarity index 100% rename from backend/proteins/migrations_old/0010_auto_20180501_1547.py rename to backend/proteins/migrations/0010_auto_20180501_1547.py diff --git a/backend/proteins/migrations_old/0010_auto_20180525_1840.py b/backend/proteins/migrations/0010_auto_20180525_1840.py similarity index 100% rename from backend/proteins/migrations_old/0010_auto_20180525_1840.py rename to backend/proteins/migrations/0010_auto_20180525_1840.py diff --git a/backend/proteins/migrations_old/0011_auto_20180525_2349.py b/backend/proteins/migrations/0011_auto_20180525_2349.py similarity index 100% rename from backend/proteins/migrations_old/0011_auto_20180525_2349.py rename to backend/proteins/migrations/0011_auto_20180525_2349.py diff --git a/backend/proteins/migrations_old/0012_auto_20180708_1811.py b/backend/proteins/migrations/0012_auto_20180708_1811.py similarity index 100% rename from backend/proteins/migrations_old/0012_auto_20180708_1811.py rename to backend/proteins/migrations/0012_auto_20180708_1811.py diff --git a/backend/proteins/migrations_old/0013_auto_20180718_1717.py b/backend/proteins/migrations/0013_auto_20180718_1717.py similarity index 100% rename from backend/proteins/migrations_old/0013_auto_20180718_1717.py rename to backend/proteins/migrations/0013_auto_20180718_1717.py diff --git a/backend/proteins/migrations_old/0014_auto_20180718_2340.py b/backend/proteins/migrations/0014_auto_20180718_2340.py similarity index 100% rename from backend/proteins/migrations_old/0014_auto_20180718_2340.py rename to backend/proteins/migrations/0014_auto_20180718_2340.py diff --git a/backend/proteins/migrations_old/0015_auto_20180720_1921.py b/backend/proteins/migrations/0015_auto_20180720_1921.py similarity index 100% rename from backend/proteins/migrations_old/0015_auto_20180720_1921.py rename to backend/proteins/migrations/0015_auto_20180720_1921.py diff --git a/backend/proteins/migrations_old/0016_auto_20180722_1314.py b/backend/proteins/migrations/0016_auto_20180722_1314.py similarity index 100% rename from backend/proteins/migrations_old/0016_auto_20180722_1314.py rename to backend/proteins/migrations/0016_auto_20180722_1314.py diff --git a/backend/proteins/migrations_old/0017_auto_20180722_1626.py b/backend/proteins/migrations/0017_auto_20180722_1626.py similarity index 100% rename from backend/proteins/migrations_old/0017_auto_20180722_1626.py rename to backend/proteins/migrations/0017_auto_20180722_1626.py diff --git a/backend/proteins/migrations_old/0018_protein_cofactor.py b/backend/proteins/migrations/0018_protein_cofactor.py similarity index 100% rename from backend/proteins/migrations_old/0018_protein_cofactor.py rename to backend/proteins/migrations/0018_protein_cofactor.py diff --git a/backend/proteins/migrations_old/0019_auto_20180723_2200.py b/backend/proteins/migrations/0019_auto_20180723_2200.py similarity index 100% rename from backend/proteins/migrations_old/0019_auto_20180723_2200.py rename to backend/proteins/migrations/0019_auto_20180723_2200.py diff --git a/backend/proteins/migrations_old/0020_auto_20180729_0234.py b/backend/proteins/migrations/0020_auto_20180729_0234.py similarity index 100% rename from backend/proteins/migrations_old/0020_auto_20180729_0234.py rename to backend/proteins/migrations/0020_auto_20180729_0234.py diff --git a/backend/proteins/migrations_old/0021_auto_20180804_0203.py b/backend/proteins/migrations/0021_auto_20180804_0203.py similarity index 100% rename from backend/proteins/migrations_old/0021_auto_20180804_0203.py rename to backend/proteins/migrations/0021_auto_20180804_0203.py diff --git a/backend/proteins/migrations_old/0022_osermeasurement.py b/backend/proteins/migrations/0022_osermeasurement.py similarity index 100% rename from backend/proteins/migrations_old/0022_osermeasurement.py rename to backend/proteins/migrations/0022_osermeasurement.py diff --git a/backend/proteins/migrations_old/0023_spectrum_reference.py b/backend/proteins/migrations/0023_spectrum_reference.py similarity index 100% rename from backend/proteins/migrations_old/0023_spectrum_reference.py rename to backend/proteins/migrations/0023_spectrum_reference.py diff --git a/backend/proteins/migrations_old/0024_auto_20181011_1659.py b/backend/proteins/migrations/0024_auto_20181011_1659.py similarity index 100% rename from backend/proteins/migrations_old/0024_auto_20181011_1659.py rename to backend/proteins/migrations/0024_auto_20181011_1659.py diff --git a/backend/proteins/migrations_old/0025_auto_20181011_1715.py b/backend/proteins/migrations/0025_auto_20181011_1715.py similarity index 100% rename from backend/proteins/migrations_old/0025_auto_20181011_1715.py rename to backend/proteins/migrations/0025_auto_20181011_1715.py diff --git a/backend/proteins/migrations_old/0026_bleachmeasurement_cell_type.py b/backend/proteins/migrations/0026_bleachmeasurement_cell_type.py similarity index 100% rename from backend/proteins/migrations_old/0026_bleachmeasurement_cell_type.py rename to backend/proteins/migrations/0026_bleachmeasurement_cell_type.py diff --git a/backend/proteins/migrations_old/0027_auto_20181011_1754.py b/backend/proteins/migrations/0027_auto_20181011_1754.py similarity index 100% rename from backend/proteins/migrations_old/0027_auto_20181011_1754.py rename to backend/proteins/migrations/0027_auto_20181011_1754.py diff --git a/backend/proteins/migrations_old/0028_auto_20181012_2011.py b/backend/proteins/migrations/0028_auto_20181012_2011.py similarity index 100% rename from backend/proteins/migrations_old/0028_auto_20181012_2011.py rename to backend/proteins/migrations/0028_auto_20181012_2011.py diff --git a/backend/proteins/migrations_old/0029_auto_20181014_1241.py b/backend/proteins/migrations/0029_auto_20181014_1241.py similarity index 100% rename from backend/proteins/migrations_old/0029_auto_20181014_1241.py rename to backend/proteins/migrations/0029_auto_20181014_1241.py diff --git a/backend/proteins/migrations_old/0030_lineage.py b/backend/proteins/migrations/0030_lineage.py similarity index 100% rename from backend/proteins/migrations_old/0030_lineage.py rename to backend/proteins/migrations/0030_lineage.py diff --git a/backend/proteins/migrations_old/0031_auto_20181103_1531.py b/backend/proteins/migrations/0031_auto_20181103_1531.py similarity index 100% rename from backend/proteins/migrations_old/0031_auto_20181103_1531.py rename to backend/proteins/migrations/0031_auto_20181103_1531.py diff --git a/backend/proteins/migrations_old/0032_auto_20181107_2015.py b/backend/proteins/migrations/0032_auto_20181107_2015.py similarity index 100% rename from backend/proteins/migrations_old/0032_auto_20181107_2015.py rename to backend/proteins/migrations/0032_auto_20181107_2015.py diff --git a/backend/proteins/migrations_old/0033_auto_20181107_2119.py b/backend/proteins/migrations/0033_auto_20181107_2119.py similarity index 100% rename from backend/proteins/migrations_old/0033_auto_20181107_2119.py rename to backend/proteins/migrations/0033_auto_20181107_2119.py diff --git a/backend/proteins/migrations_old/0034_lineage_rootmut.py b/backend/proteins/migrations/0034_lineage_rootmut.py similarity index 100% rename from backend/proteins/migrations_old/0034_lineage_rootmut.py rename to backend/proteins/migrations/0034_lineage_rootmut.py diff --git a/backend/proteins/migrations_old/0035_auto_20181110_0103.py b/backend/proteins/migrations/0035_auto_20181110_0103.py similarity index 100% rename from backend/proteins/migrations_old/0035_auto_20181110_0103.py rename to backend/proteins/migrations/0035_auto_20181110_0103.py diff --git a/backend/proteins/migrations_old/0036_lineage_root_node.py b/backend/proteins/migrations/0036_lineage_root_node.py similarity index 100% rename from backend/proteins/migrations_old/0036_lineage_root_node.py rename to backend/proteins/migrations/0036_lineage_root_node.py diff --git a/backend/proteins/migrations_old/0037_auto_20181205_2035.py b/backend/proteins/migrations/0037_auto_20181205_2035.py similarity index 100% rename from backend/proteins/migrations_old/0037_auto_20181205_2035.py rename to backend/proteins/migrations/0037_auto_20181205_2035.py diff --git a/backend/proteins/migrations_old/0038_auto_20181205_2044.py b/backend/proteins/migrations/0038_auto_20181205_2044.py similarity index 100% rename from backend/proteins/migrations_old/0038_auto_20181205_2044.py rename to backend/proteins/migrations/0038_auto_20181205_2044.py diff --git a/backend/proteins/migrations_old/0039_auto_20181206_0009.py b/backend/proteins/migrations/0039_auto_20181206_0009.py similarity index 100% rename from backend/proteins/migrations_old/0039_auto_20181206_0009.py rename to backend/proteins/migrations/0039_auto_20181206_0009.py diff --git a/backend/proteins/migrations_old/0040_auto_20181210_0345.py b/backend/proteins/migrations/0040_auto_20181210_0345.py similarity index 100% rename from backend/proteins/migrations_old/0040_auto_20181210_0345.py rename to backend/proteins/migrations/0040_auto_20181210_0345.py diff --git a/backend/proteins/migrations_old/0041_auto_20181216_1743.py b/backend/proteins/migrations/0041_auto_20181216_1743.py similarity index 100% rename from backend/proteins/migrations_old/0041_auto_20181216_1743.py rename to backend/proteins/migrations/0041_auto_20181216_1743.py diff --git a/backend/proteins/migrations_old/0042_auto_20181216_1744.py b/backend/proteins/migrations/0042_auto_20181216_1744.py similarity index 100% rename from backend/proteins/migrations_old/0042_auto_20181216_1744.py rename to backend/proteins/migrations/0042_auto_20181216_1744.py diff --git a/backend/proteins/migrations_old/0043_remove_excerpt_protein.py b/backend/proteins/migrations/0043_remove_excerpt_protein.py similarity index 100% rename from backend/proteins/migrations_old/0043_remove_excerpt_protein.py rename to backend/proteins/migrations/0043_remove_excerpt_protein.py diff --git a/backend/proteins/migrations_old/0044_auto_20181218_1310.py b/backend/proteins/migrations/0044_auto_20181218_1310.py similarity index 100% rename from backend/proteins/migrations_old/0044_auto_20181218_1310.py rename to backend/proteins/migrations/0044_auto_20181218_1310.py diff --git a/backend/proteins/migrations_old/0045_dye_is_dark.py b/backend/proteins/migrations/0045_dye_is_dark.py similarity index 100% rename from backend/proteins/migrations_old/0045_dye_is_dark.py rename to backend/proteins/migrations/0045_dye_is_dark.py diff --git a/backend/proteins/migrations_old/0046_auto_20190121_1341.py b/backend/proteins/migrations/0046_auto_20190121_1341.py similarity index 100% rename from backend/proteins/migrations_old/0046_auto_20190121_1341.py rename to backend/proteins/migrations/0046_auto_20190121_1341.py diff --git a/backend/proteins/migrations_old/0047_auto_20190319_1525.py b/backend/proteins/migrations/0047_auto_20190319_1525.py similarity index 100% rename from backend/proteins/migrations_old/0047_auto_20190319_1525.py rename to backend/proteins/migrations/0047_auto_20190319_1525.py diff --git a/backend/proteins/migrations_old/0048_change_protein_uuid.py b/backend/proteins/migrations/0048_change_protein_uuid.py similarity index 100% rename from backend/proteins/migrations_old/0048_change_protein_uuid.py rename to backend/proteins/migrations/0048_change_protein_uuid.py diff --git a/backend/proteins/migrations_old/0049_auto_20190323_1947.py b/backend/proteins/migrations/0049_auto_20190323_1947.py similarity index 100% rename from backend/proteins/migrations_old/0049_auto_20190323_1947.py rename to backend/proteins/migrations/0049_auto_20190323_1947.py diff --git a/backend/proteins/migrations_old/0050_auto_20190714_1318.py b/backend/proteins/migrations/0050_auto_20190714_1318.py similarity index 100% rename from backend/proteins/migrations_old/0050_auto_20190714_1318.py rename to backend/proteins/migrations/0050_auto_20190714_1318.py diff --git a/backend/proteins/migrations_old/0051_alter_bleachmeasurement_created_by_and_more.py b/backend/proteins/migrations/0051_alter_bleachmeasurement_created_by_and_more.py similarity index 100% rename from backend/proteins/migrations_old/0051_alter_bleachmeasurement_created_by_and_more.py rename to backend/proteins/migrations/0051_alter_bleachmeasurement_created_by_and_more.py diff --git a/backend/proteins/migrations_old/0052_alter_protein_chromophore.py b/backend/proteins/migrations/0052_alter_protein_chromophore.py similarity index 100% rename from backend/proteins/migrations_old/0052_alter_protein_chromophore.py rename to backend/proteins/migrations/0052_alter_protein_chromophore.py diff --git a/backend/proteins/migrations_old/0053_alter_protein_chromophore.py b/backend/proteins/migrations/0053_alter_protein_chromophore.py similarity index 100% rename from backend/proteins/migrations_old/0053_alter_protein_chromophore.py rename to backend/proteins/migrations/0053_alter_protein_chromophore.py diff --git a/backend/proteins/migrations_old/0054_microscope_cfg_calc_efficiency_and_more.py b/backend/proteins/migrations/0054_microscope_cfg_calc_efficiency_and_more.py similarity index 100% rename from backend/proteins/migrations_old/0054_microscope_cfg_calc_efficiency_and_more.py rename to backend/proteins/migrations/0054_microscope_cfg_calc_efficiency_and_more.py diff --git a/backend/proteins/migrations_old/0055_spectrum_status_spectrum_status_changed.py b/backend/proteins/migrations/0055_spectrum_status_spectrum_status_changed.py similarity index 100% rename from backend/proteins/migrations_old/0055_spectrum_status_spectrum_status_changed.py rename to backend/proteins/migrations/0055_spectrum_status_spectrum_status_changed.py diff --git a/backend/proteins/migrations_old/0056_spectrum_spectrum_state_status_idx_and_more.py b/backend/proteins/migrations/0056_spectrum_spectrum_state_status_idx_and_more.py similarity index 100% rename from backend/proteins/migrations_old/0056_spectrum_spectrum_state_status_idx_and_more.py rename to backend/proteins/migrations/0056_spectrum_spectrum_state_status_idx_and_more.py diff --git a/backend/proteins/migrations_old/0057_add_status_index.py b/backend/proteins/migrations/0057_add_status_index.py similarity index 100% rename from backend/proteins/migrations_old/0057_add_status_index.py rename to backend/proteins/migrations/0057_add_status_index.py diff --git a/backend/proteins/migrations_old/0058_snapgeneplasmid_protein_snapgene_plasmids.py b/backend/proteins/migrations/0058_snapgeneplasmid_protein_snapgene_plasmids.py similarity index 100% rename from backend/proteins/migrations_old/0058_snapgeneplasmid_protein_snapgene_plasmids.py rename to backend/proteins/migrations/0058_snapgeneplasmid_protein_snapgene_plasmids.py diff --git a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py new file mode 100644 index 000000000..36b1f3d5b --- /dev/null +++ b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py @@ -0,0 +1,212 @@ +# Generated manually for schema overhaul + +from django.core.validators import MaxValueValidator, MinValueValidator +from django.contrib.postgres.fields import ArrayField +from django.db import migrations, models +import django.db.models.deletion +import model_utils.fields +import django.utils.timezone + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0058_snapgeneplasmid_protein_snapgene_plasmids"), + ("references", "0001_initial"), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + database_operations=[ + # In the database: copy old tables to _old versions, then drop originals + # This avoids index naming conflicts + migrations.RunSQL( + sql=""" + CREATE TABLE proteins_state_old AS SELECT * FROM proteins_state; + DROP TABLE proteins_state CASCADE; + """, + reverse_sql="DROP TABLE IF EXISTS proteins_state_old CASCADE;", + ), + migrations.RunSQL( + sql=""" + CREATE TABLE proteins_dye_old AS SELECT * FROM proteins_dye; + DROP TABLE proteins_dye CASCADE; + """, + reverse_sql="DROP TABLE IF EXISTS proteins_dye_old CASCADE;", + ), + ], + state_operations=[ + # In Django's state: remove old models + migrations.DeleteModel(name="State"), + migrations.DeleteModel(name="Dye"), + ], + ), + + # Now create all new models + migrations.CreateModel( + name="Fluorophore", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created", model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name="created")), + ("modified", model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name="modified")), + # Fluorescence data fields + ("ex_max", models.PositiveSmallIntegerField(blank=True, null=True, validators=[MinValueValidator(300), MaxValueValidator(900)], db_index=True, help_text="Excitation maximum (nm)")), + ("em_max", models.PositiveSmallIntegerField(blank=True, null=True, validators=[MinValueValidator(300), MaxValueValidator(1000)], db_index=True, help_text="Emission maximum (nm)")), + ("emhex", models.CharField(max_length=7, blank=True)), + ("exhex", models.CharField(max_length=7, blank=True)), + ("ext_coeff", models.IntegerField(blank=True, null=True, verbose_name="Extinction Coefficient (M-1 cm-1)", validators=[MinValueValidator(0), MaxValueValidator(300000)])), + ("qy", models.FloatField(null=True, blank=True, verbose_name="Quantum Yield", validators=[MinValueValidator(0), MaxValueValidator(1)])), + ("brightness", models.FloatField(null=True, blank=True, editable=False)), + ("lifetime", models.FloatField(null=True, blank=True, help_text="Lifetime (ns)", validators=[MinValueValidator(0), MaxValueValidator(20)])), + ("pka", models.FloatField(null=True, blank=True, verbose_name="pKa", validators=[MinValueValidator(2), MaxValueValidator(12)])), + ("twop_ex_max", models.PositiveSmallIntegerField(blank=True, null=True, verbose_name="Peak 2P excitation", validators=[MinValueValidator(700), MaxValueValidator(1600)], db_index=True)), + ("twop_peakGM", models.FloatField(null=True, blank=True, verbose_name="Peak 2P cross-section of S0->S1 (GM)", validators=[MinValueValidator(0), MaxValueValidator(200)])), + ("twop_qy", models.FloatField(null=True, blank=True, verbose_name="2P Quantum Yield", validators=[MinValueValidator(0), MaxValueValidator(1)])), + ("is_dark", models.BooleanField(default=False, verbose_name="Dark State", help_text="This state does not fluorescence")), + # Identity fields + ("label", models.CharField(max_length=255, db_index=True)), + ("slug", models.SlugField(max_length=100, unique=True)), # Increased from default 50 to accommodate long dye names + ("entity_type", models.CharField(max_length=10, choices=[("protein", "Protein"), ("dye", "Dye")], db_index=True)), + # Lineage tracking + ("source_map", models.JSONField(default=dict, blank=True)), + # Author tracking (from Authorable mixin) + ("created_by_id", models.IntegerField(null=True, blank=True)), + ("updated_by_id", models.IntegerField(null=True, blank=True)), + ], + options={ + "indexes": [ + models.Index(fields=["ex_max"], name="fluorophore_ex_max_idx"), + models.Index(fields=["em_max"], name="fluorophore_em_max_idx"), + ], + }, + ), + + migrations.CreateModel( + name="FluorescenceMeasurement", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created", model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name="created")), + ("modified", model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name="modified")), + # Fluorescence data fields (same as Fluorophore) + ("ex_max", models.PositiveSmallIntegerField(blank=True, null=True, validators=[MinValueValidator(300), MaxValueValidator(900)], db_index=True, help_text="Excitation maximum (nm)")), + ("em_max", models.PositiveSmallIntegerField(blank=True, null=True, validators=[MinValueValidator(300), MaxValueValidator(1000)], db_index=True, help_text="Emission maximum (nm)")), + ("emhex", models.CharField(max_length=7, blank=True)), + ("exhex", models.CharField(max_length=7, blank=True)), + ("ext_coeff", models.IntegerField(blank=True, null=True, verbose_name="Extinction Coefficient (M-1 cm-1)", validators=[MinValueValidator(0), MaxValueValidator(300000)])), + ("qy", models.FloatField(null=True, blank=True, verbose_name="Quantum Yield", validators=[MinValueValidator(0), MaxValueValidator(1)])), + ("brightness", models.FloatField(null=True, blank=True, editable=False)), + ("lifetime", models.FloatField(null=True, blank=True, help_text="Lifetime (ns)", validators=[MinValueValidator(0), MaxValueValidator(20)])), + ("pka", models.FloatField(null=True, blank=True, verbose_name="pKa", validators=[MinValueValidator(2), MaxValueValidator(12)])), + ("twop_ex_max", models.PositiveSmallIntegerField(blank=True, null=True, verbose_name="Peak 2P excitation", validators=[MinValueValidator(700), MaxValueValidator(1600)], db_index=True)), + ("twop_peakGM", models.FloatField(null=True, blank=True, verbose_name="Peak 2P cross-section of S0->S1 (GM)", validators=[MinValueValidator(0), MaxValueValidator(200)])), + ("twop_qy", models.FloatField(null=True, blank=True, verbose_name="2P Quantum Yield", validators=[MinValueValidator(0), MaxValueValidator(1)])), + ("is_dark", models.BooleanField(default=False, verbose_name="Dark State", help_text="This state does not fluorescence")), + # Measurement-specific fields + ("date_measured", models.DateField(null=True, blank=True)), + ("conditions", models.TextField(blank=True, help_text="pH, solvent, temp, etc.")), + ("is_trusted", models.BooleanField(default=False, help_text="If True, this measurement overrides others.")), + # Foreign keys + ("fluorophore", models.ForeignKey("Fluorophore", related_name="measurements", on_delete=django.db.models.deletion.CASCADE)), + ("reference", models.ForeignKey("references.Reference", on_delete=django.db.models.deletion.CASCADE, null=True, blank=True)), + # Author tracking fields + ("created_by_id", models.IntegerField(null=True, blank=True)), + ("updated_by_id", models.IntegerField(null=True, blank=True)), + ], + options={ + "abstract": False, + }, + ), + + migrations.CreateModel( + name="Dye", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created", model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name="created")), + ("modified", model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name="modified")), + ("name", models.CharField(max_length=255, db_index=True)), + ("slug", models.SlugField(max_length=100, unique=True)), # Increased from default 50 to accommodate long names + ("synonyms", ArrayField(models.CharField(max_length=255), blank=True, default=list)), + ("structural_status", models.CharField(max_length=20, choices=[("DEFINED", "Defined Structure"), ("PROPRIETARY", "Proprietary / Unknown Structure")], default="DEFINED")), + ("canonical_smiles", models.TextField(blank=True)), + ("inchi", models.TextField(blank=True)), + ("inchikey", models.CharField(max_length=27, blank=True, db_index=True)), + ("molblock", models.TextField(blank=True, help_text="V3000 Molfile for precise rendering")), + ("parent_mixture", models.ForeignKey("self", on_delete=django.db.models.deletion.SET_NULL, null=True, blank=True, related_name="isomers")), + ("chemical_class", models.CharField(max_length=100, blank=True, db_index=True)), + ("equilibrium_constant_klz", models.FloatField(null=True, blank=True, help_text="Equilibrium constant between non-fluorescent lactone and fluorescent zwitterion.")), + # Product mixin fields + ("manufacturer", models.CharField(max_length=128, blank=True)), + ("part", models.CharField(max_length=128, blank=True)), + ("url", models.URLField(blank=True)), + # Author tracking fields + ("created_by_id", models.IntegerField(null=True, blank=True)), + ("updated_by_id", models.IntegerField(null=True, blank=True)), + ], + options={ + "constraints": [ + models.UniqueConstraint( + fields=["inchikey"], + name="unique_defined_molecule", + condition=models.Q(structural_status="DEFINED"), + ) + ], + }, + ), + + migrations.CreateModel( + name="DyeState", + fields=[ + ("fluorophore_ptr", models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to="proteins.fluorophore")), + ("name", models.CharField(max_length=255, help_text="e.g., 'Bound to DNA' or 'In Methanol'")), + ("solvent", models.CharField(max_length=100, default="PBS")), + ("ph", models.FloatField(default=7.4)), + ("environment", models.CharField(max_length=20, choices=[], default="FREE")), + ("is_reference", models.BooleanField(default=False, help_text="If True, this is the default state shown on the dye summary card.")), + ("dye", models.ForeignKey("Dye", on_delete=django.db.models.deletion.CASCADE, related_name="states")), + ], + options={ + "abstract": False, + }, + bases=("proteins.fluorophore",), + ), + + # Re-create State model as MTI child of Fluorophore + migrations.CreateModel( + name="State", + fields=[ + ("fluorophore_ptr", models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to="proteins.fluorophore")), + ("name", models.CharField(max_length=64, default="default")), + ("protein", models.ForeignKey("Protein", related_name="states", help_text="The protein to which this state belongs", on_delete=django.db.models.deletion.CASCADE)), + ("maturation", models.FloatField(null=True, blank=True, help_text="Maturation time (min)", validators=[MinValueValidator(0), MaxValueValidator(1600)])), + ], + options={ + "abstract": False, + }, + bases=("proteins.fluorophore",), + ), + + # Add owner_fluor to Spectrum (nullable for now) + migrations.AddField( + model_name="spectrum", + name="owner_fluor", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="spectra", + to="proteins.fluorophore", + ), + ), + + # Add fluor FK to OcFluorEff (nullable for now) + migrations.AddField( + model_name="ocfluoreff", + name="fluor", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="oc_effs", + to="proteins.fluorophore", + ), + ), + ] diff --git a/backend/proteins/migrations/0060_migrate_data_from_old_schema.py b/backend/proteins/migrations/0060_migrate_data_from_old_schema.py new file mode 100644 index 000000000..37d9c0766 --- /dev/null +++ b/backend/proteins/migrations/0060_migrate_data_from_old_schema.py @@ -0,0 +1,348 @@ +# Generated manually for schema overhaul + +from django.db import migrations + + +def migrate_state_data(apps, schema_editor): + """Migrate State data from old schema to new Fluorophore + State MTI structure.""" + # Get models from migration state + Fluorophore = apps.get_model("proteins", "Fluorophore") + State = apps.get_model("proteins", "State") + Protein = apps.get_model("proteins", "Protein") + FluorescenceMeasurement = apps.get_model("proteins", "FluorescenceMeasurement") + + # Access old State data directly via raw SQL + with schema_editor.connection.cursor() as cursor: + cursor.execute(""" + SELECT id, created, modified, name, slug, is_dark, + ex_max, em_max, ext_coeff, qy, brightness, + lifetime, pka, twop_ex_max, "twop_peakGM", twop_qy, + maturation, protein_id + FROM proteins_state_old + """) + + for row in cursor.fetchall(): + (old_id, created, modified, name, slug, is_dark, + ex_max, em_max, ext_coeff, qy, brightness, + lifetime, pka, twop_ex_max, twop_peakgm, twop_qy, + maturation, protein_id) = row + + # Get protein for label + try: + protein = Protein.objects.get(id=protein_id) + label = f"{protein.name} ({name})" if name and name != "default" else protein.name + # Handle empty/null slugs with guaranteed non-empty fallback + if not slug or (isinstance(slug, str) and slug.strip() == ""): + # Try protein slug + name, or protein slug, or fallback to state ID + if name and name != "default": + state_slug = f"{protein.slug}-{name}" if protein.slug else f"state-{old_id}" + else: + state_slug = protein.slug if protein.slug else f"state-{old_id}" + else: + state_slug = slug + + # Final safety check - ensure slug is not empty + if not state_slug or state_slug.strip() == "": + state_slug = f"state-{old_id}" + + except Protein.DoesNotExist: + print(f"Warning: Protein {protein_id} not found for State {old_id}, skipping") + continue + + # Ensure slug uniqueness by checking if it already exists + base_slug = state_slug + counter = 1 + while Fluorophore.objects.filter(slug=state_slug).exists(): + state_slug = f"{base_slug}-{counter}" + counter += 1 + + # Create Fluorophore parent (MTI will link automatically) + fluorophore = Fluorophore.objects.create( + created=created, + modified=modified, + label=label, + slug=state_slug, + entity_type="protein", + ex_max=ex_max, + em_max=em_max, + ext_coeff=ext_coeff, + qy=qy, + brightness=brightness, + lifetime=lifetime, + pka=pka, + twop_ex_max=twop_ex_max, + twop_peakGM=twop_peakgm, # Map from SQL result to model field + twop_qy=twop_qy, + is_dark=is_dark, + + + ) + + # Create State (MTI child) pointing to the Fluorophore + # Use raw SQL to avoid Django MTI trying to update parent with empty values + cursor.execute(""" + INSERT INTO proteins_state (fluorophore_ptr_id, name, protein_id, maturation) + VALUES (%s, %s, %s, %s) + """, [fluorophore.pk, name, protein_id, maturation]) + + # Create FluorescenceMeasurement from old State data if there's any fluorescence data + if any([ex_max, em_max, qy, ext_coeff, lifetime, pka]): + # Get protein's primary reference (may be None) + reference = protein.primary_reference if hasattr(protein, 'primary_reference') else None + reference_id = protein.primary_reference_id if hasattr(protein, 'primary_reference_id') else None + + FluorescenceMeasurement.objects.create( + fluorophore=fluorophore, + reference_id=reference_id, + ex_max=ex_max, + em_max=em_max, + ext_coeff=ext_coeff, + qy=qy, + brightness=brightness, + lifetime=lifetime, + pka=pka, + twop_ex_max=twop_ex_max, + twop_peakGM=twop_peakgm, + twop_qy=twop_qy, + is_dark=is_dark, + is_trusted=True, # Mark as trusted since it's the original data + + + ) + + print(f"Migrated {State.objects.count()} State records") + + +def migrate_dye_data(apps, schema_editor): + """Migrate Dye data from old schema to new Dye container + DyeState structure.""" + Fluorophore = apps.get_model("proteins", "Fluorophore") + Dye = apps.get_model("proteins", "Dye") + DyeState = apps.get_model("proteins", "DyeState") + FluorescenceMeasurement = apps.get_model("proteins", "FluorescenceMeasurement") + + # Access old Dye data via raw SQL + with schema_editor.connection.cursor() as cursor: + cursor.execute(""" + SELECT id, created, modified, name, slug, is_dark, + ex_max, em_max, ext_coeff, qy, brightness, + lifetime, pka, twop_ex_max, "twop_peakGM", twop_qy + FROM proteins_dye_old + """) + + for row in cursor.fetchall(): + (old_id, created, modified, name, slug, is_dark, + ex_max, em_max, ext_coeff, qy, brightness, + lifetime, pka, twop_ex_max, twop_peakgm, twop_qy) = row + + # Handle empty/null slugs + if not slug or slug.strip() == "": + dye_slug = f"dye-{old_id}" # Use old ID as fallback + else: + dye_slug = slug + + # Ensure Dye slug uniqueness + base_dye_slug = dye_slug + counter = 1 + while Dye.objects.filter(slug=dye_slug).exists(): + dye_slug = f"{base_dye_slug}-{counter}" + counter += 1 + + # Old Dye schema doesn't have chemical structure fields + # Mark all as PROPRIETARY to avoid unique constraint issues + # (Can be updated later with actual chemical data) + + # Create Dye container (without fluorescence properties) + dye = Dye.objects.create( + created=created, + modified=modified, + name=name, + slug=dye_slug, + inchikey="", # No chemical data in old schema + structural_status="PROPRIETARY", # Safe default for old dyes + + + # Note: Other fields like smiles, inchi, etc. can be added later + # if they exist in the old schema + ) + + # Create Fluorophore for this DyeState + # Ensure unique fluorophore slug + fluorophore_slug = f"{dye_slug}-default" + base_fluor_slug = fluorophore_slug + counter = 1 + while Fluorophore.objects.filter(slug=fluorophore_slug).exists(): + fluorophore_slug = f"{base_fluor_slug}-{counter}" + counter += 1 + + # Don't pass emhex/exhex - the save() method will compute them from wavelengths + fluorophore = Fluorophore.objects.create( + created=created, + modified=modified, + label=name, + slug=fluorophore_slug, + entity_type="dye", + ex_max=ex_max, + em_max=em_max, + ext_coeff=ext_coeff, + qy=qy, + brightness=brightness, + lifetime=lifetime, + pka=pka, + twop_ex_max=twop_ex_max, + twop_peakGM=twop_peakgm, + twop_qy=twop_qy, + is_dark=is_dark, + + + ) + + # Create DyeState (one per old Dye) + # Use raw SQL to avoid Django MTI trying to update parent with empty values + cursor.execute(""" + INSERT INTO proteins_dyestate (fluorophore_ptr_id, dye_id, name, solvent, ph, environment, is_reference) + VALUES (%s, %s, %s, %s, %s, %s, %s) + """, [fluorophore.pk, dye.pk, "default", "PBS", 7.4, "FREE", True]) + + # Create FluorescenceMeasurement from old Dye data + if any([ex_max, em_max, qy, ext_coeff, lifetime, pka]): + # For dyes, we don't have a primary_reference concept in old schema + # We'll leave reference as None for now + FluorescenceMeasurement.objects.create( + fluorophore=fluorophore, + reference_id=None, + ex_max=ex_max, + em_max=em_max, + ext_coeff=ext_coeff, + qy=qy, + brightness=brightness, + lifetime=lifetime, + pka=pka, + twop_ex_max=twop_ex_max, + twop_peakGM=twop_peakgm, + twop_qy=twop_qy, + is_dark=is_dark, + is_trusted=True, + + + ) + + print(f"Migrated {Dye.objects.count()} Dye records to Dye containers") + print(f"Created {DyeState.objects.count()} DyeState records") + + +def update_spectrum_ownership(apps, schema_editor): + """Update Spectrum foreign keys to point to new Fluorophore records.""" + Spectrum = apps.get_model("proteins", "Spectrum") + Fluorophore = apps.get_model("proteins", "Fluorophore") + + # We need to map old State/Dye IDs to new Fluorophore IDs + # This is tricky because we need to query the old tables + + with schema_editor.connection.cursor() as cursor: + # Update spectra that were owned by States + cursor.execute(""" + UPDATE proteins_spectrum s + SET owner_fluor_id = ( + SELECT f.id + FROM proteins_fluorophore f + JOIN proteins_state ns ON ns.fluorophore_ptr_id = f.id + JOIN proteins_state_old os ON os.slug = f.slug + WHERE os.id = s.owner_state_id + ) + WHERE s.owner_state_id IS NOT NULL + """) + + state_count = cursor.rowcount + print(f"Updated {state_count} spectra owned by States") + + # Update spectra that were owned by Dyes + # Note: DyeState slug is "{dye_slug}-default", so we need to match carefully + cursor.execute(""" + UPDATE proteins_spectrum s + SET owner_fluor_id = ( + SELECT f.id + FROM proteins_fluorophore f + JOIN proteins_dyestate ds ON ds.fluorophore_ptr_id = f.id + JOIN proteins_dye d ON d.id = ds.dye_id + JOIN proteins_dye_old od ON od.slug = d.slug + WHERE od.id = s.owner_dye_id + ) + WHERE s.owner_dye_id IS NOT NULL + """) + + dye_count = cursor.rowcount + print(f"Updated {dye_count} spectra owned by Dyes") + + +def update_ocfluoreff(apps, schema_editor): + """Update OcFluorEff to use direct FK to Fluorophore.""" + with schema_editor.connection.cursor() as cursor: + # Update OcFluorEff records that pointed to States via GenericFK + cursor.execute(""" + UPDATE proteins_ocfluoreff o + SET fluor_id = ( + SELECT f.id + FROM proteins_fluorophore f + JOIN proteins_state s ON s.fluorophore_ptr_id = f.id + WHERE s.fluorophore_ptr_id IN ( + SELECT fluorophore_ptr_id + FROM proteins_state ps + JOIN proteins_state_old os ON os.slug = ( + SELECT slug FROM proteins_fluorophore WHERE id = ps.fluorophore_ptr_id + ) + WHERE os.id = o.object_id::integer + ) + ) + WHERE o.content_type_id = ( + SELECT id FROM django_content_type + WHERE app_label = 'proteins' AND model = 'state' + ) + """) + + state_count = cursor.rowcount + print(f"Updated {state_count} OcFluorEff records that pointed to States") + + # Update OcFluorEff records that pointed to Dyes via GenericFK + cursor.execute(""" + UPDATE proteins_ocfluoreff o + SET fluor_id = ( + SELECT f.id + FROM proteins_fluorophore f + JOIN proteins_dyestate ds ON ds.fluorophore_ptr_id = f.id + JOIN proteins_dye d ON d.id = ds.dye_id + JOIN proteins_dye_old od ON od.slug = d.slug + WHERE od.id = o.object_id::integer + ) + WHERE o.content_type_id = ( + SELECT id FROM django_content_type + WHERE app_label = 'proteins' AND model = 'dye' + ) + """) + + dye_count = cursor.rowcount + print(f"Updated {dye_count} OcFluorEff records that pointed to Dyes") + + +def migrate_forward(apps, schema_editor): + """Run all migration functions.""" + print("Starting data migration from old schema...") + migrate_state_data(apps, schema_editor) + migrate_dye_data(apps, schema_editor) + update_spectrum_ownership(apps, schema_editor) + update_ocfluoreff(apps, schema_editor) + print("Data migration complete!") + + +def migrate_reverse(apps, schema_editor): + """Reverse migration - not implemented (too complex).""" + raise NotImplementedError("Reversing this migration is not supported") + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0059_add_fluorophore_and_new_models"), + ] + + operations = [ + migrations.RunPython(migrate_forward, migrate_reverse), + ] diff --git a/backend/proteins/migrations/0061_cleanup_old_schema.py b/backend/proteins/migrations/0061_cleanup_old_schema.py new file mode 100644 index 000000000..bc9252d36 --- /dev/null +++ b/backend/proteins/migrations/0061_cleanup_old_schema.py @@ -0,0 +1,67 @@ +# Generated manually for schema overhaul + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("proteins", "0060_migrate_data_from_old_schema"), + ] + + operations = [ + # Step 1: Drop old tables that we renamed in 0059 + migrations.RunSQL( + sql="DROP TABLE IF EXISTS proteins_state_old CASCADE;", + reverse_sql=migrations.RunSQL.noop, + ), + migrations.RunSQL( + sql="DROP TABLE IF EXISTS proteins_dye_old CASCADE;", + reverse_sql=migrations.RunSQL.noop, + ), + + # Step 2: Remove old foreign keys from Spectrum + migrations.RemoveField( + model_name="spectrum", + name="owner_state", + ), + migrations.RemoveField( + model_name="spectrum", + name="owner_dye", + ), + + # Step 3: Make owner_fluor non-nullable now that all data is migrated + # First, verify no nulls exist (will fail if there are any) + migrations.RunSQL( + sql=""" + DO $$ + BEGIN + IF EXISTS (SELECT 1 FROM proteins_spectrum WHERE owner_fluor_id IS NULL AND (owner_filter_id IS NULL AND owner_light_id IS NULL AND owner_camera_id IS NULL)) THEN + RAISE EXCEPTION 'Found Spectrum records with no owner after migration!'; + END IF; + END $$; + """, + reverse_sql=migrations.RunSQL.noop, + ), + + # Step 4: Remove GenericForeignKey fields from OcFluorEff + migrations.RemoveField( + model_name="ocfluoreff", + name="content_type", + ), + migrations.RemoveField( + model_name="ocfluoreff", + name="object_id", + ), + + # Step 5: Make fluor FK on OcFluorEff non-nullable + migrations.AlterField( + model_name="ocfluoreff", + name="fluor", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="oc_effs", + to="proteins.fluorophore", + ), + ), + ] diff --git a/backend/proteins/migrations_old/0001_initial.py b/backend/proteins/migrations_old/0001_initial.py deleted file mode 100644 index 48d52dce1..000000000 --- a/backend/proteins/migrations_old/0001_initial.py +++ /dev/null @@ -1,799 +0,0 @@ -# Generated by Django 1.11.9 on 2018-02-13 01:08 - -import uuid - -import django.contrib.postgres.fields -import django.core.validators -import django.db.models.deletion -import django.utils.timezone -import model_utils.fields -from django.conf import settings -from django.contrib.postgres.operations import TrigramExtension -from django.db import migrations, models - -import proteins.fields -import proteins.validators - - -class Migration(migrations.Migration): - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("references", "0001_initial"), - ] - - operations = [ - TrigramExtension(), - migrations.CreateModel( - name="BleachMeasurement", - fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), - ), - ( - "rate", - models.FloatField( - help_text="Photobleaching half-life (s)", - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(3000), - ], - verbose_name="Bleach Rate", - ), - ), - ( - "power", - models.FloatField( - blank=True, - help_text="If not reported, use '-1'", - null=True, - validators=[django.core.validators.MinValueValidator(-1)], - verbose_name="Illumination Power", - ), - ), - ( - "units", - models.CharField(blank=True, help_text="e.g. W/cm2", max_length=100, verbose_name="Power Unit"), - ), - ( - "light", - models.CharField( - blank=True, - choices=[("a", "Arc-lamp"), ("la", "Laser"), ("le", "LED"), ("o", "Other")], - max_length=2, - verbose_name="Light Source", - ), - ), - ( - "modality", - models.CharField( - blank=True, - choices=[ - ("wf", "Widefield"), - ("ps", "Point Scanning Confocal"), - ("sd", "Spinning Disc Confocal"), - ("s", "Spectrophotometer"), - ("t", "TIRF"), - ("o", "Other"), - ], - max_length=2, - verbose_name="Imaging Modality", - ), - ), - ("temp", models.FloatField(blank=True, null=True, verbose_name="Temperature")), - ( - "fusion", - models.CharField( - blank=True, help_text="(if applicable)", max_length=60, verbose_name="Fusion Protein" - ), - ), - ( - "in_cell", - models.IntegerField( - blank=True, - choices=[(-1, "Unkown"), (0, "No"), (1, "Yes")], - default=-1, - help_text="protein expressed in living cells", - verbose_name="In cells?", - ), - ), - ( - "created_by", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="bleachmeasurement_author", - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "reference", - models.ForeignKey( - blank=True, - help_text="Reference where the measurement was made", - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="bleach_measurements", - to="references.Reference", - verbose_name="Measurement Reference", - ), - ), - ], - options={ - "abstract": False, - }, - ), - migrations.CreateModel( - name="FRETpair", - fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), - ), - ("radius", models.FloatField(blank=True, null=True)), - ], - options={ - "verbose_name": "FRET Pair", - }, - ), - migrations.CreateModel( - name="Mutation", - fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ( - "mutations", - django.contrib.postgres.fields.ArrayField( - base_field=models.CharField(max_length=5), - size=None, - validators=[ - django.core.validators.RegexValidator( - "^[ACDEFGHIKLMNPQRSTVWY-][1-9][0-9]{0,2}[ACDEFGHIKLMNPQRSTVWY]$", - "not a valid mutation code: eg S65T", - ) - ], - ), - ), - ], - ), - migrations.CreateModel( - name="Organism", - fields=[ - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), - ), - ( - "id", - models.PositiveIntegerField( - help_text="NCBI Taxonomy ID", primary_key=True, serialize=False, verbose_name="Taxonomy ID" - ), - ), - ("scientific_name", models.CharField(blank=True, max_length=128)), - ("division", models.CharField(blank=True, max_length=128)), - ("common_name", models.CharField(blank=True, max_length=128)), - ("species", models.CharField(blank=True, max_length=128)), - ("genus", models.CharField(blank=True, max_length=128)), - ("rank", models.CharField(blank=True, max_length=128)), - ( - "created_by", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="organism_author", - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "updated_by", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="organism_modifier", - to=settings.AUTH_USER_MODEL, - ), - ), - ], - options={ - "verbose_name": "Organism", - "ordering": ["scientific_name"], - }, - ), - migrations.CreateModel( - name="Protein", - fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), - ), - ( - "status", - model_utils.fields.StatusField( - choices=[("pending", "pending"), ("approved", "approved"), ("hidden", "hidden")], - default="pending", - max_length=100, - no_check_for_status=True, - verbose_name="status", - ), - ), - ( - "status_changed", - model_utils.fields.MonitorField( - default=django.utils.timezone.now, monitor="status", verbose_name="status changed" - ), - ), - ("uuid", models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), - ("name", models.CharField(db_index=True, help_text="Name of the fluorescent protein", max_length=128)), - ("slug", models.SlugField(help_text="URL slug for the protein", max_length=64, unique=True)), - ("base_name", models.CharField(max_length=128)), - ( - "aliases", - django.contrib.postgres.fields.ArrayField( - base_field=models.CharField(max_length=200), blank=True, null=True, size=None - ), - ), - ("chromophore", models.CharField(blank=True, max_length=5, null=True)), - ( - "seq", - models.CharField( - blank=True, - help_text="Amino acid sequence (IPG ID is preferred)", - max_length=1024, - null=True, - unique=True, - validators=[proteins.validators.protein_sequence_validator], - verbose_name="Sequence", - ), - ), - ( - "pdb", - django.contrib.postgres.fields.ArrayField( - base_field=models.CharField(max_length=4), - blank=True, - null=True, - size=None, - verbose_name="Protein DataBank ID", - ), - ), - ( - "genbank", - models.CharField( - blank=True, - help_text="NCBI Genbank Accession", - max_length=12, - null=True, - unique=True, - verbose_name="Genbank Accession", - ), - ), - ( - "uniprot", - models.CharField( - blank=True, - max_length=10, - null=True, - unique=True, - validators=[ - django.core.validators.RegexValidator( - "[OPQ][0-9][A-Z0-9]{3}[0-9]|[A-NR-Z][0-9]([A-Z][A-Z0-9]{2}[0-9]){1,2}", - "Not a valid UniProt Accession", - ) - ], - verbose_name="UniProtKB Accession", - ), - ), - ( - "ipg_id", - models.CharField( - blank=True, - help_text="Identical Protein Group ID at Pubmed", - max_length=12, - null=True, - unique=True, - verbose_name="IPG ID", - ), - ), - ("mw", models.FloatField(blank=True, help_text="Molecular Weight", null=True)), - ( - "agg", - models.CharField( - blank=True, - choices=[ - ("m", "Monomer"), - ("d", "Dimer"), - ("td", "Tandem dimer"), - ("wd", "Weak dimer"), - ("t", "Tetramer"), - ], - help_text="Oligomerization tendency", - max_length=2, - ), - ), - ( - "switch_type", - models.CharField( - blank=True, - choices=[ - ("b", "Basic"), - ("pa", "Photoactivatable"), - ("ps", "Photoswitchable"), - ("pc", "Photoconvertible"), - ("t", "Timer"), - ("o", "Multistate"), - ], - help_text="Photoswitching type (basic if none)", - max_length=2, - verbose_name="Type", - ), - ), - ("blurb", models.CharField(blank=True, help_text="Brief descriptive blurb", max_length=512)), - ( - "FRET_partner", - models.ManyToManyField(blank=True, through="proteins.FRETpair", to="proteins.Protein"), - ), - ( - "created_by", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="protein_author", - to=settings.AUTH_USER_MODEL, - ), - ), - ], - options={ - "ordering": ["name"], - }, - ), - migrations.CreateModel( - name="ProteinCollection", - fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), - ), - ("name", models.CharField(max_length=100)), - ("description", models.CharField(blank=True, max_length=512)), - ( - "private", - models.BooleanField( - default=False, - help_text="Private collections can not be seen by or shared with other users", - verbose_name="Private Collection", - ), - ), - ( - "owner", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="collections", - to=settings.AUTH_USER_MODEL, - verbose_name="Protein Collection", - ), - ), - ("proteins", models.ManyToManyField(related_name="collection_memberships", to="proteins.Protein")), - ], - ), - migrations.CreateModel( - name="State", - fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), - ), - ("name", models.CharField(default="default", max_length=64)), - ("slug", models.SlugField(help_text="Unique slug for the state", max_length=128, unique=True)), - ( - "is_dark", - models.BooleanField( - default=False, help_text="This state does not fluorescence", verbose_name="Dark State" - ), - ), - ( - "ex_max", - models.PositiveSmallIntegerField( - blank=True, - db_index=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(300), - django.core.validators.MaxValueValidator(900), - ], - ), - ), - ( - "em_max", - models.PositiveSmallIntegerField( - blank=True, - db_index=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(300), - django.core.validators.MaxValueValidator(1000), - ], - ), - ), - ( - "ex_spectra", - proteins.fields.SpectrumField( - blank=True, help_text="List of [[wavelength, value],...] pairs", null=True - ), - ), - ( - "em_spectra", - proteins.fields.SpectrumField( - blank=True, help_text="List of [[wavelength, value],...] pairs", null=True - ), - ), - ( - "ext_coeff", - models.IntegerField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(300000), - ], - verbose_name="Extinction Coefficient", - ), - ), - ( - "qy", - models.FloatField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(1), - ], - verbose_name="Quantum Yield", - ), - ), - ("brightness", models.FloatField(blank=True, editable=False, null=True)), - ( - "pka", - models.FloatField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(2), - django.core.validators.MaxValueValidator(12), - ], - verbose_name="pKa", - ), - ), - ( - "maturation", - models.FloatField( - blank=True, - help_text="Maturation time (min)", - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(1600), - ], - ), - ), - ( - "lifetime", - models.FloatField( - blank=True, - help_text="Lifetime (ns)", - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(20), - ], - ), - ), - ( - "created_by", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="state_author", - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "protein", - models.ForeignKey( - help_text="The protein to which this state belongs", - on_delete=django.db.models.deletion.CASCADE, - related_name="states", - to="proteins.Protein", - ), - ), - ], - options={ - "verbose_name": "State", - }, - ), - migrations.CreateModel( - name="StateTransition", - fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), - ), - ( - "trans_wave", - models.PositiveSmallIntegerField( - blank=True, - help_text="Wavelength required", - null=True, - validators=[ - django.core.validators.MinValueValidator(300), - django.core.validators.MaxValueValidator(1000), - ], - verbose_name="Transition Wavelength", - ), - ), - ( - "from_state", - models.ForeignKey( - help_text="The initial state ", - on_delete=django.db.models.deletion.CASCADE, - related_name="transitions_from", - to="proteins.State", - verbose_name="From state", - ), - ), - ( - "protein", - models.ForeignKey( - help_text="The protein that demonstrates this transition", - on_delete=django.db.models.deletion.CASCADE, - related_name="transitions", - to="proteins.Protein", - verbose_name="Protein Transitioning", - ), - ), - ( - "to_state", - models.ForeignKey( - help_text="The state after transition", - on_delete=django.db.models.deletion.CASCADE, - related_name="transitions_to", - to="proteins.State", - verbose_name="To state", - ), - ), - ], - options={ - "abstract": False, - }, - ), - migrations.AddField( - model_name="state", - name="transitions", - field=models.ManyToManyField( - blank=True, - related_name="transition_state", - through="proteins.StateTransition", - to="proteins.State", - verbose_name="State Transitions", - ), - ), - migrations.AddField( - model_name="state", - name="updated_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="state_modifier", - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AddField( - model_name="protein", - name="default_state", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="default_for", - to="proteins.State", - ), - ), - migrations.AddField( - model_name="protein", - name="parent_organism", - field=models.ForeignKey( - blank=True, - help_text="Organism from which the protein was engineered", - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="proteins", - to="proteins.Organism", - verbose_name="Parental organism", - ), - ), - migrations.AddField( - model_name="protein", - name="primary_reference", - field=models.ForeignKey( - blank=True, - help_text="Preferably the publication that introduced the protein", - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="primary_proteins", - to="references.Reference", - verbose_name="Primary Reference", - ), - ), - migrations.AddField( - model_name="protein", - name="references", - field=models.ManyToManyField(blank=True, related_name="proteins", to="references.Reference"), - ), - migrations.AddField( - model_name="protein", - name="updated_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="protein_modifier", - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AddField( - model_name="mutation", - name="parent", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="proteins", - to="proteins.Protein", - verbose_name="Parent Protein", - ), - ), - migrations.AddField( - model_name="fretpair", - name="acceptor", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="FK_FRETacceptor_protein", - to="proteins.Protein", - verbose_name="acceptor", - ), - ), - migrations.AddField( - model_name="fretpair", - name="created_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="fretpair_author", - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AddField( - model_name="fretpair", - name="donor", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="FK_FRETdonor_protein", - to="proteins.Protein", - verbose_name="donor", - ), - ), - migrations.AddField( - model_name="fretpair", - name="pair_references", - field=models.ManyToManyField(blank=True, related_name="FK_FRETpair_reference", to="references.Reference"), - ), - migrations.AddField( - model_name="fretpair", - name="updated_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="fretpair_modifier", - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AddField( - model_name="bleachmeasurement", - name="state", - field=models.ForeignKey( - help_text="The state on which this measurement was made", - on_delete=django.db.models.deletion.CASCADE, - related_name="bleach_measurements", - to="proteins.State", - verbose_name="Protein (state)", - ), - ), - migrations.AddField( - model_name="bleachmeasurement", - name="updated_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="bleachmeasurement_modifier", - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AlterUniqueTogether( - name="state", - unique_together={("protein", "ex_max", "em_max", "ext_coeff", "qy")}, - ), - migrations.AlterUniqueTogether( - name="proteincollection", - unique_together={("owner", "name")}, - ), - ] From f70f57c9b7871cd9f61f30909777a28c8b0c7409 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sat, 22 Nov 2025 11:38:47 -0500 Subject: [PATCH 16/57] add notes --- backend/proteins/migrations/0059_notes.md | 285 ++++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 backend/proteins/migrations/0059_notes.md diff --git a/backend/proteins/migrations/0059_notes.md b/backend/proteins/migrations/0059_notes.md new file mode 100644 index 000000000..1c42dd000 --- /dev/null +++ b/backend/proteins/migrations/0059_notes.md @@ -0,0 +1,285 @@ +# Migration Strategy: Full History Preservation (No Squashing) + +## Overview + +Move all 58 old migrations back, then add incremental migrations to transform the schema while maintaining ability to migrate any historical backup. + +## File Structure Changes + +**Before:** + +``` +migrations/ +├── 0001_initial.py (NEW schema, complete) +├── 0002_update_ocfluoreff_to_use_direct_fk.py +migrations_old/ +├── 0001_initial.py through 0058_*.py (OLD schema) +``` + +**After:** + +``` +migrations/ +├── 0001_initial.py (moved from migrations_old) +├── 0002_* through 0058_* (moved from migrations_old) +├── 0059_add_fluorophore_and_new_models.py (NEW) +├── 0060_convert_state_to_mti.py (NEW) +├── 0061_migrate_dye_to_container_and_dyestate.py (NEW) +├── 0062_migrate_data.py (NEW - RunPython) +├── 0063_update_foreign_keys.py (NEW) +├── 0064_cleanup_old_fields.py (NEW) +``` + +## Migration Sequence + +### Migration 0059: Add Fluorophore and New Models + +**Purpose:** Create new schema elements non-destructively + +**Operations:** + +1. Create `Fluorophore` model (standalone for now) +2. Create `FluorescenceMeasurement` model with FK to Fluorophore +3. Create `DyeState` model (MTI child of Fluorophore) +4. Create NEW `Dye` container model with temp table name `proteins_dye_new` +5. Add `owner_fluor` FK to Spectrum (nullable, for transition) +6. Add `fluor` FK to OcFluorEff (nullable, for transition) + +**Key:** All additions, no deletions. Old schema still works. + +### Migration 0060: Convert State to MTI Child + +**Purpose:** Make existing State model inherit from Fluorophore + +**Operations:** + +1. Add `fluorophore_ptr` OneToOneField to State (nullable initially) +2. For each existing State record: + - Create corresponding Fluorophore record + - Set State.fluorophore_ptr +3. Make fluorophore_ptr non-nullable +4. Add MTI meta options to State + +**Challenge:** This is complex in Django. Alternative approach: + +- Create StateNew with proper MTI +- Copy data State → StateNew +- Rename tables later + +**Decision needed:** Which approach for State MTI? + +### Migration 0061: Migrate Dye → Container + DyeState + +**Purpose:** Split old Dye into container Dye + DyeState + +**RunPython Operations:** + +```python +def migrate_dyes(apps, schema_editor): + OldDye = apps.get_model('proteins', 'Dye') # Uses old table + NewDye = apps.get_model('proteins', 'DyeNew') # Temp container + DyeState = apps.get_model('proteins', 'DyeState') + + for old_dye in OldDye.objects.all(): + # Create container (copy all non-fluorescence fields) + new_dye = NewDye.objects.create( + name=old_dye.name, + slug=old_dye.slug, + # Copy: description, aliases, references, etc. + ) + + # Create DyeState (one per old Dye) + # Fluorophore parent auto-created by MTI + DyeState.objects.create( + dye=new_dye, + # Fluorophore fields: + label=old_dye.name, + slug=old_dye.slug, + entity_type='dye', + ) +``` + +### Migration 0062: Migrate Fluorescence Data + +**Purpose:** Create FluorescenceMeasurement records from old State/Dye data + +**RunPython Operations:** + +```python +def migrate_fluorescence_data(apps, schema_editor): + State = apps.get_model('proteins', 'State') + DyeState = apps.get_model('proteins', 'DyeState') + FluorescenceMeasurement = apps.get_model('proteins', 'FluorescenceMeasurement') + + # Migrate State fluorescence data + for state in State.objects.all(): + # Get reference (may be None) + ref = state.protein.primary_reference if state.protein else None + + # Only create if has fluorescence data + if state.ex_max or state.em_max or state.qy: + FluorescenceMeasurement.objects.create( + fluorophore=state.fluorophore_ptr, + reference=ref, + ex_max=state.ex_max, + em_max=state.em_max, + em_std=state.em_std, + ext_coeff=state.ext_coeff, + qy=state.qy, + brightness=state.brightness, + pka=state.pka, + lifetime=state.lifetime, + # ... all other fluorescence fields + ) + # save() triggers rebuild_attributes() → materializes to Fluorophore + + # Migrate DyeState fluorescence data + # (similar pattern) +``` + +### Migration 0063: Update Foreign Keys + +**Purpose:** Point all FKs to new schema + +**RunPython Operations:** + +```python +def update_spectrum_ownership(apps, schema_editor): + Spectrum = apps.get_model('proteins', 'Spectrum') + State = apps.get_model('proteins', 'State') + DyeState = apps.get_model('proteins', 'DyeState') + + # Update spectra owned by States + for spectrum in Spectrum.objects.filter(owner_state__isnull=False): + state = State.objects.get(id=spectrum.owner_state_id) + spectrum.owner_fluor = state.fluorophore_ptr + spectrum.save(update_fields=['owner_fluor']) + + # Update spectra owned by old Dyes + # Need to find corresponding DyeState by slug + for spectrum in Spectrum.objects.filter(owner_dye__isnull=False): + old_dye = spectrum.owner_dye + dyestate = DyeState.objects.get(slug=old_dye.slug) + spectrum.owner_fluor = dyestate.fluorophore_ptr + spectrum.save(update_fields=['owner_fluor']) + +def update_ocfluoreff(apps, schema_editor): + # Similar pattern - already in current 0002, adapt to 0063 +``` + +### Migration 0064: Cleanup Old Schema + +**Purpose:** Remove old fields and tables + +**Operations:** + +1. Drop fields from State: + - All fluorescence property fields (ex_max, em_max, qy, etc.) + - Keep: protein FK, name, maturation, etc. + +2. Drop fields from Spectrum: + - owner_state FK + - owner_dye FK + - Keep: owner_fluor FK (make non-nullable) + +3. Rename Dye tables: + - Drop old proteins_dye table + - Rename proteins_dye_new → proteins_dye + +4. Drop any other deprecated fields/tables + +## Testing Protocol + +### Test 1: Fresh Install (New Database) + +```bash +dropdb fpbase && createdb fpbase +uv run python backend/manage.py migrate +uv run pytest --create-db +``` + +**Expected:** ✅ Runs 0001-0064, all tests pass + +### Test 2: Production Migration Simulation + +```bash +just pgpull # Drops local, pulls production, runs migrate +``` + +**Expected:** + +- Detects migrations 0001-0058 already applied +- Runs only 0059-0064 +- ✅ Data migrated correctly + +### Test 3: Partial Backup Migration + +```bash +# Restore backup from middle of history (e.g., only 0001-0034 applied) +uv run python backend/manage.py migrate +``` + +**Expected:** + +- Runs 0035-0064 in sequence +- ✅ Migrates successfully + +### Test 4: Data Integrity Verification + +```python +# In shell_plus after migration +# Verify State MTI +assert State.objects.filter(fluorophore_ptr__isnull=True).count() == 0 + +# Verify DyeState created +old_dye_count = 50 # Note before migration +assert DyeState.objects.count() == old_dye_count + +# Verify Spectrum ownership migrated +assert Spectrum.objects.filter(owner_fluor__isnull=True).count() == 0 +assert Spectrum.objects.filter(owner_state__isnull=False).count() == 0 + +# Verify Fluorophore materialized data +for state in State.objects.all(): + if state.protein.primary_reference: + assert state.fluorophore_ptr.ex_max is not None +``` + +## Key Decision Points + +**Q1: How to handle State MTI conversion?** + +- Option A: Add fluorophore_ptr to existing State, populate, convert +- Option B: Create StateNew with MTI, migrate data, rename table +- **Recommendation:** Option A if possible (cleaner), Option B if Django doesn't support it + +**Q2: Order of data migration vs FK updates?** + +- Must create all Fluorophore/DyeState records BEFORE updating Spectrum FKs +- Order: 0059 (tables) → 0060 (State MTI) → 0061 (DyeState) → 0062 (data) → 0063 (FKs) → 0064 (cleanup) + +**Q3: Handle BleachMeasurement, OSERMeasurement FKs?** + +- These reference State via FK +- Need to verify they still work after State becomes MTI child +- May need FK updates in 0063 + +## Implementation Steps + +1. **Move migrations:** `mv migrations_old/* migrations/` +2. **Delete current:** `rm migrations/0001_initial.py migrations/0002_*.py` +3. **Create 0059-0064** with schema operations above +4. **Test with fresh DB:** Verify all 64 migrations run +5. **Test with pgpull:** Verify production migration works +6. **Write verification script:** Data integrity checks +7. **Deploy to production** +8. **Delete migrations_old/** after success + +## Risk Mitigation + +- ✅ Full history preserved forever +- ✅ Any backup can migrate +- ✅ Incremental migrations easier to debug than big-bang +- ✅ Can test each migration step independently +- ⚠️ More migrations = more complexity +- ⚠️ MTI conversion is tricky, needs careful testing From 61458e1b81a6c3a2b098c7f66c5cddcd9cb46c4c Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sat, 22 Nov 2025 11:50:36 -0500 Subject: [PATCH 17/57] revise notes --- backend/proteins/migrations/0059_notes.md | 466 ++++++++++++---------- 1 file changed, 251 insertions(+), 215 deletions(-) diff --git a/backend/proteins/migrations/0059_notes.md b/backend/proteins/migrations/0059_notes.md index 1c42dd000..b725eca8c 100644 --- a/backend/proteins/migrations/0059_notes.md +++ b/backend/proteins/migrations/0059_notes.md @@ -1,285 +1,321 @@ -# Migration Strategy: Full History Preservation (No Squashing) +# Schema Overhaul Migration: Full History Preservation (No Squashing) ## Overview -Move all 58 old migrations back, then add incremental migrations to transform the schema while maintaining ability to migrate any historical backup. +This documents the successful migration from the old schema (separate State/Dye models with fluorescence properties) to the new schema (Fluorophore MTI parent with State/DyeState children, Dye containers, and FluorescenceMeasurement tracking). -## File Structure Changes +All 58 old migrations were preserved, with 3 new migrations added to transform the schema while maintaining the ability to migrate any historical backup. -**Before:** +## File Structure -``` -migrations/ -├── 0001_initial.py (NEW schema, complete) -├── 0002_update_ocfluoreff_to_use_direct_fk.py -migrations_old/ -├── 0001_initial.py through 0058_*.py (OLD schema) -``` - -**After:** +**Final Structure:** ``` migrations/ -├── 0001_initial.py (moved from migrations_old) -├── 0002_* through 0058_* (moved from migrations_old) -├── 0059_add_fluorophore_and_new_models.py (NEW) -├── 0060_convert_state_to_mti.py (NEW) -├── 0061_migrate_dye_to_container_and_dyestate.py (NEW) -├── 0062_migrate_data.py (NEW - RunPython) -├── 0063_update_foreign_keys.py (NEW) -├── 0064_cleanup_old_fields.py (NEW) +├── 0001_initial.py through 0058_*.py (preserved from old schema) +├── 0059_add_fluorophore_and_new_models.py (schema transformation) +├── 0060_migrate_data_from_old_schema.py (data migration) +├── 0061_cleanup_old_schema.py (cleanup) ``` ## Migration Sequence ### Migration 0059: Add Fluorophore and New Models -**Purpose:** Create new schema elements non-destructively - -**Operations:** +**Purpose:** Create complete new schema in one step while preserving old tables for data migration. -1. Create `Fluorophore` model (standalone for now) -2. Create `FluorescenceMeasurement` model with FK to Fluorophore -3. Create `DyeState` model (MTI child of Fluorophore) -4. Create NEW `Dye` container model with temp table name `proteins_dye_new` -5. Add `owner_fluor` FK to Spectrum (nullable, for transition) -6. Add `fluor` FK to OcFluorEff (nullable, for transition) +**Approach:** Used `SeparateDatabaseAndState` to handle the State/Dye table transitions cleanly: +- **Database operations:** Copy old tables to `*_old` versions, then drop originals +- **State operations:** Delete old models from Django's migration state + +**Models Created:** + +1. **Fluorophore** (MTI parent): + - Fields: label, slug (max_length=100), entity_type, ex_max, em_max, qy, brightness, etc. + - Slug increased to 100 chars to accommodate long dye names (e.g., "fluospheres-nile-red-fluorescent-microspheres-default") + - Author tracking via nullable IntegerFields (created_by_id, updated_by_id) to avoid FK dependency on auth + +2. **FluorescenceMeasurement**: + - Tracks individual measurements with reference to source paper + - FK to Fluorophore (required) + - FK to Reference (nullable - some proteins lack primary_reference) + - Fields: all fluorescence properties + date_measured, conditions, is_trusted + +3. **Dye** (container model): + - No fluorescence properties (those moved to DyeState) + - Fields: name, slug (max_length=100), synonyms (ArrayField), structural_status, inchikey, etc. + - Product mixin fields: manufacturer, part, url + - UniqueConstraint on inchikey only for DEFINED status + +4. **DyeState** (MTI child of Fluorophore): + - One DyeState per environmental condition for a Dye + - Fields: dye FK, name, solvent, ph, environment, is_reference + +5. **State** (MTI child of Fluorophore): + - Recreated as MTI child (was previously standalone) + - Fields: protein FK, name, maturation + - Fluorescence properties inherited from Fluorophore parent + +**Foreign Key Additions:** +- Added `owner_fluor` FK to Spectrum (nullable during transition) +- Added `fluor` FK to OcFluorEff (nullable during transition) + +**Key Design Decisions:** +- All additions in one migration for atomic schema change +- Old tables preserved as `*_old` for data migration +- No deletions yet - old schema still accessible via raw SQL + +### Migration 0060: Migrate Data from Old Schema + +**Purpose:** Comprehensive data migration in a single RunPython operation. + +This migration handles all data transformation: State→Fluorophore+State, Dye→Dye+DyeState+Fluorophore, creating measurements, and updating foreign keys. + +**Functions Implemented:** + +#### 1. `migrate_state_data(apps, schema_editor)` + +Transforms each old State record into: +- Fluorophore parent (with materialized fluorescence properties) +- State MTI child (with protein FK, name, maturation) +- FluorescenceMeasurement (if fluorescence data exists) + +**Key Implementation Details:** +- Raw SQL SELECT from `proteins_state_old` table +- Slug generation with uniqueness handling: + - Use existing slug if non-empty + - Fall back to `{protein.slug}-{name}` or `state-{old_id}` + - Deduplicate with counter suffix if conflicts exist +- **MTI child creation via raw SQL INSERT** to avoid Django's save() attempting to update parent: + ```sql + INSERT INTO proteins_state (fluorophore_ptr_id, name, protein_id, maturation) + VALUES (%s, %s, %s, %s) + ``` +- PostgreSQL column quoting: `"twop_peakGM"` in SELECT, mapped to lowercase variable in Python +- FluorescenceMeasurement created with `reference_id=protein.primary_reference_id` (may be None) + +**Migrated:** 1,055 State records + +#### 2. `migrate_dye_data(apps, schema_editor)` + +Transforms each old Dye record into: +- Dye container (no fluorescence properties) +- Fluorophore parent (with materialized fluorescence properties) +- DyeState MTI child (linking Dye to Fluorophore) +- FluorescenceMeasurement (if fluorescence data exists) + +**Key Implementation Details:** +- Slug generation: `{dye_slug}-default` with uniqueness checks +- All old dyes marked as `structural_status="PROPRIETARY"` (old schema had no chemical structure data) +- Empty `inchikey=""` to avoid unique constraint violations +- **MTI child creation via raw SQL INSERT** (same reason as State) +- hex color fields (`emhex`, `exhex`) omitted - computed by model's `save()` method from wavelengths +- Default environment: `solvent="PBS"`, `ph=7.4`, `environment="FREE"` + +**Migrated:** 950 Dye records → 950 Dye containers + 950 DyeStates + +#### 3. `update_spectrum_ownership(apps, schema_editor)` + +Updates Spectrum foreign keys from old `owner_state`/`owner_dye` to new `owner_fluor`. + +**Implementation via raw SQL for performance:** +```sql +-- Update spectra owned by States +UPDATE proteins_spectrum s +SET owner_fluor_id = ( + SELECT f.id FROM proteins_fluorophore f + JOIN proteins_state ns ON ns.fluorophore_ptr_id = f.id + JOIN proteins_state_old os ON os.slug = f.slug + WHERE os.id = s.owner_state_id +) +WHERE s.owner_state_id IS NOT NULL + +-- Update spectra owned by Dyes (similar pattern) +``` -**Key:** All additions, no deletions. Old schema still works. +**Updated:** 1,191 spectra from States + 1,849 spectra from Dyes -### Migration 0060: Convert State to MTI Child +#### 4. `update_ocfluoreff(apps, schema_editor)` -**Purpose:** Make existing State model inherit from Fluorophore +Updates OcFluorEff from GenericForeignKey to direct FK to Fluorophore. -**Operations:** +**Implementation:** Similar SQL pattern matching old content_type/object_id to new Fluorophore records. -1. Add `fluorophore_ptr` OneToOneField to State (nullable initially) -2. For each existing State record: - - Create corresponding Fluorophore record - - Set State.fluorophore_ptr -3. Make fluorophore_ptr non-nullable -4. Add MTI meta options to State +**Updated:** 0 OcFluorEff records (production has no data in this table yet) -**Challenge:** This is complex in Django. Alternative approach: +### Migration 0061: Cleanup Old Schema -- Create StateNew with proper MTI -- Copy data State → StateNew -- Rename tables later +**Purpose:** Remove old tables and fields, finalize the schema transformation. -**Decision needed:** Which approach for State MTI? +**Operations:** -### Migration 0061: Migrate Dye → Container + DyeState +1. **Drop old backup tables:** + - `DROP TABLE proteins_state_old CASCADE` + - `DROP TABLE proteins_dye_old CASCADE` -**Purpose:** Split old Dye into container Dye + DyeState +2. **Remove deprecated Spectrum fields:** + - `owner_state` FK (data migrated to owner_fluor) + - `owner_dye` FK (data migrated to owner_fluor) -**RunPython Operations:** +3. **Remove deprecated OcFluorEff fields:** + - `content_type` (GenericForeignKey component) + - `object_id` (GenericForeignKey component) -```python -def migrate_dyes(apps, schema_editor): - OldDye = apps.get_model('proteins', 'Dye') # Uses old table - NewDye = apps.get_model('proteins', 'DyeNew') # Temp container - DyeState = apps.get_model('proteins', 'DyeState') - - for old_dye in OldDye.objects.all(): - # Create container (copy all non-fluorescence fields) - new_dye = NewDye.objects.create( - name=old_dye.name, - slug=old_dye.slug, - # Copy: description, aliases, references, etc. - ) - - # Create DyeState (one per old Dye) - # Fluorophore parent auto-created by MTI - DyeState.objects.create( - dye=new_dye, - # Fluorophore fields: - label=old_dye.name, - slug=old_dye.slug, - entity_type='dye', - ) -``` +4. **Make fluor FK non-nullable:** + - `OcFluorEff.fluor` now required (was nullable during transition) -### Migration 0062: Migrate Fluorescence Data +5. **Verification step:** + - SQL check: Fail if any Spectrum records have no owner after migration + - Ensures data integrity before making schema changes -**Purpose:** Create FluorescenceMeasurement records from old State/Dye data +## Testing Results -**RunPython Operations:** +### Test 1: Fresh Install (New Database) -```python -def migrate_fluorescence_data(apps, schema_editor): - State = apps.get_model('proteins', 'State') - DyeState = apps.get_model('proteins', 'DyeState') - FluorescenceMeasurement = apps.get_model('proteins', 'FluorescenceMeasurement') - - # Migrate State fluorescence data - for state in State.objects.all(): - # Get reference (may be None) - ref = state.protein.primary_reference if state.protein else None - - # Only create if has fluorescence data - if state.ex_max or state.em_max or state.qy: - FluorescenceMeasurement.objects.create( - fluorophore=state.fluorophore_ptr, - reference=ref, - ex_max=state.ex_max, - em_max=state.em_max, - em_std=state.em_std, - ext_coeff=state.ext_coeff, - qy=state.qy, - brightness=state.brightness, - pka=state.pka, - lifetime=state.lifetime, - # ... all other fluorescence fields - ) - # save() triggers rebuild_attributes() → materializes to Fluorophore - - # Migrate DyeState fluorescence data - # (similar pattern) +**Command:** +```bash +uv run pytest --create-db ``` -### Migration 0063: Update Foreign Keys - -**Purpose:** Point all FKs to new schema +**Result:** ✅ **PASSED** - All 88 backend tests passed +- Migrations 0001-0061 applied successfully +- All protein tests passed +- Schema correctly created from scratch -**RunPython Operations:** +### Test 2: Production Migration Simulation -```python -def update_spectrum_ownership(apps, schema_editor): - Spectrum = apps.get_model('proteins', 'Spectrum') - State = apps.get_model('proteins', 'State') - DyeState = apps.get_model('proteins', 'DyeState') - - # Update spectra owned by States - for spectrum in Spectrum.objects.filter(owner_state__isnull=False): - state = State.objects.get(id=spectrum.owner_state_id) - spectrum.owner_fluor = state.fluorophore_ptr - spectrum.save(update_fields=['owner_fluor']) - - # Update spectra owned by old Dyes - # Need to find corresponding DyeState by slug - for spectrum in Spectrum.objects.filter(owner_dye__isnull=False): - old_dye = spectrum.owner_dye - dyestate = DyeState.objects.get(slug=old_dye.slug) - spectrum.owner_fluor = dyestate.fluorophore_ptr - spectrum.save(update_fields=['owner_fluor']) - -def update_ocfluoreff(apps, schema_editor): - # Similar pattern - already in current 0002, adapt to 0063 +**Command:** +```bash +just pgpull # Drops local DB, pulls from Heroku production, runs migrate ``` -### Migration 0064: Cleanup Old Schema +**Result:** ✅ **PASSED** - Migration completed successfully -**Purpose:** Remove old fields and tables +**Migration Output:** +``` +Operations to perform: + Apply all migrations: account, admin, auth, avatar, contenttypes, favit, proteins, references, reversion, sessions, sites, socialaccount, users +Running migrations: + Applying proteins.0059_add_fluorophore_and_new_models... OK + Applying proteins.0060_migrate_data_from_old_schema... + Starting data migration from old schema... + Migrated 1055 State records + Migrated 950 Dye records to Dye containers + Created 950 DyeState records + Updated 1191 spectra owned by States + Updated 1849 spectra owned by Dyes + Updated 0 OcFluorEff records that pointed to States + Updated 0 OcFluorEff records that pointed to Dyes + Data migration complete! + OK + Applying proteins.0061_cleanup_old_schema... OK +``` -**Operations:** +**Full Test Suite:** +```bash +just test # Runs all backend + e2e tests +``` +**Result:** ✅ **PASSED** - All 54 tests passed in 14.94s -1. Drop fields from State: - - All fluorescence property fields (ex_max, em_max, qy, etc.) - - Keep: protein FK, name, maturation, etc. +### Test 3: Data Integrity Verification -2. Drop fields from Spectrum: - - owner_state FK - - owner_dye FK - - Keep: owner_fluor FK (make non-nullable) +Post-migration checks confirmed: +- ✅ All 1,055 States have Fluorophore parents (no orphans) +- ✅ All 950 Dyes converted to Dye containers with DyeStates +- ✅ All 3,040 Spectra migrated to new `owner_fluor` FK +- ✅ No Spectra with null owners +- ✅ Old `owner_state` and `owner_dye` FKs removed +- ✅ Fluorophore slugs unique (longest: 53 chars, handled by max_length=100) -3. Rename Dye tables: - - Drop old proteins_dye table - - Rename proteins_dye_new → proteins_dye +## Key Technical Challenges & Solutions -4. Drop any other deprecated fields/tables +### Challenge 1: MTI Child Creation in Migrations -## Testing Protocol +**Problem:** Django's MTI `.save()` tries to UPDATE the parent record when creating a child, causing: +``` +IntegrityError: duplicate key value violates unique constraint "proteins_fluorophore_slug_key" +DETAIL: Key (slug)=() already exists. +``` -### Test 1: Fresh Install (New Database) +**Root Cause:** When calling `State.objects.create(fluorophore_ptr=fluorophore, ...)`, Django's MTI machinery attempts to save the parent with empty field values. -```bash -dropdb fpbase && createdb fpbase -uv run python backend/manage.py migrate -uv run pytest --create-db +**Solution:** Use raw SQL INSERT for MTI child tables: +```python +cursor.execute(""" + INSERT INTO proteins_state (fluorophore_ptr_id, name, protein_id, maturation) + VALUES (%s, %s, %s, %s) +""", [fluorophore.pk, name, protein_id, maturation]) ``` -**Expected:** ✅ Runs 0001-0064, all tests pass +This bypasses Django's save logic and directly creates the child record. -### Test 2: Production Migration Simulation +### Challenge 2: PostgreSQL Column Case Sensitivity -```bash -just pgpull # Drops local, pulls production, runs migrate +**Problem:** Column `twop_peakGM` created with mixed case, but unquoted identifiers in SQL become lowercase: +``` +UndefinedColumn: column "twop_peakgm" does not exist ``` -**Expected:** - -- Detects migrations 0001-0058 already applied -- Runs only 0059-0064 -- ✅ Data migrated correctly +**Solution:** Quote column names in SELECT statements: +```python +cursor.execute(""" + SELECT ..., "twop_peakGM", ... + FROM proteins_state_old +""") +``` +Then map to lowercase Python variable, then assign to camelCase model field. -### Test 3: Partial Backup Migration +### Challenge 3: Slug Length Constraints -```bash -# Restore backup from middle of history (e.g., only 0001-0034 applied) -uv run python backend/manage.py migrate +**Problem:** SlugField default max_length=50, but production has dye names like: +``` +"FluoSpheres nile red fluorescent microspheres" +→ slug: "fluospheres-nile-red-fluorescent-microspheres-default" (53 chars) ``` -**Expected:** - -- Runs 0035-0064 in sequence -- ✅ Migrates successfully +**Solution:** Increased SlugField max_length to 100 in both Fluorophore and Dye models. -### Test 4: Data Integrity Verification +### Challenge 4: Empty/Duplicate Slugs -```python -# In shell_plus after migration -# Verify State MTI -assert State.objects.filter(fluorophore_ptr__isnull=True).count() == 0 - -# Verify DyeState created -old_dye_count = 50 # Note before migration -assert DyeState.objects.count() == old_dye_count - -# Verify Spectrum ownership migrated -assert Spectrum.objects.filter(owner_fluor__isnull=True).count() == 0 -assert Spectrum.objects.filter(owner_state__isnull=False).count() == 0 - -# Verify Fluorophore materialized data -for state in State.objects.all(): - if state.protein.primary_reference: - assert state.fluorophore_ptr.ex_max is not None -``` +**Problem:** Production data has States with empty or duplicate slugs. -## Key Decision Points +**Solution:** Comprehensive slug generation with fallbacks: +1. Use existing slug if non-empty +2. Generate from `{protein.slug}-{name}` or fallback to `state-{old_id}` +3. Check for uniqueness, append `-{counter}` if duplicate +4. Final safety check: ensure non-empty before creating -**Q1: How to handle State MTI conversion?** +### Challenge 5: Nullable References -- Option A: Add fluorophore_ptr to existing State, populate, convert -- Option B: Create StateNew with MTI, migrate data, rename table -- **Recommendation:** Option A if possible (cleaner), Option B if Django doesn't support it +**Problem:** Some Proteins don't have `primary_reference`, but FluorescenceMeasurement.reference was required. -**Q2: Order of data migration vs FK updates?** +**Solution:** Made `reference` FK nullable: `null=True, blank=True` -- Must create all Fluorophore/DyeState records BEFORE updating Spectrum FKs -- Order: 0059 (tables) → 0060 (State MTI) → 0061 (DyeState) → 0062 (data) → 0063 (FKs) → 0064 (cleanup) +### Challenge 6: Dye Chemical Structure Data -**Q3: Handle BleachMeasurement, OSERMeasurement FKs?** +**Problem:** Old Dye schema has no inchikey field, but new schema has `UniqueConstraint(inchikey, condition=Q(structural_status="DEFINED"))`. -- These reference State via FK -- Need to verify they still work after State becomes MTI child -- May need FK updates in 0063 +**Solution:** +- Mark all old dyes as `structural_status="PROPRIETARY"` +- Set `inchikey=""` (unique constraint only applies to DEFINED dyes) +- Can be updated later when chemical data is added -## Implementation Steps +## Implementation Summary -1. **Move migrations:** `mv migrations_old/* migrations/` -2. **Delete current:** `rm migrations/0001_initial.py migrations/0002_*.py` -3. **Create 0059-0064** with schema operations above -4. **Test with fresh DB:** Verify all 64 migrations run -5. **Test with pgpull:** Verify production migration works -6. **Write verification script:** Data integrity checks -7. **Deploy to production** -8. **Delete migrations_old/** after success +**Completed Steps:** +1. ✅ Moved all 58 migrations from `migrations_old/` to `migrations/` +2. ✅ Deleted placeholder `0001_initial.py` and `0002_*.py` +3. ✅ Created 0059 (schema), 0060 (data), 0061 (cleanup) +4. ✅ Tested with fresh DB: All 88 tests passed +5. ✅ Tested with pgpull: Production migration successful +6. ✅ Verified data integrity: All records migrated correctly +7. 🚀 Ready for production deployment -## Risk Mitigation +## Benefits Achieved -- ✅ Full history preserved forever -- ✅ Any backup can migrate -- ✅ Incremental migrations easier to debug than big-bang -- ✅ Can test each migration step independently -- ⚠️ More migrations = more complexity -- ⚠️ MTI conversion is tricky, needs careful testing +- ✅ **Full history preserved:** All 58 original migrations retained +- ✅ **Any backup can migrate:** Historical backups can migrate to latest schema +- ✅ **Atomic migration:** Single `0060` RunPython does all data transformation +- ✅ **Zero data loss:** All 1,055 States and 950 Dyes migrated +- ✅ **Clean schema:** Old fields removed, new MTI structure in place +- ✅ **Tested thoroughly:** Fresh DB, production simulation, and full test suite all pass From b0bb086134e3e12fa02851b7915a756c74f28040 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sat, 22 Nov 2025 15:35:06 -0500 Subject: [PATCH 18/57] apply suggestions --- .../0059_add_fluorophore_and_new_models.py | 2 + .../0060_migrate_data_from_old_schema.py | 65 ++++++++++++++++++- .../migrations/0061_cleanup_old_schema.py | 12 ++++ backend/proteins/models/fluorescence_data.py | 21 +++++- .../models/fluorescence_measurement.py | 17 +++-- backend/proteins/models/fluorophore.py | 11 ++-- 6 files changed, 115 insertions(+), 13 deletions(-) diff --git a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py index 36b1f3d5b..6ffe6735a 100644 --- a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py +++ b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py @@ -76,6 +76,8 @@ class Migration(migrations.Migration): "indexes": [ models.Index(fields=["ex_max"], name="fluorophore_ex_max_idx"), models.Index(fields=["em_max"], name="fluorophore_em_max_idx"), + models.Index(fields=["label", "entity_type"], name="fluorophore_label_type_idx"), + models.Index(fields=["entity_type", "is_dark"], name="fluorophore_type_dark_idx"), ], }, ), diff --git a/backend/proteins/migrations/0060_migrate_data_from_old_schema.py b/backend/proteins/migrations/0060_migrate_data_from_old_schema.py index 37d9c0766..55ef9f4e4 100644 --- a/backend/proteins/migrations/0060_migrate_data_from_old_schema.py +++ b/backend/proteins/migrations/0060_migrate_data_from_old_schema.py @@ -1,7 +1,11 @@ # Generated manually for schema overhaul +import logging + from django.db import migrations +logger = logging.getLogger(__name__) + def migrate_state_data(apps, schema_editor): """Migrate State data from old schema to new Fluorophore + State MTI structure.""" @@ -50,11 +54,23 @@ def migrate_state_data(apps, schema_editor): continue # Ensure slug uniqueness by checking if it already exists + original_slug = state_slug base_slug = state_slug counter = 1 while Fluorophore.objects.filter(slug=state_slug).exists(): state_slug = f"{base_slug}-{counter}" counter += 1 + if counter > 100: + raise ValueError( + f"Could not generate unique slug for State {old_id} " + f"after 100 attempts (original: {original_slug})" + ) + + if state_slug != original_slug: + logger.warning( + f"Slug collision during State migration: {original_slug} -> {state_slug} " + f"(State ID: {old_id}, Protein: {protein.name})" + ) # Create Fluorophore parent (MTI will link automatically) fluorophore = Fluorophore.objects.create( @@ -141,11 +157,23 @@ def migrate_dye_data(apps, schema_editor): dye_slug = slug # Ensure Dye slug uniqueness + original_dye_slug = dye_slug base_dye_slug = dye_slug counter = 1 while Dye.objects.filter(slug=dye_slug).exists(): dye_slug = f"{base_dye_slug}-{counter}" counter += 1 + if counter > 100: + raise ValueError( + f"Could not generate unique slug for Dye {old_id} " + f"after 100 attempts (original: {original_dye_slug})" + ) + + if dye_slug != original_dye_slug: + logger.warning( + f"Slug collision during Dye migration: {original_dye_slug} -> {dye_slug} " + f"(Dye ID: {old_id}, Name: {name})" + ) # Old Dye schema doesn't have chemical structure fields # Mark all as PROPRIETARY to avoid unique constraint issues @@ -168,11 +196,23 @@ def migrate_dye_data(apps, schema_editor): # Create Fluorophore for this DyeState # Ensure unique fluorophore slug fluorophore_slug = f"{dye_slug}-default" + original_fluor_slug = fluorophore_slug base_fluor_slug = fluorophore_slug counter = 1 while Fluorophore.objects.filter(slug=fluorophore_slug).exists(): fluorophore_slug = f"{base_fluor_slug}-{counter}" counter += 1 + if counter > 100: + raise ValueError( + f"Could not generate unique fluorophore slug for Dye {old_id} " + f"after 100 attempts (original: {original_fluor_slug})" + ) + + if fluorophore_slug != original_fluor_slug: + logger.warning( + f"Fluorophore slug collision during Dye migration: {original_fluor_slug} -> {fluorophore_slug} " + f"(Dye ID: {old_id}, Name: {name})" + ) # Don't pass emhex/exhex - the save() method will compute them from wavelengths fluorophore = Fluorophore.objects.create( @@ -334,8 +374,29 @@ def migrate_forward(apps, schema_editor): def migrate_reverse(apps, schema_editor): - """Reverse migration - not implemented (too complex).""" - raise NotImplementedError("Reversing this migration is not supported") + """Reverse migration - not supported. + + This migration performs a one-way data transformation from the old schema + (separate State and Dye models) to the new schema (MTI-based Fluorophore hierarchy). + + Reversing this migration would require: + 1. Decomposing Fluorophore + State back into old State structure + 2. Decomposing Fluorophore + DyeState back into old Dye structure + 3. Merging FluorescenceMeasurement data back into parent entities + 4. Restoring old spectrum ownership relationships + + This is not safely automatable. If you need to rollback this migration: + 1. Restore from a database backup taken before running this migration + 2. Do NOT attempt to use Django's reverse migration functionality + 3. Estimated restore time: 30-60 minutes depending on database size + + See deployment documentation for rollback procedures. + """ + raise RuntimeError( + "This migration cannot be reversed. " + "Restore from database backup if rollback is needed. " + "See migration docstring for details." + ) class Migration(migrations.Migration): diff --git a/backend/proteins/migrations/0061_cleanup_old_schema.py b/backend/proteins/migrations/0061_cleanup_old_schema.py index bc9252d36..345e7f3c1 100644 --- a/backend/proteins/migrations/0061_cleanup_old_schema.py +++ b/backend/proteins/migrations/0061_cleanup_old_schema.py @@ -1,4 +1,16 @@ # Generated manually for schema overhaul +# +# WARNING: This migration is NOT REVERSIBLE +# ========================================== +# This migration drops the old State and Dye tables and removes deprecated fields. +# Once this migration runs, there is no automated way to reverse it. +# +# Rollback procedure: +# 1. Restore from database backup taken before migration 0059 +# 2. Do NOT use Django's migrate command to reverse - it will not work +# 3. Manual data recovery may be required if backup is unavailable +# +# Always take a full database backup before running this migration in production. from django.db import migrations, models import django.db.models.deletion diff --git a/backend/proteins/models/fluorescence_data.py b/backend/proteins/models/fluorescence_data.py index 95948918f..6d475fd78 100644 --- a/backend/proteins/models/fluorescence_data.py +++ b/backend/proteins/models/fluorescence_data.py @@ -99,5 +99,22 @@ def save(self, *args, **kwargs): @classmethod def get_measurable_fields(cls): - """Helper to return list of field names: ['ex_max', 'qy', ...]""" - return [f.name for f in cls._meta.fields] + """Return only fluorescence property field names. + + Excludes metadata fields like id, created, modified, etc. + Only returns fields that represent actual fluorescence measurements. + """ + MEASURABLE = { + "ex_max", + "em_max", + "ext_coeff", + "qy", + "brightness", + "lifetime", + "pka", + "twop_ex_max", + "twop_peakGM", + "twop_qy", + "is_dark", + } + return [f.name for f in cls._meta.fields if f.name in MEASURABLE] diff --git a/backend/proteins/models/fluorescence_measurement.py b/backend/proteins/models/fluorescence_measurement.py index 7edc2b793..c002f4948 100644 --- a/backend/proteins/models/fluorescence_measurement.py +++ b/backend/proteins/models/fluorescence_measurement.py @@ -17,8 +17,13 @@ class FluorescenceMeasurement(AbstractFluorescenceData): fluorophore = models.ForeignKey["Fluorophore"]( "Fluorophore", related_name="measurements", on_delete=models.CASCADE ) - reference_id: int - reference = models.ForeignKey["Reference"]("references.Reference", on_delete=models.CASCADE) + reference_id: int | None + reference = models.ForeignKey["Reference | None"]( + "references.Reference", + on_delete=models.CASCADE, + null=True, + blank=True, + ) # Metadata specific to the act of measuring date_measured = models.DateField(null=True, blank=True) @@ -27,10 +32,12 @@ class FluorescenceMeasurement(AbstractFluorescenceData): # Curator Override is_trusted = models.BooleanField(default=False, help_text="If True, this measurement overrides others.") - def save(self, *args, **kwargs) -> None: + def save(self, *args, rebuild_cache: bool = True, **kwargs) -> None: + # Allow opt-out of rebuild during bulk operations to avoid N+1 queries super().save(*args, **kwargs) - # Always keep the parent cache in sync - self.fluorophore.rebuild_attributes() + # Keep the parent cache in sync unless explicitly disabled + if rebuild_cache: + self.fluorophore.rebuild_attributes() def delete(self, *args, **kwargs) -> None: f = self.fluorophore diff --git a/backend/proteins/models/fluorophore.py b/backend/proteins/models/fluorophore.py index 4c66e9945..79392c428 100644 --- a/backend/proteins/models/fluorophore.py +++ b/backend/proteins/models/fluorophore.py @@ -75,6 +75,8 @@ class Meta: indexes = [ models.Index(fields=["ex_max"]), models.Index(fields=["em_max"]), + models.Index(fields=["label", "entity_type"]), + models.Index(fields=["entity_type", "is_dark"]), ] def __str__(self): @@ -129,11 +131,12 @@ def rebuild_attributes(self): self.source_map = new_source_map # 4. Refresh Label (Hoisting) - if hasattr(self, "protein_state"): - ps = self.protein_state + # Django MTI creates reverse relations with lowercase model names + if hasattr(self, "state"): + ps = self.state self.label = f"{ps.protein.name} ({ps.name})" - elif hasattr(self, "dye_state"): - ds = self.dye_state + elif hasattr(self, "dyestate"): + ds = self.dyestate self.label = f"{ds.dye.name} ({ds.name})" self.save() From 3bcff92d4bbd3f026b15d265acc7e993f293df85 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sun, 23 Nov 2025 09:58:17 -0500 Subject: [PATCH 19/57] move scout --- backend/config/settings/base.py | 1 - backend/config/settings/production.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/config/settings/base.py b/backend/config/settings/base.py index 97f52519e..f347b7eaf 100644 --- a/backend/config/settings/base.py +++ b/backend/config/settings/base.py @@ -63,7 +63,6 @@ THIRD_PARTY_APPS = [ "django_structlog", # Structured logging - "scout_apm.django", # APM monitoring "crispy_forms", # Form layouts "crispy_bootstrap5", "allauth", # registration diff --git a/backend/config/settings/production.py b/backend/config/settings/production.py index 658ef8cd7..5199ad472 100644 --- a/backend/config/settings/production.py +++ b/backend/config/settings/production.py @@ -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 From 1ef7a5b6e37a1e0f5d4d7e740913dc39e6246307 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sun, 23 Nov 2025 11:43:40 -0500 Subject: [PATCH 20/57] single migration --- .../0059_add_fluorophore_and_new_models.py | 485 ++++++++++++++++++ .../0060_migrate_data_from_old_schema.py | 409 --------------- .../migrations/0061_cleanup_old_schema.py | 79 --- 3 files changed, 485 insertions(+), 488 deletions(-) delete mode 100644 backend/proteins/migrations/0060_migrate_data_from_old_schema.py delete mode 100644 backend/proteins/migrations/0061_cleanup_old_schema.py diff --git a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py index 6ffe6735a..758b20f9f 100644 --- a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py +++ b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py @@ -6,6 +6,429 @@ import django.db.models.deletion import model_utils.fields import django.utils.timezone +import logging +from django.db import migrations, models +import django.db.models.deletion +from django.db import migrations + +logger = logging.getLogger(__name__) + + +def migrate_state_data(apps, schema_editor): + """Migrate State data from old schema to new Fluorophore + State MTI structure.""" + # Get models from migration state + Fluorophore = apps.get_model("proteins", "Fluorophore") + State = apps.get_model("proteins", "State") + Protein = apps.get_model("proteins", "Protein") + FluorescenceMeasurement = apps.get_model("proteins", "FluorescenceMeasurement") + + # Access old State data directly via raw SQL + with schema_editor.connection.cursor() as cursor: + cursor.execute(""" + SELECT id, created, modified, name, slug, is_dark, + ex_max, em_max, ext_coeff, qy, brightness, + lifetime, pka, twop_ex_max, "twop_peakGM", twop_qy, + maturation, protein_id + FROM proteins_state_old + """) + + for row in cursor.fetchall(): + (old_id, created, modified, name, slug, is_dark, + ex_max, em_max, ext_coeff, qy, brightness, + lifetime, pka, twop_ex_max, twop_peakgm, twop_qy, + maturation, protein_id) = row + + # Get protein for label + try: + protein = Protein.objects.get(id=protein_id) + label = f"{protein.name} ({name})" if name and name != "default" else protein.name + # Handle empty/null slugs with guaranteed non-empty fallback + if not slug or (isinstance(slug, str) and slug.strip() == ""): + # Try protein slug + name, or protein slug, or fallback to state ID + if name and name != "default": + state_slug = f"{protein.slug}-{name}" if protein.slug else f"state-{old_id}" + else: + state_slug = protein.slug if protein.slug else f"state-{old_id}" + else: + state_slug = slug + + # Final safety check - ensure slug is not empty + if not state_slug or state_slug.strip() == "": + state_slug = f"state-{old_id}" + + except Protein.DoesNotExist: + print(f"Warning: Protein {protein_id} not found for State {old_id}, skipping") + continue + + # Ensure slug uniqueness by checking if it already exists + original_slug = state_slug + base_slug = state_slug + counter = 1 + while Fluorophore.objects.filter(slug=state_slug).exists(): + state_slug = f"{base_slug}-{counter}" + counter += 1 + if counter > 100: + raise ValueError( + f"Could not generate unique slug for State {old_id} " + f"after 100 attempts (original: {original_slug})" + ) + + if state_slug != original_slug: + logger.warning( + f"Slug collision during State migration: {original_slug} -> {state_slug} " + f"(State ID: {old_id}, Protein: {protein.name})" + ) + + # Create Fluorophore parent (MTI will link automatically) + fluorophore = Fluorophore.objects.create( + created=created, + modified=modified, + label=label, + slug=state_slug, + entity_type="protein", + ex_max=ex_max, + em_max=em_max, + ext_coeff=ext_coeff, + qy=qy, + brightness=brightness, + lifetime=lifetime, + pka=pka, + twop_ex_max=twop_ex_max, + twop_peakGM=twop_peakgm, # Map from SQL result to model field + twop_qy=twop_qy, + is_dark=is_dark, + + + ) + + # Create State (MTI child) pointing to the Fluorophore + # Use raw SQL to avoid Django MTI trying to update parent with empty values + cursor.execute(""" + INSERT INTO proteins_state (fluorophore_ptr_id, name, protein_id, maturation) + VALUES (%s, %s, %s, %s) + """, [fluorophore.pk, name, protein_id, maturation]) + + # Create FluorescenceMeasurement from old State data if there's any fluorescence data + if any([ex_max, em_max, qy, ext_coeff, lifetime, pka]): + # Get protein's primary reference (may be None) + reference = protein.primary_reference if hasattr(protein, 'primary_reference') else None + reference_id = protein.primary_reference_id if hasattr(protein, 'primary_reference_id') else None + + FluorescenceMeasurement.objects.create( + fluorophore=fluorophore, + reference_id=reference_id, + ex_max=ex_max, + em_max=em_max, + ext_coeff=ext_coeff, + qy=qy, + brightness=brightness, + lifetime=lifetime, + pka=pka, + twop_ex_max=twop_ex_max, + twop_peakGM=twop_peakgm, + twop_qy=twop_qy, + is_dark=is_dark, + is_trusted=True, # Mark as trusted since it's the original data + + + ) + + print(f"Migrated {State.objects.count()} State records") + + +def migrate_dye_data(apps, schema_editor): + """Migrate Dye data from old schema to new Dye container + DyeState structure.""" + Fluorophore = apps.get_model("proteins", "Fluorophore") + Dye = apps.get_model("proteins", "Dye") + DyeState = apps.get_model("proteins", "DyeState") + FluorescenceMeasurement = apps.get_model("proteins", "FluorescenceMeasurement") + + # Access old Dye data via raw SQL + with schema_editor.connection.cursor() as cursor: + cursor.execute(""" + SELECT id, created, modified, name, slug, is_dark, + ex_max, em_max, ext_coeff, qy, brightness, + lifetime, pka, twop_ex_max, "twop_peakGM", twop_qy + FROM proteins_dye_old + """) + + for row in cursor.fetchall(): + (old_id, created, modified, name, slug, is_dark, + ex_max, em_max, ext_coeff, qy, brightness, + lifetime, pka, twop_ex_max, twop_peakgm, twop_qy) = row + + # Handle empty/null slugs + if not slug or slug.strip() == "": + dye_slug = f"dye-{old_id}" # Use old ID as fallback + else: + dye_slug = slug + + # Ensure Dye slug uniqueness + original_dye_slug = dye_slug + base_dye_slug = dye_slug + counter = 1 + while Dye.objects.filter(slug=dye_slug).exists(): + dye_slug = f"{base_dye_slug}-{counter}" + counter += 1 + if counter > 100: + raise ValueError( + f"Could not generate unique slug for Dye {old_id} " + f"after 100 attempts (original: {original_dye_slug})" + ) + + if dye_slug != original_dye_slug: + logger.warning( + f"Slug collision during Dye migration: {original_dye_slug} -> {dye_slug} " + f"(Dye ID: {old_id}, Name: {name})" + ) + + # Old Dye schema doesn't have chemical structure fields + # Mark all as PROPRIETARY to avoid unique constraint issues + # (Can be updated later with actual chemical data) + + # Create Dye container (without fluorescence properties) + dye = Dye.objects.create( + created=created, + modified=modified, + name=name, + slug=dye_slug, + inchikey="", # No chemical data in old schema + structural_status="PROPRIETARY", # Safe default for old dyes + + + # Note: Other fields like smiles, inchi, etc. can be added later + # if they exist in the old schema + ) + + # Create Fluorophore for this DyeState + # Ensure unique fluorophore slug + fluorophore_slug = f"{dye_slug}-default" + original_fluor_slug = fluorophore_slug + base_fluor_slug = fluorophore_slug + counter = 1 + while Fluorophore.objects.filter(slug=fluorophore_slug).exists(): + fluorophore_slug = f"{base_fluor_slug}-{counter}" + counter += 1 + if counter > 100: + raise ValueError( + f"Could not generate unique fluorophore slug for Dye {old_id} " + f"after 100 attempts (original: {original_fluor_slug})" + ) + + if fluorophore_slug != original_fluor_slug: + logger.warning( + f"Fluorophore slug collision during Dye migration: {original_fluor_slug} -> {fluorophore_slug} " + f"(Dye ID: {old_id}, Name: {name})" + ) + + # Don't pass emhex/exhex - the save() method will compute them from wavelengths + fluorophore = Fluorophore.objects.create( + created=created, + modified=modified, + label=name, + slug=fluorophore_slug, + entity_type="dye", + ex_max=ex_max, + em_max=em_max, + ext_coeff=ext_coeff, + qy=qy, + brightness=brightness, + lifetime=lifetime, + pka=pka, + twop_ex_max=twop_ex_max, + twop_peakGM=twop_peakgm, + twop_qy=twop_qy, + is_dark=is_dark, + + + ) + + # Create DyeState (one per old Dye) + # Use raw SQL to avoid Django MTI trying to update parent with empty values + cursor.execute(""" + INSERT INTO proteins_dyestate (fluorophore_ptr_id, dye_id, name, solvent, ph, environment, is_reference) + VALUES (%s, %s, %s, %s, %s, %s, %s) + """, [fluorophore.pk, dye.pk, "default", "PBS", 7.4, "FREE", True]) + + # Create FluorescenceMeasurement from old Dye data + if any([ex_max, em_max, qy, ext_coeff, lifetime, pka]): + # For dyes, we don't have a primary_reference concept in old schema + # We'll leave reference as None for now + FluorescenceMeasurement.objects.create( + fluorophore=fluorophore, + reference_id=None, + ex_max=ex_max, + em_max=em_max, + ext_coeff=ext_coeff, + qy=qy, + brightness=brightness, + lifetime=lifetime, + pka=pka, + twop_ex_max=twop_ex_max, + twop_peakGM=twop_peakgm, + twop_qy=twop_qy, + is_dark=is_dark, + is_trusted=True, + + + ) + + print(f"Migrated {Dye.objects.count()} Dye records to Dye containers") + print(f"Created {DyeState.objects.count()} DyeState records") + + +def update_spectrum_ownership(apps, schema_editor): + """Update Spectrum foreign keys to point to new Fluorophore records.""" + Spectrum = apps.get_model("proteins", "Spectrum") + Fluorophore = apps.get_model("proteins", "Fluorophore") + + # We need to map old State/Dye IDs to new Fluorophore IDs + # This is tricky because we need to query the old tables + + with schema_editor.connection.cursor() as cursor: + # Update spectra that were owned by States + cursor.execute(""" + UPDATE proteins_spectrum s + SET owner_fluor_id = ( + SELECT f.id + FROM proteins_fluorophore f + JOIN proteins_state ns ON ns.fluorophore_ptr_id = f.id + JOIN proteins_state_old os ON os.slug = f.slug + WHERE os.id = s.owner_state_id + ) + WHERE s.owner_state_id IS NOT NULL + """) + + state_count = cursor.rowcount + print(f"Updated {state_count} spectra owned by States") + + # Update spectra that were owned by Dyes + # Note: DyeState slug is "{dye_slug}-default", so we need to match carefully + cursor.execute(""" + UPDATE proteins_spectrum s + SET owner_fluor_id = ( + SELECT f.id + FROM proteins_fluorophore f + JOIN proteins_dyestate ds ON ds.fluorophore_ptr_id = f.id + JOIN proteins_dye d ON d.id = ds.dye_id + JOIN proteins_dye_old od ON od.slug = d.slug + WHERE od.id = s.owner_dye_id + ) + WHERE s.owner_dye_id IS NOT NULL + """) + + dye_count = cursor.rowcount + print(f"Updated {dye_count} spectra owned by Dyes") + + +def update_ocfluoreff(apps, schema_editor): + """Update OcFluorEff to use direct FK to Fluorophore.""" + with schema_editor.connection.cursor() as cursor: + # Update OcFluorEff records that pointed to States via GenericFK + cursor.execute(""" + UPDATE proteins_ocfluoreff o + SET fluor_id = ( + SELECT f.id + FROM proteins_fluorophore f + JOIN proteins_state s ON s.fluorophore_ptr_id = f.id + WHERE s.fluorophore_ptr_id IN ( + SELECT fluorophore_ptr_id + FROM proteins_state ps + JOIN proteins_state_old os ON os.slug = ( + SELECT slug FROM proteins_fluorophore WHERE id = ps.fluorophore_ptr_id + ) + WHERE os.id = o.object_id::integer + ) + ) + WHERE o.content_type_id = ( + SELECT id FROM django_content_type + WHERE app_label = 'proteins' AND model = 'state' + ) + """) + + state_count = cursor.rowcount + print(f"Updated {state_count} OcFluorEff records that pointed to States") + + # Update OcFluorEff records that pointed to Dyes via GenericFK + cursor.execute(""" + UPDATE proteins_ocfluoreff o + SET fluor_id = ( + SELECT f.id + FROM proteins_fluorophore f + JOIN proteins_dyestate ds ON ds.fluorophore_ptr_id = f.id + JOIN proteins_dye d ON d.id = ds.dye_id + JOIN proteins_dye_old od ON od.slug = d.slug + WHERE od.id = o.object_id::integer + ) + WHERE o.content_type_id = ( + SELECT id FROM django_content_type + WHERE app_label = 'proteins' AND model = 'dye' + ) + """) + + dye_count = cursor.rowcount + print(f"Updated {dye_count} OcFluorEff records that pointed to Dyes") + + +def populate_emhex_exhex(apps, _schema_editor): + """Populate emhex/exhex for all Fluorophore objects. + + The historical models don't include the save() logic from AbstractFluorescenceData, + so we need to manually calculate these fields after creating the objects. + """ + from proteins.util.helpers import wave_to_hex + + Fluorophore = apps.get_model("proteins", "Fluorophore") + + fluorophores_to_update = [] + for fluor in Fluorophore.objects.all(): + fluor.emhex = "#000" if fluor.is_dark else wave_to_hex(fluor.em_max) + fluor.exhex = wave_to_hex(fluor.ex_max) + fluorophores_to_update.append(fluor) + + Fluorophore.objects.bulk_update( + fluorophores_to_update, + ["emhex", "exhex"], + batch_size=500 + ) + print(f"Populated emhex/exhex for {len(fluorophores_to_update)} fluorophores") + + +def migrate_forward(apps, schema_editor): + """Run all migration functions.""" + print("Starting data migration from old schema...") + migrate_state_data(apps, schema_editor) + migrate_dye_data(apps, schema_editor) + update_spectrum_ownership(apps, schema_editor) + update_ocfluoreff(apps, schema_editor) + populate_emhex_exhex(apps, schema_editor) + print("Data migration complete!") + + +def migrate_reverse(_apps, _schema_editor): + """Reverse migration - not supported. + + This migration performs a one-way data transformation from the old schema + (separate State and Dye models) to the new schema (MTI-based Fluorophore hierarchy). + + Reversing this migration would require: + 1. Decomposing Fluorophore + State back into old State structure + 2. Decomposing Fluorophore + DyeState back into old Dye structure + 3. Merging FluorescenceMeasurement data back into parent entities + 4. Restoring old spectrum ownership relationships + + This is not safely automatable. If you need to rollback this migration: + 1. Restore from a database backup taken before running this migration + 2. Do NOT attempt to use Django's reverse migration functionality + 3. Estimated restore time: 30-60 minutes depending on database size + + See deployment documentation for rollback procedures. + """ + raise RuntimeError( + "This migration cannot be reversed. " + "Restore from database backup if rollback is needed. " + "See migration docstring for details." + ) class Migration(migrations.Migration): @@ -211,4 +634,66 @@ class Migration(migrations.Migration): to="proteins.fluorophore", ), ), + + # ---------------------------------------------- + + # Perform manual data migration steps + migrations.RunPython(migrate_forward, migrate_reverse), + + # ---------------------------------------------- + + # Step 1: Drop old tables that we renamed in 0059 + migrations.RunSQL( + sql="DROP TABLE IF EXISTS proteins_state_old CASCADE;", + reverse_sql=migrations.RunSQL.noop, + ), + migrations.RunSQL( + sql="DROP TABLE IF EXISTS proteins_dye_old CASCADE;", + reverse_sql=migrations.RunSQL.noop, + ), + + # Step 2: Remove old foreign keys from Spectrum + migrations.RemoveField( + model_name="spectrum", + name="owner_state", + ), + migrations.RemoveField( + model_name="spectrum", + name="owner_dye", + ), + + # Step 3: Make owner_fluor non-nullable now that all data is migrated + # First, verify no nulls exist (will fail if there are any) + migrations.RunSQL( + sql=""" + DO $$ + BEGIN + IF EXISTS (SELECT 1 FROM proteins_spectrum WHERE owner_fluor_id IS NULL AND (owner_filter_id IS NULL AND owner_light_id IS NULL AND owner_camera_id IS NULL)) THEN + RAISE EXCEPTION 'Found Spectrum records with no owner after migration!'; + END IF; + END $$; + """, + reverse_sql=migrations.RunSQL.noop, + ), + + # Step 4: Remove GenericForeignKey fields from OcFluorEff + migrations.RemoveField( + model_name="ocfluoreff", + name="content_type", + ), + migrations.RemoveField( + model_name="ocfluoreff", + name="object_id", + ), + + # Step 5: Make fluor FK on OcFluorEff non-nullable + migrations.AlterField( + model_name="ocfluoreff", + name="fluor", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="oc_effs", + to="proteins.fluorophore", + ), + ), ] diff --git a/backend/proteins/migrations/0060_migrate_data_from_old_schema.py b/backend/proteins/migrations/0060_migrate_data_from_old_schema.py deleted file mode 100644 index 55ef9f4e4..000000000 --- a/backend/proteins/migrations/0060_migrate_data_from_old_schema.py +++ /dev/null @@ -1,409 +0,0 @@ -# Generated manually for schema overhaul - -import logging - -from django.db import migrations - -logger = logging.getLogger(__name__) - - -def migrate_state_data(apps, schema_editor): - """Migrate State data from old schema to new Fluorophore + State MTI structure.""" - # Get models from migration state - Fluorophore = apps.get_model("proteins", "Fluorophore") - State = apps.get_model("proteins", "State") - Protein = apps.get_model("proteins", "Protein") - FluorescenceMeasurement = apps.get_model("proteins", "FluorescenceMeasurement") - - # Access old State data directly via raw SQL - with schema_editor.connection.cursor() as cursor: - cursor.execute(""" - SELECT id, created, modified, name, slug, is_dark, - ex_max, em_max, ext_coeff, qy, brightness, - lifetime, pka, twop_ex_max, "twop_peakGM", twop_qy, - maturation, protein_id - FROM proteins_state_old - """) - - for row in cursor.fetchall(): - (old_id, created, modified, name, slug, is_dark, - ex_max, em_max, ext_coeff, qy, brightness, - lifetime, pka, twop_ex_max, twop_peakgm, twop_qy, - maturation, protein_id) = row - - # Get protein for label - try: - protein = Protein.objects.get(id=protein_id) - label = f"{protein.name} ({name})" if name and name != "default" else protein.name - # Handle empty/null slugs with guaranteed non-empty fallback - if not slug or (isinstance(slug, str) and slug.strip() == ""): - # Try protein slug + name, or protein slug, or fallback to state ID - if name and name != "default": - state_slug = f"{protein.slug}-{name}" if protein.slug else f"state-{old_id}" - else: - state_slug = protein.slug if protein.slug else f"state-{old_id}" - else: - state_slug = slug - - # Final safety check - ensure slug is not empty - if not state_slug or state_slug.strip() == "": - state_slug = f"state-{old_id}" - - except Protein.DoesNotExist: - print(f"Warning: Protein {protein_id} not found for State {old_id}, skipping") - continue - - # Ensure slug uniqueness by checking if it already exists - original_slug = state_slug - base_slug = state_slug - counter = 1 - while Fluorophore.objects.filter(slug=state_slug).exists(): - state_slug = f"{base_slug}-{counter}" - counter += 1 - if counter > 100: - raise ValueError( - f"Could not generate unique slug for State {old_id} " - f"after 100 attempts (original: {original_slug})" - ) - - if state_slug != original_slug: - logger.warning( - f"Slug collision during State migration: {original_slug} -> {state_slug} " - f"(State ID: {old_id}, Protein: {protein.name})" - ) - - # Create Fluorophore parent (MTI will link automatically) - fluorophore = Fluorophore.objects.create( - created=created, - modified=modified, - label=label, - slug=state_slug, - entity_type="protein", - ex_max=ex_max, - em_max=em_max, - ext_coeff=ext_coeff, - qy=qy, - brightness=brightness, - lifetime=lifetime, - pka=pka, - twop_ex_max=twop_ex_max, - twop_peakGM=twop_peakgm, # Map from SQL result to model field - twop_qy=twop_qy, - is_dark=is_dark, - - - ) - - # Create State (MTI child) pointing to the Fluorophore - # Use raw SQL to avoid Django MTI trying to update parent with empty values - cursor.execute(""" - INSERT INTO proteins_state (fluorophore_ptr_id, name, protein_id, maturation) - VALUES (%s, %s, %s, %s) - """, [fluorophore.pk, name, protein_id, maturation]) - - # Create FluorescenceMeasurement from old State data if there's any fluorescence data - if any([ex_max, em_max, qy, ext_coeff, lifetime, pka]): - # Get protein's primary reference (may be None) - reference = protein.primary_reference if hasattr(protein, 'primary_reference') else None - reference_id = protein.primary_reference_id if hasattr(protein, 'primary_reference_id') else None - - FluorescenceMeasurement.objects.create( - fluorophore=fluorophore, - reference_id=reference_id, - ex_max=ex_max, - em_max=em_max, - ext_coeff=ext_coeff, - qy=qy, - brightness=brightness, - lifetime=lifetime, - pka=pka, - twop_ex_max=twop_ex_max, - twop_peakGM=twop_peakgm, - twop_qy=twop_qy, - is_dark=is_dark, - is_trusted=True, # Mark as trusted since it's the original data - - - ) - - print(f"Migrated {State.objects.count()} State records") - - -def migrate_dye_data(apps, schema_editor): - """Migrate Dye data from old schema to new Dye container + DyeState structure.""" - Fluorophore = apps.get_model("proteins", "Fluorophore") - Dye = apps.get_model("proteins", "Dye") - DyeState = apps.get_model("proteins", "DyeState") - FluorescenceMeasurement = apps.get_model("proteins", "FluorescenceMeasurement") - - # Access old Dye data via raw SQL - with schema_editor.connection.cursor() as cursor: - cursor.execute(""" - SELECT id, created, modified, name, slug, is_dark, - ex_max, em_max, ext_coeff, qy, brightness, - lifetime, pka, twop_ex_max, "twop_peakGM", twop_qy - FROM proteins_dye_old - """) - - for row in cursor.fetchall(): - (old_id, created, modified, name, slug, is_dark, - ex_max, em_max, ext_coeff, qy, brightness, - lifetime, pka, twop_ex_max, twop_peakgm, twop_qy) = row - - # Handle empty/null slugs - if not slug or slug.strip() == "": - dye_slug = f"dye-{old_id}" # Use old ID as fallback - else: - dye_slug = slug - - # Ensure Dye slug uniqueness - original_dye_slug = dye_slug - base_dye_slug = dye_slug - counter = 1 - while Dye.objects.filter(slug=dye_slug).exists(): - dye_slug = f"{base_dye_slug}-{counter}" - counter += 1 - if counter > 100: - raise ValueError( - f"Could not generate unique slug for Dye {old_id} " - f"after 100 attempts (original: {original_dye_slug})" - ) - - if dye_slug != original_dye_slug: - logger.warning( - f"Slug collision during Dye migration: {original_dye_slug} -> {dye_slug} " - f"(Dye ID: {old_id}, Name: {name})" - ) - - # Old Dye schema doesn't have chemical structure fields - # Mark all as PROPRIETARY to avoid unique constraint issues - # (Can be updated later with actual chemical data) - - # Create Dye container (without fluorescence properties) - dye = Dye.objects.create( - created=created, - modified=modified, - name=name, - slug=dye_slug, - inchikey="", # No chemical data in old schema - structural_status="PROPRIETARY", # Safe default for old dyes - - - # Note: Other fields like smiles, inchi, etc. can be added later - # if they exist in the old schema - ) - - # Create Fluorophore for this DyeState - # Ensure unique fluorophore slug - fluorophore_slug = f"{dye_slug}-default" - original_fluor_slug = fluorophore_slug - base_fluor_slug = fluorophore_slug - counter = 1 - while Fluorophore.objects.filter(slug=fluorophore_slug).exists(): - fluorophore_slug = f"{base_fluor_slug}-{counter}" - counter += 1 - if counter > 100: - raise ValueError( - f"Could not generate unique fluorophore slug for Dye {old_id} " - f"after 100 attempts (original: {original_fluor_slug})" - ) - - if fluorophore_slug != original_fluor_slug: - logger.warning( - f"Fluorophore slug collision during Dye migration: {original_fluor_slug} -> {fluorophore_slug} " - f"(Dye ID: {old_id}, Name: {name})" - ) - - # Don't pass emhex/exhex - the save() method will compute them from wavelengths - fluorophore = Fluorophore.objects.create( - created=created, - modified=modified, - label=name, - slug=fluorophore_slug, - entity_type="dye", - ex_max=ex_max, - em_max=em_max, - ext_coeff=ext_coeff, - qy=qy, - brightness=brightness, - lifetime=lifetime, - pka=pka, - twop_ex_max=twop_ex_max, - twop_peakGM=twop_peakgm, - twop_qy=twop_qy, - is_dark=is_dark, - - - ) - - # Create DyeState (one per old Dye) - # Use raw SQL to avoid Django MTI trying to update parent with empty values - cursor.execute(""" - INSERT INTO proteins_dyestate (fluorophore_ptr_id, dye_id, name, solvent, ph, environment, is_reference) - VALUES (%s, %s, %s, %s, %s, %s, %s) - """, [fluorophore.pk, dye.pk, "default", "PBS", 7.4, "FREE", True]) - - # Create FluorescenceMeasurement from old Dye data - if any([ex_max, em_max, qy, ext_coeff, lifetime, pka]): - # For dyes, we don't have a primary_reference concept in old schema - # We'll leave reference as None for now - FluorescenceMeasurement.objects.create( - fluorophore=fluorophore, - reference_id=None, - ex_max=ex_max, - em_max=em_max, - ext_coeff=ext_coeff, - qy=qy, - brightness=brightness, - lifetime=lifetime, - pka=pka, - twop_ex_max=twop_ex_max, - twop_peakGM=twop_peakgm, - twop_qy=twop_qy, - is_dark=is_dark, - is_trusted=True, - - - ) - - print(f"Migrated {Dye.objects.count()} Dye records to Dye containers") - print(f"Created {DyeState.objects.count()} DyeState records") - - -def update_spectrum_ownership(apps, schema_editor): - """Update Spectrum foreign keys to point to new Fluorophore records.""" - Spectrum = apps.get_model("proteins", "Spectrum") - Fluorophore = apps.get_model("proteins", "Fluorophore") - - # We need to map old State/Dye IDs to new Fluorophore IDs - # This is tricky because we need to query the old tables - - with schema_editor.connection.cursor() as cursor: - # Update spectra that were owned by States - cursor.execute(""" - UPDATE proteins_spectrum s - SET owner_fluor_id = ( - SELECT f.id - FROM proteins_fluorophore f - JOIN proteins_state ns ON ns.fluorophore_ptr_id = f.id - JOIN proteins_state_old os ON os.slug = f.slug - WHERE os.id = s.owner_state_id - ) - WHERE s.owner_state_id IS NOT NULL - """) - - state_count = cursor.rowcount - print(f"Updated {state_count} spectra owned by States") - - # Update spectra that were owned by Dyes - # Note: DyeState slug is "{dye_slug}-default", so we need to match carefully - cursor.execute(""" - UPDATE proteins_spectrum s - SET owner_fluor_id = ( - SELECT f.id - FROM proteins_fluorophore f - JOIN proteins_dyestate ds ON ds.fluorophore_ptr_id = f.id - JOIN proteins_dye d ON d.id = ds.dye_id - JOIN proteins_dye_old od ON od.slug = d.slug - WHERE od.id = s.owner_dye_id - ) - WHERE s.owner_dye_id IS NOT NULL - """) - - dye_count = cursor.rowcount - print(f"Updated {dye_count} spectra owned by Dyes") - - -def update_ocfluoreff(apps, schema_editor): - """Update OcFluorEff to use direct FK to Fluorophore.""" - with schema_editor.connection.cursor() as cursor: - # Update OcFluorEff records that pointed to States via GenericFK - cursor.execute(""" - UPDATE proteins_ocfluoreff o - SET fluor_id = ( - SELECT f.id - FROM proteins_fluorophore f - JOIN proteins_state s ON s.fluorophore_ptr_id = f.id - WHERE s.fluorophore_ptr_id IN ( - SELECT fluorophore_ptr_id - FROM proteins_state ps - JOIN proteins_state_old os ON os.slug = ( - SELECT slug FROM proteins_fluorophore WHERE id = ps.fluorophore_ptr_id - ) - WHERE os.id = o.object_id::integer - ) - ) - WHERE o.content_type_id = ( - SELECT id FROM django_content_type - WHERE app_label = 'proteins' AND model = 'state' - ) - """) - - state_count = cursor.rowcount - print(f"Updated {state_count} OcFluorEff records that pointed to States") - - # Update OcFluorEff records that pointed to Dyes via GenericFK - cursor.execute(""" - UPDATE proteins_ocfluoreff o - SET fluor_id = ( - SELECT f.id - FROM proteins_fluorophore f - JOIN proteins_dyestate ds ON ds.fluorophore_ptr_id = f.id - JOIN proteins_dye d ON d.id = ds.dye_id - JOIN proteins_dye_old od ON od.slug = d.slug - WHERE od.id = o.object_id::integer - ) - WHERE o.content_type_id = ( - SELECT id FROM django_content_type - WHERE app_label = 'proteins' AND model = 'dye' - ) - """) - - dye_count = cursor.rowcount - print(f"Updated {dye_count} OcFluorEff records that pointed to Dyes") - - -def migrate_forward(apps, schema_editor): - """Run all migration functions.""" - print("Starting data migration from old schema...") - migrate_state_data(apps, schema_editor) - migrate_dye_data(apps, schema_editor) - update_spectrum_ownership(apps, schema_editor) - update_ocfluoreff(apps, schema_editor) - print("Data migration complete!") - - -def migrate_reverse(apps, schema_editor): - """Reverse migration - not supported. - - This migration performs a one-way data transformation from the old schema - (separate State and Dye models) to the new schema (MTI-based Fluorophore hierarchy). - - Reversing this migration would require: - 1. Decomposing Fluorophore + State back into old State structure - 2. Decomposing Fluorophore + DyeState back into old Dye structure - 3. Merging FluorescenceMeasurement data back into parent entities - 4. Restoring old spectrum ownership relationships - - This is not safely automatable. If you need to rollback this migration: - 1. Restore from a database backup taken before running this migration - 2. Do NOT attempt to use Django's reverse migration functionality - 3. Estimated restore time: 30-60 minutes depending on database size - - See deployment documentation for rollback procedures. - """ - raise RuntimeError( - "This migration cannot be reversed. " - "Restore from database backup if rollback is needed. " - "See migration docstring for details." - ) - - -class Migration(migrations.Migration): - dependencies = [ - ("proteins", "0059_add_fluorophore_and_new_models"), - ] - - operations = [ - migrations.RunPython(migrate_forward, migrate_reverse), - ] diff --git a/backend/proteins/migrations/0061_cleanup_old_schema.py b/backend/proteins/migrations/0061_cleanup_old_schema.py deleted file mode 100644 index 345e7f3c1..000000000 --- a/backend/proteins/migrations/0061_cleanup_old_schema.py +++ /dev/null @@ -1,79 +0,0 @@ -# Generated manually for schema overhaul -# -# WARNING: This migration is NOT REVERSIBLE -# ========================================== -# This migration drops the old State and Dye tables and removes deprecated fields. -# Once this migration runs, there is no automated way to reverse it. -# -# Rollback procedure: -# 1. Restore from database backup taken before migration 0059 -# 2. Do NOT use Django's migrate command to reverse - it will not work -# 3. Manual data recovery may be required if backup is unavailable -# -# Always take a full database backup before running this migration in production. - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - dependencies = [ - ("proteins", "0060_migrate_data_from_old_schema"), - ] - - operations = [ - # Step 1: Drop old tables that we renamed in 0059 - migrations.RunSQL( - sql="DROP TABLE IF EXISTS proteins_state_old CASCADE;", - reverse_sql=migrations.RunSQL.noop, - ), - migrations.RunSQL( - sql="DROP TABLE IF EXISTS proteins_dye_old CASCADE;", - reverse_sql=migrations.RunSQL.noop, - ), - - # Step 2: Remove old foreign keys from Spectrum - migrations.RemoveField( - model_name="spectrum", - name="owner_state", - ), - migrations.RemoveField( - model_name="spectrum", - name="owner_dye", - ), - - # Step 3: Make owner_fluor non-nullable now that all data is migrated - # First, verify no nulls exist (will fail if there are any) - migrations.RunSQL( - sql=""" - DO $$ - BEGIN - IF EXISTS (SELECT 1 FROM proteins_spectrum WHERE owner_fluor_id IS NULL AND (owner_filter_id IS NULL AND owner_light_id IS NULL AND owner_camera_id IS NULL)) THEN - RAISE EXCEPTION 'Found Spectrum records with no owner after migration!'; - END IF; - END $$; - """, - reverse_sql=migrations.RunSQL.noop, - ), - - # Step 4: Remove GenericForeignKey fields from OcFluorEff - migrations.RemoveField( - model_name="ocfluoreff", - name="content_type", - ), - migrations.RemoveField( - model_name="ocfluoreff", - name="object_id", - ), - - # Step 5: Make fluor FK on OcFluorEff non-nullable - migrations.AlterField( - model_name="ocfluoreff", - name="fluor", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="oc_effs", - to="proteins.fluorophore", - ), - ), - ] From 0c0e651cbe09d4602c403c97cb6489f0ebc75a5e Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sun, 23 Nov 2025 13:42:39 -0500 Subject: [PATCH 21/57] conditional scout celery --- backend/fpbase/celery.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/fpbase/celery.py b/backend/fpbase/celery.py index 916c58a23..175de10ab 100644 --- a/backend/fpbase/celery.py +++ b/backend/fpbase/celery.py @@ -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) From e505190b21bab858ebe1ed771fec7df664a8c8ae Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sun, 23 Nov 2025 14:02:41 -0500 Subject: [PATCH 22/57] just typing --- .../0059_add_fluorophore_and_new_models.py | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py index 758b20f9f..f18d5846a 100644 --- a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py +++ b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py @@ -1,3 +1,5 @@ +from __future__ import annotations +from typing import Any # Generated manually for schema overhaul from django.core.validators import MaxValueValidator, MinValueValidator @@ -10,11 +12,20 @@ from django.db import migrations, models import django.db.models.deletion from django.db import migrations - +from django.apps.registry import Apps +from django.db.backends.base.schema import BaseDatabaseSchemaEditor +from django.db.backends.utils import CursorWrapper logger = logging.getLogger(__name__) -def migrate_state_data(apps, schema_editor): +def _dictfetchall(cursor: CursorWrapper) -> list[dict[str, Any]]: + """Return all rows from a cursor as a dict. Assume the column names are unique.""" + if not cursor.description: + return [] + columns = (col.name for col in cursor.description) + return [dict(zip(columns, row)) for row in cursor.fetchall()] + +def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: """Migrate State data from old schema to new Fluorophore + State MTI structure.""" # Get models from migration state Fluorophore = apps.get_model("proteins", "Fluorophore") @@ -136,7 +147,7 @@ def migrate_state_data(apps, schema_editor): print(f"Migrated {State.objects.count()} State records") -def migrate_dye_data(apps, schema_editor): +def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: """Migrate Dye data from old schema to new Dye container + DyeState structure.""" Fluorophore = apps.get_model("proteins", "Fluorophore") Dye = apps.get_model("proteins", "Dye") @@ -277,7 +288,7 @@ def migrate_dye_data(apps, schema_editor): print(f"Created {DyeState.objects.count()} DyeState records") -def update_spectrum_ownership(apps, schema_editor): +def update_spectrum_ownership(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: """Update Spectrum foreign keys to point to new Fluorophore records.""" Spectrum = apps.get_model("proteins", "Spectrum") Fluorophore = apps.get_model("proteins", "Fluorophore") @@ -321,7 +332,7 @@ def update_spectrum_ownership(apps, schema_editor): print(f"Updated {dye_count} spectra owned by Dyes") -def update_ocfluoreff(apps, schema_editor): +def update_ocfluoreff(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: """Update OcFluorEff to use direct FK to Fluorophore.""" with schema_editor.connection.cursor() as cursor: # Update OcFluorEff records that pointed to States via GenericFK @@ -394,7 +405,7 @@ def populate_emhex_exhex(apps, _schema_editor): print(f"Populated emhex/exhex for {len(fluorophores_to_update)} fluorophores") -def migrate_forward(apps, schema_editor): +def migrate_forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: """Run all migration functions.""" print("Starting data migration from old schema...") migrate_state_data(apps, schema_editor) From 95f51bd1848858ed7e9d5e6e274fe04a23b16775 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sun, 23 Nov 2025 14:02:49 -0500 Subject: [PATCH 23/57] just notes and tests --- migration_notes.md | 10 +++++++++ test_migration.sh | 53 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 migration_notes.md create mode 100755 test_migration.sh diff --git a/migration_notes.md b/migration_notes.md new file mode 100644 index 000000000..675d4d1e7 --- /dev/null +++ b/migration_notes.md @@ -0,0 +1,10 @@ +# Migration notes + + - `common name` (VARCHAR 255) + - `molecular formula` in Hill notation (VARCHAR) + - `molecular weight` (DECIMAL 10,4) + - `canonical SMILES` (TEXT) as the primary structure representation + - `InChI` (TEXT) for standardized structure encoding + - `InChIKey` (CHAR 27) provide uniqueness guarantees and cross-database compatibility + - `CAS numbers` (VARCHAR 50) enable literature searches + - `PubChem CIDs` (INTEGER) offer free cross-referencing to the world's largest chemical database. diff --git a/test_migration.sh b/test_migration.sh new file mode 100755 index 000000000..96db56232 --- /dev/null +++ b/test_migration.sh @@ -0,0 +1,53 @@ +#!/bin/bash +set -e # Exit on error + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +SOURCE_DB="fpbase_pre" +TARGET_DB="fpbase_migrated" + +echo -e "${YELLOW}=== FPbase Migration Testing Script ===${NC}" +echo "" +echo "This script will:" +echo " 1. Drop '${TARGET_DB}' database if it exists" +echo " 2. Create a fresh copy from '${SOURCE_DB}'" +echo " 3. Run migration 0059 on the new database" +echo " 4. Leave '${SOURCE_DB}' completely untouched" +echo "" + +# Check if source database exists +if ! psql -lqt | cut -d \| -f 1 | grep -qw "$SOURCE_DB"; then + echo "ERROR: Source database '$SOURCE_DB' does not exist!" + exit 1 +fi + +echo -e "${GREEN}Step 1: Dropping existing '${TARGET_DB}' database (if it exists)${NC}" +psql postgres -c "DROP DATABASE IF EXISTS ${TARGET_DB};" 2>/dev/null || true + +echo -e "${GREEN}Step 2: Creating '${TARGET_DB}' as a copy of '${SOURCE_DB}'${NC}" +psql postgres -c "CREATE DATABASE ${TARGET_DB} WITH TEMPLATE ${SOURCE_DB};" + +echo -e "${GREEN}Step 3: Running migration 0059 on '${TARGET_DB}'${NC}" + +# Set DATABASE_URL to point to the new database +# This assumes PostgreSQL is running locally on default port +export DATABASE_URL="postgresql://localhost/${TARGET_DB}" + +# Run the specific migration +uv run backend/manage.py migrate proteins 0059 + +echo "" +echo -e "${GREEN}=== Migration complete! ===${NC}" +echo "" +echo "Databases:" +echo " - ${SOURCE_DB}: untouched (original test data)" +echo " - ${TARGET_DB}: migrated (contains new schema)" +echo "" +echo "To inspect the migrated database:" +echo " psql ${TARGET_DB}" +echo "" +echo "To run this script again:" +echo " ./test_migration.sh" From 01f9beae2f6d99bd4487d8b9d783e5a9e1de29f2 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Mon, 24 Nov 2025 09:13:16 -0500 Subject: [PATCH 24/57] preserve old state id --- .../0059_add_fluorophore_and_new_models.py | 68 ++++++++++++------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py index f18d5846a..43f1acb89 100644 --- a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py +++ b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py @@ -52,25 +52,27 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N # Get protein for label try: protein = Protein.objects.get(id=protein_id) - label = f"{protein.name} ({name})" if name and name != "default" else protein.name - # Handle empty/null slugs with guaranteed non-empty fallback - if not slug or (isinstance(slug, str) and slug.strip() == ""): - # Try protein slug + name, or protein slug, or fallback to state ID - if name and name != "default": - state_slug = f"{protein.slug}-{name}" if protein.slug else f"state-{old_id}" - else: - state_slug = protein.slug if protein.slug else f"state-{old_id}" - else: - state_slug = slug - - # Final safety check - ensure slug is not empty - if not state_slug or state_slug.strip() == "": - state_slug = f"state-{old_id}" - except Protein.DoesNotExist: + # note, the db doesn't have any cases of this print(f"Warning: Protein {protein_id} not found for State {old_id}, skipping") continue + label = f"{protein.name} ({name})" if name and name != "default" else protein.name + # Handle empty/null slugs with guaranteed non-empty fallback + if not slug or (isinstance(slug, str) and slug.strip() == ""): + # Try protein slug + name, or protein slug, or fallback to state ID + if name and name != "default": + state_slug = f"{protein.slug}-{name}" if protein.slug else f"state-{old_id}" + else: + state_slug = protein.slug if protein.slug else f"state-{old_id}" + else: + state_slug = slug + + # Final safety check - ensure slug is not empty + if not state_slug or state_slug.strip() == "": + state_slug = f"state-{old_id}" + + # Ensure slug uniqueness by checking if it already exists original_slug = state_slug base_slug = state_slug @@ -92,6 +94,7 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N # Create Fluorophore parent (MTI will link automatically) fluorophore = Fluorophore.objects.create( + id=old_id, # Preserve old ID for easier FK updates later created=created, modified=modified, label=label, @@ -120,14 +123,11 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N """, [fluorophore.pk, name, protein_id, maturation]) # Create FluorescenceMeasurement from old State data if there's any fluorescence data - if any([ex_max, em_max, qy, ext_coeff, lifetime, pka]): - # Get protein's primary reference (may be None) - reference = protein.primary_reference if hasattr(protein, 'primary_reference') else None - reference_id = protein.primary_reference_id if hasattr(protein, 'primary_reference_id') else None - + if any([ex_max, em_max, qy, ext_coeff, lifetime, pka, twop_ex_max, twop_peakgm, twop_qy]): FluorescenceMeasurement.objects.create( + id=old_id, # Preserve old ID fluorophore=fluorophore, - reference_id=reference_id, + reference_id=protein.primary_reference_id, ex_max=ex_max, em_max=em_max, ext_coeff=ext_coeff, @@ -140,12 +140,31 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N twop_qy=twop_qy, is_dark=is_dark, is_trusted=True, # Mark as trusted since it's the original data - - ) print(f"Migrated {State.objects.count()} State records") + # Reset the Fluorophore and FluorescenceMeasurement ID sequences + # to avoid conflicts when creating Dye fluorophores and their measurements + with schema_editor.connection.cursor() as cursor: + cursor.execute(""" + SELECT setval( + pg_get_serial_sequence('proteins_fluorophore', 'id'), + COALESCE((SELECT MAX(id) FROM proteins_fluorophore), 1) + ) + """) + fluor_seq = cursor.fetchone()[0] + print(f"Reset Fluorophore ID sequence to {fluor_seq}") + + cursor.execute(""" + SELECT setval( + pg_get_serial_sequence('proteins_fluorescencemeasurement', 'id'), + COALESCE((SELECT MAX(id) FROM proteins_fluorescencemeasurement), 1) + ) + """) + meas_seq = cursor.fetchone()[0] + print(f"Reset FluorescenceMeasurement ID sequence to {meas_seq}") + def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: """Migrate Dye data from old schema to new Dye container + DyeState structure.""" @@ -203,7 +222,6 @@ def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> Non modified=modified, name=name, slug=dye_slug, - inchikey="", # No chemical data in old schema structural_status="PROPRIETARY", # Safe default for old dyes @@ -250,8 +268,6 @@ def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> Non twop_peakGM=twop_peakgm, twop_qy=twop_qy, is_dark=is_dark, - - ) # Create DyeState (one per old Dye) From ac6fd3ffc070d632d9cd4fe5a97ad328bd2b71eb Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Mon, 24 Nov 2025 13:38:08 -0500 Subject: [PATCH 25/57] cleanup and miminize new model, add owner_name to flourophore, change peakGM to peak_gm --- backend/proteins/admin.py | 6 +- backend/proteins/factories.py | 9 +- .../0059_add_fluorophore_and_new_models.py | 212 +++++++++--------- backend/proteins/migrations/0059_notes.md | 8 +- backend/proteins/models/dye.py | 181 ++++++++------- backend/proteins/models/fluorescence_data.py | 5 +- backend/proteins/models/fluorophore.py | 59 +++-- backend/proteins/models/protein.py | 14 +- backend/proteins/models/spectrum.py | 14 +- backend/proteins/schema/types.py | 2 +- backend/tests/test_api/test_graphql_schema.py | 5 +- .../tests/test_proteins/test_ajax_views.py | 1 - 12 files changed, 264 insertions(+), 252 deletions(-) diff --git a/backend/proteins/admin.py b/backend/proteins/admin.py index 6e2911eca..4f81d3116 100644 --- a/backend/proteins/admin.py +++ b/backend/proteins/admin.py @@ -113,7 +113,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", @@ -208,7 +208,7 @@ class SpectrumAdmin(VersionAdmin): list_filter = ("status", "created", "category", "subtype") readonly_fields = ("owner", "name", "created", "modified", "spectrum_preview") search_fields = ( - "owner_fluor__label", + "owner_fluor__name", "owner_filter__name", "owner_camera__name", "owner_light__name", @@ -342,7 +342,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", ) diff --git a/backend/proteins/factories.py b/backend/proteins/factories.py index 72da98574..15277c406 100644 --- a/backend/proteins/factories.py +++ b/backend/proteins/factories.py @@ -219,13 +219,6 @@ class Meta: name = factory.Sequence(lambda n: f"TestDye{n}") slug = factory.LazyAttribute(lambda o: slugify(o.name)) - structural_status = "DEFINED" - canonical_smiles = "" - inchi = "" - # Generate a fake but unique InChIKey (format: XXXXXXXXXXXXXX-YYYYYYYYYY-Z, exactly 27 chars) - inchikey = factory.Sequence(lambda n: f"TEST{n:06d}ABCD-EFGHIJKLMN-O") - molblock = "" - chemical_class = "" class ProteinFactory(factory.django.DjangoModelFactory[Protein]): @@ -380,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 diff --git a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py index 43f1acb89..b2424d28e 100644 --- a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py +++ b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py @@ -39,15 +39,15 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N SELECT id, created, modified, name, slug, is_dark, ex_max, em_max, ext_coeff, qy, brightness, lifetime, pka, twop_ex_max, "twop_peakGM", twop_qy, - maturation, protein_id + maturation, protein_id, created_by_id, updated_by_id FROM proteins_state_old """) for row in cursor.fetchall(): (old_id, created, modified, name, slug, is_dark, ex_max, em_max, ext_coeff, qy, brightness, - lifetime, pka, twop_ex_max, twop_peakgm, twop_qy, - maturation, protein_id) = row + lifetime, pka, twop_ex_max, twop_peak_gm, twop_qy, + maturation, protein_id, created_by_id, updated_by_id) = row # Get protein for label try: @@ -57,7 +57,6 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N print(f"Warning: Protein {protein_id} not found for State {old_id}, skipping") continue - label = f"{protein.name} ({name})" if name and name != "default" else protein.name # Handle empty/null slugs with guaranteed non-empty fallback if not slug or (isinstance(slug, str) and slug.strip() == ""): # Try protein slug + name, or protein slug, or fallback to state ID @@ -97,9 +96,11 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N id=old_id, # Preserve old ID for easier FK updates later created=created, modified=modified, - label=label, + name=name, slug=state_slug, - entity_type="protein", + entity_type="p", + owner_name=protein.name, + owner_slug=protein.slug, ex_max=ex_max, em_max=em_max, ext_coeff=ext_coeff, @@ -108,22 +109,22 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N lifetime=lifetime, pka=pka, twop_ex_max=twop_ex_max, - twop_peakGM=twop_peakgm, # Map from SQL result to model field + twop_peak_gm=twop_peak_gm, # Map from SQL result to model field twop_qy=twop_qy, is_dark=is_dark, - - + created_by_id=created_by_id, + updated_by_id=updated_by_id ) # Create State (MTI child) pointing to the Fluorophore # Use raw SQL to avoid Django MTI trying to update parent with empty values cursor.execute(""" - INSERT INTO proteins_state (fluorophore_ptr_id, name, protein_id, maturation) - VALUES (%s, %s, %s, %s) - """, [fluorophore.pk, name, protein_id, maturation]) + INSERT INTO proteins_state (fluorophore_ptr_id, protein_id, maturation) + VALUES (%s, %s, %s) + """, [fluorophore.pk, protein_id, maturation]) # Create FluorescenceMeasurement from old State data if there's any fluorescence data - if any([ex_max, em_max, qy, ext_coeff, lifetime, pka, twop_ex_max, twop_peakgm, twop_qy]): + if any([ex_max, em_max, qy, ext_coeff, lifetime, pka, twop_ex_max, twop_peak_gm, twop_qy]): FluorescenceMeasurement.objects.create( id=old_id, # Preserve old ID fluorophore=fluorophore, @@ -136,10 +137,12 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N lifetime=lifetime, pka=pka, twop_ex_max=twop_ex_max, - twop_peakGM=twop_peakgm, + twop_peak_gm=twop_peak_gm, twop_qy=twop_qy, is_dark=is_dark, is_trusted=True, # Mark as trusted since it's the original data + created_by_id=created_by_id, + updated_by_id=updated_by_id ) print(f"Migrated {State.objects.count()} State records") @@ -185,7 +188,7 @@ def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> Non for row in cursor.fetchall(): (old_id, created, modified, name, slug, is_dark, ex_max, em_max, ext_coeff, qy, brightness, - lifetime, pka, twop_ex_max, twop_peakgm, twop_qy) = row + lifetime, pka, twop_ex_max, twop_peak_gm, twop_qy) = row # Handle empty/null slugs if not slug or slug.strip() == "": @@ -222,11 +225,6 @@ def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> Non modified=modified, name=name, slug=dye_slug, - structural_status="PROPRIETARY", # Safe default for old dyes - - - # Note: Other fields like smiles, inchi, etc. can be added later - # if they exist in the old schema ) # Create Fluorophore for this DyeState @@ -254,9 +252,11 @@ def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> Non fluorophore = Fluorophore.objects.create( created=created, modified=modified, - label=name, + name="default", slug=fluorophore_slug, - entity_type="dye", + entity_type="d", + owner_name=dye.name, + owner_slug=dye.slug, ex_max=ex_max, em_max=em_max, ext_coeff=ext_coeff, @@ -265,7 +265,7 @@ def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> Non lifetime=lifetime, pka=pka, twop_ex_max=twop_ex_max, - twop_peakGM=twop_peakgm, + twop_peak_gm=twop_peak_gm, twop_qy=twop_qy, is_dark=is_dark, ) @@ -273,9 +273,9 @@ def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> Non # Create DyeState (one per old Dye) # Use raw SQL to avoid Django MTI trying to update parent with empty values cursor.execute(""" - INSERT INTO proteins_dyestate (fluorophore_ptr_id, dye_id, name, solvent, ph, environment, is_reference) - VALUES (%s, %s, %s, %s, %s, %s, %s) - """, [fluorophore.pk, dye.pk, "default", "PBS", 7.4, "FREE", True]) + INSERT INTO proteins_dyestate (fluorophore_ptr_id, dye_id) + VALUES (%s, %s) + """, [fluorophore.pk, dye.pk]) # Create FluorescenceMeasurement from old Dye data if any([ex_max, em_max, qy, ext_coeff, lifetime, pka]): @@ -292,12 +292,10 @@ def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> Non lifetime=lifetime, pka=pka, twop_ex_max=twop_ex_max, - twop_peakGM=twop_peakgm, + twop_peak_gm=twop_peak_gm, twop_qy=twop_qy, is_dark=is_dark, is_trusted=True, - - ) print(f"Migrated {Dye.objects.count()} Dye records to Dye containers") @@ -458,6 +456,52 @@ def migrate_reverse(_apps, _schema_editor): ) +def abstract_fluorescence_data_fields(): + """Return fresh field instances for AbstractFluorescenceData. + + Each model needs its own unique field instances to avoid state conflicts. + """ + return [ + ("is_dark", models.BooleanField(default=False, verbose_name="Dark State", help_text="This state does not fluorescence")), + ("ex_max", models.PositiveSmallIntegerField(blank=True, null=True, validators=[MinValueValidator(300), MaxValueValidator(900)], db_index=True, help_text="Excitation maximum (nm)")), + ("em_max", models.PositiveSmallIntegerField(blank=True, null=True, validators=[MinValueValidator(300), MaxValueValidator(1000)], db_index=True, help_text="Emission maximum (nm)")), + ("ext_coeff", models.IntegerField(blank=True, null=True, verbose_name="Extinction Coefficient (M-1 cm-1)", validators=[MinValueValidator(0), MaxValueValidator(300000)])), + ("qy", models.FloatField(null=True, blank=True, verbose_name="Quantum Yield", validators=[MinValueValidator(0), MaxValueValidator(1)])), + ("brightness", models.FloatField(null=True, blank=True, editable=False)), + ("lifetime", models.FloatField(null=True, blank=True, help_text="Lifetime (ns)", validators=[MinValueValidator(0), MaxValueValidator(20)])), + ("pka", models.FloatField(null=True, blank=True, verbose_name="pKa", validators=[MinValueValidator(2), MaxValueValidator(12)])), + ("twop_ex_max", models.PositiveSmallIntegerField(blank=True, null=True, verbose_name="Peak 2P excitation", validators=[MinValueValidator(700), MaxValueValidator(1600)], db_index=True)), + ("twop_peak_gm", models.FloatField(null=True, blank=True, verbose_name="Peak 2P cross-section of S0->S1 (GM)", validators=[MinValueValidator(0), MaxValueValidator(200)])), + ("twop_qy", models.FloatField(null=True, blank=True, verbose_name="2P Quantum Yield", validators=[MinValueValidator(0), MaxValueValidator(1)])), + ("emhex", models.CharField(max_length=7, blank=True)), + ("exhex", models.CharField(max_length=7, blank=True)), + ] + + +def authorable_mixin_fields(): + """Return fresh field instances for Authorable mixin.""" + return [ + ("created_by_id", models.IntegerField(null=True, blank=True)), + ("updated_by_id", models.IntegerField(null=True, blank=True)), + ] + + +def timestamped_mixin_fields(): + """Return fresh field instances for TimeStampedModel mixin.""" + return [ + ("created", model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name="created")), + ("modified", model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name="modified")), + ] + + +def product_mixin_fields(): + """Return fresh field instances for Product mixin.""" + return [ + ("manufacturer", models.CharField(max_length=128, blank=True)), + ("part", models.CharField(max_length=128, blank=True)), + ("url", models.URLField(blank=True)), + ] + class Migration(migrations.Migration): dependencies = [ ("proteins", "0058_snapgeneplasmid_protein_snapgene_plasmids"), @@ -465,6 +509,7 @@ class Migration(migrations.Migration): ] operations = [ + # Step 1: move State/Dye tables to _old versions and delete the models. migrations.SeparateDatabaseAndState( database_operations=[ # In the database: copy old tables to _old versions, then drop originals @@ -491,42 +536,30 @@ class Migration(migrations.Migration): ], ), - # Now create all new models + # Step 2: Create all new models + # Fluorophore is the new "State" base model for MTI + # State and DyeState are MTI children of Fluorophore + # What was Dye is now is a container model for DyeStates (like Protein for States) migrations.CreateModel( name="Fluorophore", fields=[ ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ("created", model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name="created")), - ("modified", model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name="modified")), - # Fluorescence data fields - ("ex_max", models.PositiveSmallIntegerField(blank=True, null=True, validators=[MinValueValidator(300), MaxValueValidator(900)], db_index=True, help_text="Excitation maximum (nm)")), - ("em_max", models.PositiveSmallIntegerField(blank=True, null=True, validators=[MinValueValidator(300), MaxValueValidator(1000)], db_index=True, help_text="Emission maximum (nm)")), - ("emhex", models.CharField(max_length=7, blank=True)), - ("exhex", models.CharField(max_length=7, blank=True)), - ("ext_coeff", models.IntegerField(blank=True, null=True, verbose_name="Extinction Coefficient (M-1 cm-1)", validators=[MinValueValidator(0), MaxValueValidator(300000)])), - ("qy", models.FloatField(null=True, blank=True, verbose_name="Quantum Yield", validators=[MinValueValidator(0), MaxValueValidator(1)])), - ("brightness", models.FloatField(null=True, blank=True, editable=False)), - ("lifetime", models.FloatField(null=True, blank=True, help_text="Lifetime (ns)", validators=[MinValueValidator(0), MaxValueValidator(20)])), - ("pka", models.FloatField(null=True, blank=True, verbose_name="pKa", validators=[MinValueValidator(2), MaxValueValidator(12)])), - ("twop_ex_max", models.PositiveSmallIntegerField(blank=True, null=True, verbose_name="Peak 2P excitation", validators=[MinValueValidator(700), MaxValueValidator(1600)], db_index=True)), - ("twop_peakGM", models.FloatField(null=True, blank=True, verbose_name="Peak 2P cross-section of S0->S1 (GM)", validators=[MinValueValidator(0), MaxValueValidator(200)])), - ("twop_qy", models.FloatField(null=True, blank=True, verbose_name="2P Quantum Yield", validators=[MinValueValidator(0), MaxValueValidator(1)])), - ("is_dark", models.BooleanField(default=False, verbose_name="Dark State", help_text="This state does not fluorescence")), - # Identity fields - ("label", models.CharField(max_length=255, db_index=True)), - ("slug", models.SlugField(max_length=100, unique=True)), # Increased from default 50 to accommodate long dye names - ("entity_type", models.CharField(max_length=10, choices=[("protein", "Protein"), ("dye", "Dye")], db_index=True)), - # Lineage tracking + *timestamped_mixin_fields(), + ("name", models.CharField(max_length=100, db_index=True, default="default")), + ("slug", models.SlugField(max_length=200, unique=True)), + ("entity_type", models.CharField(max_length=2, choices=[("p", "Protein"), ("d", "Dye")], db_index=True)), + ("owner_name", models.CharField(max_length=255, db_index=True, blank=True, null=True, help_text="Protein/Dye name (cached for searching)")), + ("owner_slug", models.SlugField(max_length=200, blank=True, null=True, help_text="Protein/Dye slug (cached for URLs)")), + *abstract_fluorescence_data_fields(), ("source_map", models.JSONField(default=dict, blank=True)), - # Author tracking (from Authorable mixin) - ("created_by_id", models.IntegerField(null=True, blank=True)), - ("updated_by_id", models.IntegerField(null=True, blank=True)), + *authorable_mixin_fields(), + ], options={ "indexes": [ models.Index(fields=["ex_max"], name="fluorophore_ex_max_idx"), models.Index(fields=["em_max"], name="fluorophore_em_max_idx"), - models.Index(fields=["label", "entity_type"], name="fluorophore_label_type_idx"), + models.Index(fields=["owner_name"], name="fluorophore_owner_name_idx"), models.Index(fields=["entity_type", "is_dark"], name="fluorophore_type_dark_idx"), ], }, @@ -536,32 +569,14 @@ class Migration(migrations.Migration): name="FluorescenceMeasurement", fields=[ ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ("created", model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name="created")), - ("modified", model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name="modified")), - # Fluorescence data fields (same as Fluorophore) - ("ex_max", models.PositiveSmallIntegerField(blank=True, null=True, validators=[MinValueValidator(300), MaxValueValidator(900)], db_index=True, help_text="Excitation maximum (nm)")), - ("em_max", models.PositiveSmallIntegerField(blank=True, null=True, validators=[MinValueValidator(300), MaxValueValidator(1000)], db_index=True, help_text="Emission maximum (nm)")), - ("emhex", models.CharField(max_length=7, blank=True)), - ("exhex", models.CharField(max_length=7, blank=True)), - ("ext_coeff", models.IntegerField(blank=True, null=True, verbose_name="Extinction Coefficient (M-1 cm-1)", validators=[MinValueValidator(0), MaxValueValidator(300000)])), - ("qy", models.FloatField(null=True, blank=True, verbose_name="Quantum Yield", validators=[MinValueValidator(0), MaxValueValidator(1)])), - ("brightness", models.FloatField(null=True, blank=True, editable=False)), - ("lifetime", models.FloatField(null=True, blank=True, help_text="Lifetime (ns)", validators=[MinValueValidator(0), MaxValueValidator(20)])), - ("pka", models.FloatField(null=True, blank=True, verbose_name="pKa", validators=[MinValueValidator(2), MaxValueValidator(12)])), - ("twop_ex_max", models.PositiveSmallIntegerField(blank=True, null=True, verbose_name="Peak 2P excitation", validators=[MinValueValidator(700), MaxValueValidator(1600)], db_index=True)), - ("twop_peakGM", models.FloatField(null=True, blank=True, verbose_name="Peak 2P cross-section of S0->S1 (GM)", validators=[MinValueValidator(0), MaxValueValidator(200)])), - ("twop_qy", models.FloatField(null=True, blank=True, verbose_name="2P Quantum Yield", validators=[MinValueValidator(0), MaxValueValidator(1)])), - ("is_dark", models.BooleanField(default=False, verbose_name="Dark State", help_text="This state does not fluorescence")), - # Measurement-specific fields + *abstract_fluorescence_data_fields(), ("date_measured", models.DateField(null=True, blank=True)), ("conditions", models.TextField(blank=True, help_text="pH, solvent, temp, etc.")), ("is_trusted", models.BooleanField(default=False, help_text="If True, this measurement overrides others.")), - # Foreign keys ("fluorophore", models.ForeignKey("Fluorophore", related_name="measurements", on_delete=django.db.models.deletion.CASCADE)), ("reference", models.ForeignKey("references.Reference", on_delete=django.db.models.deletion.CASCADE, null=True, blank=True)), - # Author tracking fields - ("created_by_id", models.IntegerField(null=True, blank=True)), - ("updated_by_id", models.IntegerField(null=True, blank=True)), + *authorable_mixin_fields(), + *timestamped_mixin_fields(), ], options={ "abstract": False, @@ -572,47 +587,18 @@ class Migration(migrations.Migration): name="Dye", fields=[ ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ("created", model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name="created")), - ("modified", model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name="modified")), ("name", models.CharField(max_length=255, db_index=True)), ("slug", models.SlugField(max_length=100, unique=True)), # Increased from default 50 to accommodate long names - ("synonyms", ArrayField(models.CharField(max_length=255), blank=True, default=list)), - ("structural_status", models.CharField(max_length=20, choices=[("DEFINED", "Defined Structure"), ("PROPRIETARY", "Proprietary / Unknown Structure")], default="DEFINED")), - ("canonical_smiles", models.TextField(blank=True)), - ("inchi", models.TextField(blank=True)), - ("inchikey", models.CharField(max_length=27, blank=True, db_index=True)), - ("molblock", models.TextField(blank=True, help_text="V3000 Molfile for precise rendering")), - ("parent_mixture", models.ForeignKey("self", on_delete=django.db.models.deletion.SET_NULL, null=True, blank=True, related_name="isomers")), - ("chemical_class", models.CharField(max_length=100, blank=True, db_index=True)), - ("equilibrium_constant_klz", models.FloatField(null=True, blank=True, help_text="Equilibrium constant between non-fluorescent lactone and fluorescent zwitterion.")), - # Product mixin fields - ("manufacturer", models.CharField(max_length=128, blank=True)), - ("part", models.CharField(max_length=128, blank=True)), - ("url", models.URLField(blank=True)), - # Author tracking fields - ("created_by_id", models.IntegerField(null=True, blank=True)), - ("updated_by_id", models.IntegerField(null=True, blank=True)), + *product_mixin_fields(), + *authorable_mixin_fields(), + *timestamped_mixin_fields(), ], - options={ - "constraints": [ - models.UniqueConstraint( - fields=["inchikey"], - name="unique_defined_molecule", - condition=models.Q(structural_status="DEFINED"), - ) - ], - }, ), migrations.CreateModel( name="DyeState", fields=[ ("fluorophore_ptr", models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to="proteins.fluorophore")), - ("name", models.CharField(max_length=255, help_text="e.g., 'Bound to DNA' or 'In Methanol'")), - ("solvent", models.CharField(max_length=100, default="PBS")), - ("ph", models.FloatField(default=7.4)), - ("environment", models.CharField(max_length=20, choices=[], default="FREE")), - ("is_reference", models.BooleanField(default=False, help_text="If True, this is the default state shown on the dye summary card.")), ("dye", models.ForeignKey("Dye", on_delete=django.db.models.deletion.CASCADE, related_name="states")), ], options={ @@ -626,9 +612,9 @@ class Migration(migrations.Migration): name="State", fields=[ ("fluorophore_ptr", models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to="proteins.fluorophore")), - ("name", models.CharField(max_length=64, default="default")), ("protein", models.ForeignKey("Protein", related_name="states", help_text="The protein to which this state belongs", on_delete=django.db.models.deletion.CASCADE)), ("maturation", models.FloatField(null=True, blank=True, help_text="Maturation time (min)", validators=[MinValueValidator(0), MaxValueValidator(1600)])), + ("transitions", models.ManyToManyField(blank=True, related_name="transition_state", through="proteins.StateTransition", to="proteins.state", verbose_name="State Transitions")), ], options={ "abstract": False, @@ -662,8 +648,14 @@ class Migration(migrations.Migration): ), ), + # Add index for spectrum owner_fluor lookups + migrations.AddIndex( + model_name="spectrum", + index=models.Index(fields=["owner_fluor_id", "status"], name="spectrum_fluor_status_idx"), + ), + # ---------------------------------------------- - + # Perform manual data migration steps migrations.RunPython(migrate_forward, migrate_reverse), diff --git a/backend/proteins/migrations/0059_notes.md b/backend/proteins/migrations/0059_notes.md index b725eca8c..dd02c4cc3 100644 --- a/backend/proteins/migrations/0059_notes.md +++ b/backend/proteins/migrations/0059_notes.md @@ -91,7 +91,7 @@ Transforms each old State record into: INSERT INTO proteins_state (fluorophore_ptr_id, name, protein_id, maturation) VALUES (%s, %s, %s, %s) ``` -- PostgreSQL column quoting: `"twop_peakGM"` in SELECT, mapped to lowercase variable in Python +- PostgreSQL column quoting: `"twop_peak_gm"` in SELECT, mapped to lowercase variable in Python - FluorescenceMeasurement created with `reference_id=protein.primary_reference_id` (may be None) **Migrated:** 1,055 State records @@ -251,15 +251,15 @@ This bypasses Django's save logic and directly creates the child record. ### Challenge 2: PostgreSQL Column Case Sensitivity -**Problem:** Column `twop_peakGM` created with mixed case, but unquoted identifiers in SQL become lowercase: +**Problem:** Column `twop_peak_gm` created with mixed case, but unquoted identifiers in SQL become lowercase: ``` -UndefinedColumn: column "twop_peakgm" does not exist +UndefinedColumn: column "twop_peak_gm" does not exist ``` **Solution:** Quote column names in SELECT statements: ```python cursor.execute(""" - SELECT ..., "twop_peakGM", ... + SELECT ..., "twop_peak_gm", ... FROM proteins_state_old """) ``` diff --git a/backend/proteins/models/dye.py b/backend/proteins/models/dye.py index 152c207cf..abfdbc93d 100644 --- a/backend/proteins/models/dye.py +++ b/backend/proteins/models/dye.py @@ -1,6 +1,5 @@ from typing import TYPE_CHECKING -from django.contrib.postgres.fields import ArrayField from django.db import models from model_utils.models import TimeStampedModel @@ -8,7 +7,7 @@ from proteins.models.mixins import Authorable, Product if TYPE_CHECKING: - from proteins.models import DyeState, ReactiveDerivative + from proteins.models import DyeState from references.models import Reference # noqa: F401 @@ -23,62 +22,70 @@ class Dye(Authorable, TimeStampedModel, Product): # TODO: rename to SmallMolecu slug = models.SlugField(unique=True) # Synonyms allow users to find "FITC" when searching "Fluorescein" - synonyms = ArrayField(models.CharField(max_length=255), blank=True, default=list) + # synonyms = ArrayField(models.CharField(max_length=255), blank=True, default=list) # --- Structural Status (The "Regret-Proof" Field) --- # Allows entry of proprietary dyes without forcing a fake structure. - STRUCTURE_STATUS_CHOICES = [ - ("DEFINED", "Defined Structure"), - ("PROPRIETARY", "Proprietary / Unknown Structure"), - ] - structural_status = models.CharField(max_length=20, choices=STRUCTURE_STATUS_CHOICES, default="DEFINED") + # STRUCTURE_STATUS_CHOICES = [ + # ("DEFINED", "Defined Structure"), + # ("PROPRIETARY", "Proprietary / Unknown Structure"), + # ] + # structural_status = models.CharField(max_length=20, choices=STRUCTURE_STATUS_CHOICES, default="DEFINED") # --- Chemical Graph Data (Nullable for Proprietary Dyes) --- # We prioritize MolBlock for rendering, InChIKey for de-duplication. - canonical_smiles = models.TextField(blank=True) - inchi = models.TextField(blank=True) - inchikey = models.CharField(max_length=27, blank=True, db_index=True) - molblock = models.TextField(blank=True, help_text="V3000 Molfile for precise rendering") + # canonical_smiles = models.TextField(blank=True) + # inchi = models.TextField(blank=True) + # inchikey = models.CharField(max_length=27, blank=True, db_index=True) + # molblock = models.TextField(blank=True, help_text="V3000 Molfile for precise rendering") # --- Hierarchy & Ontology --- # Handles FITC (Parent) vs 5-FITC (Child) relationship - parent_mixture_id: int | None - parent_mixture = models.ForeignKey["Dye | None"]( - "self", - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name="isomers", - ) + # parent_mixture_id: int | None + # parent_mixture = models.ForeignKey["Dye | None"]( + # "self", + # on_delete=models.SET_NULL, + # null=True, + # blank=True, + # related_name="isomers", + # ) # Automated classification (e.g., "Rhodamine", "Cyanine", "BODIPY") # Populated via ClassyFire API or manual curation - chemical_class = models.CharField(max_length=100, blank=True, db_index=True) + # chemical_class = models.CharField(max_length=100, blank=True, db_index=True) # --- Intrinsic Physics --- # Critical for fluorogenic dyes (JF dyes, SiR-tubulin) # Describes the Lactone-Zwitterion equilibrium constant. - equilibrium_constant_klz = models.FloatField( - null=True, - blank=True, - help_text="Equilibrium constant between non-fluorescent lactone and fluorescent zwitterion.", - ) + # equilibrium_constant_klz = models.FloatField( + # null=True, + # blank=True, + # help_text="Equilibrium constant between non-fluorescent lactone and fluorescent zwitterion.", + # ) if TYPE_CHECKING: - isomers: models.QuerySet["Dye"] - states: models.QuerySet[DyeState] - derivatives: models.QuerySet[ReactiveDerivative] - collection_memberships: models.QuerySet + states = models.manager.RelatedManager["DyeState"]() + # isomers: models.QuerySet["Dye"] + # derivatives: models.QuerySet[ReactiveDerivative] + # collection_memberships: models.QuerySet class Meta: + ... # Enforce uniqueness only on defined structures to allow multiple proprietary entries - constraints = [ - models.UniqueConstraint( - fields=["inchikey"], - name="unique_defined_molecule", - condition=models.Q(structural_status="DEFINED"), - ) - ] + # constraints = [ + # models.UniqueConstraint( + # fields=["inchikey"], + # name="unique_defined_molecule", + # condition=models.Q(structural_status="DEFINED"), + # ) + # ] + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + # Update cached owner fields on all states when dye name/slug changes + # These fields are cached on Fluorophore for query performance + if self.pk: + self.states.update(owner_name=self.name, owner_slug=self.slug) def get_primary_spectrum(self): """Returns the 'Reference' DyeState (e.g. Protein-bound) for display cards.""" @@ -89,8 +96,8 @@ def get_primary_spectrum(self): # This allows us to store "Alexa 488 in PBS" and "Alexa 488 in Ethanol" as valid, # separate datasets. class DyeState(Fluorophore): - """ - Represents a SmallMolecule in a specific environmental context. + """Represents a SmallMolecule in a specific environmental context. + This holds the actual spectral data. """ @@ -98,25 +105,29 @@ class DyeState(Fluorophore): dye = models.ForeignKey["Dye"](Dye, on_delete=models.CASCADE, related_name="states") # --- Context --- - name = models.CharField(max_length=255, help_text="e.g., 'Bound to DNA' or 'In Methanol'") - solvent = models.CharField(max_length=100, default="PBS") - ph = models.FloatField(default=7.4) + # name = models.CharField(max_length=255, help_text="e.g., 'Bound to DNA' or 'In Methanol'") + # solvent = models.CharField(max_length=100, default="PBS") + # ph = models.FloatField(default=7.4) # --- Environmental Categorization --- # Helps the UI decide which spectrum to show for a specific query. - ENVIRONMENT_CHOICES = [] - environment = models.CharField(max_length=20, choices=ENVIRONMENT_CHOICES, default="FREE") + # ENVIRONMENT_CHOICES = [] + # environment = models.CharField(max_length=20, choices=ENVIRONMENT_CHOICES, default="FREE") # --- Logic --- - is_reference = models.BooleanField( - default=False, help_text="If True, this is the default state shown on the dye summary card." - ) + # is_reference = models.BooleanField( + # default=False, help_text="If True, this is the default state shown on the dye summary card." + # ) if TYPE_CHECKING: fluorophore_ptr: Fluorophore # added by Django MTI def save(self, *args, **kwargs): - self.entity_type = "dye" + self.entity_type = Fluorophore.EntityTypes.DYE + # Cache parent dye info for efficient searching + if self.dye_id: + self.owner_name = self.dye.name + self.owner_slug = self.dye.slug super().save(*args, **kwargs) def get_absolute_url(self): @@ -132,45 +143,45 @@ def get_absolute_url(self): # It separates "Chemist Tools" (Reactive Dyes) from "Biologist Tools" (Antibody Conjugates). -class ReactiveDerivative(models.Model): - """A sold product derived from the SmallMolecule. +# class ReactiveDerivative(models.Model): +# """A sold product derived from the SmallMolecule. - e.g., 'Janelia Fluor 549 NHS Ester' or 'JF549-HaloTag Ligand' - These are the products users buy to perform conjugation - (e.g., NHS esters, Maleimides, HaloTag Ligands). - """ +# e.g., 'Janelia Fluor 549 NHS Ester' or 'JF549-HaloTag Ligand' +# These are the products users buy to perform conjugation +# (e.g., NHS esters, Maleimides, HaloTag Ligands). +# """ + +# core_dye_id: int +# core_dye = models.ForeignKey["Dye"]( +# Dye, +# on_delete=models.CASCADE, +# related_name="derivatives", +# ) + +# # --- Chemistry --- +# REACTIVE_GROUP_CHOICES = [ +# ("NHS_ESTER", "NHS Ester"), +# ("HALO_TAG", "HaloTag Ligand"), +# ("SNAP_TAG", "SNAP-Tag Ligand"), +# ("CLIP_TAG", "CLIP-Tag Ligand"), +# ("MALEIMIDE", "Maleimide"), +# ("AZIDE", "Azide"), +# ("ALKYNE", "Alkyne"), +# ("BIOTIN", "Biotin"), +# ("OTHER", "Other"), +# ] +# reactive_group = models.CharField(max_length=10, choices=REACTIVE_GROUP_CHOICES) + +# # Specific structure of the linker/handle (distinct from core dye) +# full_smiles = models.TextField(blank=True, help_text="Structure of the complete reactive molecule") +# molecular_weight = models.FloatField() + +# # --- Vendor Info --- +# vendor = models.CharField(max_length=100) +# catalog_number = models.CharField(max_length=100) - core_dye_id: int - core_dye = models.ForeignKey["Dye"]( - Dye, - on_delete=models.CASCADE, - related_name="derivatives", - ) - - # --- Chemistry --- - REACTIVE_GROUP_CHOICES = [ - ("NHS_ESTER", "NHS Ester"), - ("HALO_TAG", "HaloTag Ligand"), - ("SNAP_TAG", "SNAP-Tag Ligand"), - ("CLIP_TAG", "CLIP-Tag Ligand"), - ("MALEIMIDE", "Maleimide"), - ("AZIDE", "Azide"), - ("ALKYNE", "Alkyne"), - ("BIOTIN", "Biotin"), - ("OTHER", "Other"), - ] - reactive_group = models.CharField(max_length=10, choices=REACTIVE_GROUP_CHOICES) - - # Specific structure of the linker/handle (distinct from core dye) - full_smiles = models.TextField(blank=True, help_text="Structure of the complete reactive molecule") - molecular_weight = models.FloatField() - - # --- Vendor Info --- - vendor = models.CharField(max_length=100) - catalog_number = models.CharField(max_length=100) - - def __str__(self) -> str: - return f"{self.core_dye.common_name} - {self.reactive_group} ({self.vendor} {self.catalog_number})" +# def __str__(self) -> str: +# return f"{self.core_dye.common_name} - {self.reactive_group} ({self.vendor} {self.catalog_number})" # Architectural Decision: We do not create a new DyeState or SmallMolecule for every diff --git a/backend/proteins/models/fluorescence_data.py b/backend/proteins/models/fluorescence_data.py index 6d475fd78..1ac00b8a7 100644 --- a/backend/proteins/models/fluorescence_data.py +++ b/backend/proteins/models/fluorescence_data.py @@ -10,6 +10,7 @@ class AbstractFluorescenceData(Authorable, TimeStampedModel, models.Model): """Defines the physics schema. + Used by both the Measurement (Input) and Fluorophore (Output/Cache). """ @@ -65,7 +66,7 @@ class AbstractFluorescenceData(Authorable, TimeStampedModel, models.Model): validators=[MinValueValidator(700), MaxValueValidator(1600)], db_index=True, ) - twop_peakGM = models.FloatField( + twop_peak_gm = models.FloatField( null=True, blank=True, verbose_name="Peak 2P cross-section of S0->S1 (GM)", @@ -113,7 +114,7 @@ def get_measurable_fields(cls): "lifetime", "pka", "twop_ex_max", - "twop_peakGM", + "twop_peak_gm", "twop_qy", "is_dark", } diff --git a/backend/proteins/models/fluorophore.py b/backend/proteins/models/fluorophore.py index 79392c428..41ecb5d98 100644 --- a/backend/proteins/models/fluorophore.py +++ b/backend/proteins/models/fluorophore.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING, Final, Literal from django.db import models from django.db.models import QuerySet @@ -47,13 +47,31 @@ class Fluorophore(AbstractFluorescenceData): """ class EntityTypes(models.TextChoices): - PROTEIN = ("protein", "Protein") - DYE = ("dye", "Dye") + PROTEIN = ("p", "Protein") + DYE = ("d", "Dye") - # Identity (Hoisted for performance) - label = models.CharField(max_length=255, db_index=True) - slug = models.SlugField(unique=True) - entity_type = models.CharField(max_length=10, choices=EntityTypes, db_index=True) + # Identity + DEFAULT_NAME: Final = "default" + + # State label (distinguishes states within same parent: "default", "red", "green") + name = models.CharField(max_length=100, default=DEFAULT_NAME, db_index=True) + + # Cached parent info (denormalized for search performance) + owner_name = models.CharField( + max_length=255, + db_index=True, + blank=True, + help_text="Protein/Dye name (cached for searching)", + ) + owner_slug = models.SlugField( + max_length=200, + blank=True, + help_text="Protein/Dye slug (cached for URLs)", + ) + + # Unique identifier (typically {owner_slug}-{state_name}) + slug = models.SlugField(max_length=200, unique=True) + entity_type = models.CharField(max_length=2, choices=EntityTypes, db_index=True) # Lineage Tracking # Maps field names to Measurement IDs. e.g., {'ex_max': 102, 'qy': 105} @@ -73,15 +91,24 @@ class EntityTypes(models.TextChoices): class Meta: indexes = [ - models.Index(fields=["ex_max"]), - models.Index(fields=["em_max"]), - models.Index(fields=["label", "entity_type"]), - models.Index(fields=["entity_type", "is_dark"]), + models.Index(fields=["ex_max"], name="fluorophore_ex_max_idx"), + models.Index(fields=["em_max"], name="fluorophore_em_max_idx"), + models.Index(fields=["owner_name"], name="fluorophore_owner_name_idx"), + models.Index(fields=["entity_type", "is_dark"], name="fluorophore_type_dark_idx"), ] def __str__(self): return self.label + @property + def label(self) -> str: + """Human-readable display name: 'EGFP' or 'mEos3.2 (red)'.""" + if not self.owner_name: + return self.name + if self.name == self.DEFAULT_NAME: + return self.owner_name + return f"{self.owner_name} ({self.name})" + def as_subclass(self) -> "Self": """Downcast to the specific subclass instance.""" for subclass_name in ["dyestate", "state"]: @@ -129,16 +156,6 @@ def rebuild_attributes(self): setattr(self, key, val) self.source_map = new_source_map - - # 4. Refresh Label (Hoisting) - # Django MTI creates reverse relations with lowercase model names - if hasattr(self, "state"): - ps = self.state - self.label = f"{ps.protein.name} ({ps.name})" - elif hasattr(self, "dyestate"): - ds = self.dyestate - self.label = f"{ds.dye.name} ({ds.name})" - self.save() @property diff --git a/backend/proteins/models/protein.py b/backend/proteins/models/protein.py index 82c7d0605..4ba52c767 100644 --- a/backend/proteins/models/protein.py +++ b/backend/proteins/models/protein.py @@ -433,10 +433,10 @@ def save(self, *args, **kwargs): if self.set_default_state(): super().save() - # Update label on all states when protein name changes - # The label field is cached on Fluorophore for query performance + # Update cached owner fields on all states when protein name/slug changes + # These fields are cached on Fluorophore for query performance if self.pk: - self.states.update(label=self.name) + self.states.update(owner_name=self.name, owner_slug=self.slug) class Meta: ordering = ["name"] @@ -558,9 +558,6 @@ def first_author(self): class State(Fluorophore): # TODO: rename to ProteinState - DEFAULT_NAME = "default" - - name = models.CharField(max_length=64, default=DEFAULT_NAME) # required protein_id: int protein = models.ForeignKey["Protein"]( Protein, @@ -593,9 +590,10 @@ class State(Fluorophore): # TODO: rename to ProteinState def save(self, *args, **kwargs) -> None: self.entity_type = self.EntityTypes.PROTEIN - # Set label to protein name (used in API/UI for display) + # Cache parent protein info for efficient searching if self.protein_id: - self.label = self.protein.name + self.owner_name = self.protein.name + self.owner_slug = self.protein.slug super().save(*args, **kwargs) def get_absolute_url(self): diff --git a/backend/proteins/models/spectrum.py b/backend/proteins/models/spectrum.py index 86474ae69..e875a936e 100644 --- a/backend/proteins/models/spectrum.py +++ b/backend/proteins/models/spectrum.py @@ -125,10 +125,10 @@ def owner_case(field: str, **owner_fields) -> Case: .annotate( owner_id=owner_case("id"), owner_slug=owner_case("slug"), - # Fluorophore has 'label', others have 'name' + # Fluorophore uses cached owner_name, others use direct name field owner_name=owner_case( "name", - fluor=F("owner_fluor__label"), + fluor=F("owner_fluor__owner_name"), ), # Fluorophore doesn't have URL, others do owner_url=owner_case( @@ -164,7 +164,7 @@ def fluor_slugs(self): return ( self.get_queryset() .exclude(owner_fluor=None) - .values_list("owner_fluor__slug", "owner_fluor__label") + .values_list("owner_fluor__slug", "owner_fluor__owner_name") .distinct() ) @@ -174,7 +174,7 @@ def fluorlist(self): "category", "subtype", "owner_fluor__slug", - "owner_fluor__label", + "owner_fluor__owner_name", ] Q = ( self.get_queryset() @@ -191,7 +191,7 @@ def fluorlist(self): "category": v["category"], "subtype": v["subtype"], "slug": v["owner_fluor__slug"], - "name": v["owner_fluor__label"], + "name": v["owner_fluor__owner_name"], } ) return sorted(out, key=lambda k: k["name"]) @@ -205,7 +205,7 @@ def filter_owner(self, slug): def find_similar_owners(self, query, threshold=0.4): A = ( - "owner_fluor__label", # Fluorophore.label for both State and DyeState + "owner_fluor__owner_name", # Search on cached parent name (Protein/Dye) "owner_filter__name", "owner_light__name", "owner_camera__name", @@ -581,7 +581,7 @@ def d3dict(self): elif self.subtype == self.EM: D.update({"scalar": self.owner.qy, "em_max": self.owner.em_max}) elif self.subtype == self.TWOP: - D.update({"scalar": self.owner.twop_peakGM, "twop_qy": self.owner.twop_qy}) + D.update({"scalar": self.owner.twop_peak_gm, "twop_qy": self.owner.twop_qy}) return D def d3data(self): diff --git a/backend/proteins/schema/types.py b/backend/proteins/schema/types.py index 3568b0f48..7e7daf0d5 100644 --- a/backend/proteins/schema/types.py +++ b/backend/proteins/schema/types.py @@ -123,7 +123,7 @@ def resolve_primaryReference(self, info): class FluorophoreInterface(graphene.Interface): qy = graphene.Float() extCoeff = graphene.Float(source="ext_coeff") - twopPeakgm = graphene.Float(source="twop_peakGM") + twopPeakgm = graphene.Float(source="twop_peak_gm") exMax = graphene.Float(source="ex_max") emMax = graphene.Float(source="em_max") diff --git a/backend/tests/test_api/test_graphql_schema.py b/backend/tests/test_api/test_graphql_schema.py index be1e07da9..36cd4490a 100644 --- a/backend/tests/test_api/test_graphql_schema.py +++ b/backend/tests/test_api/test_graphql_schema.py @@ -85,7 +85,7 @@ def setUp(self): # Create a DyeState (Fluorophore) for the dye from proteins.models.dye import DyeState - self.dye_state = DyeState.objects.create(dye=self.dye, name="test state", label="test-dye") + self.dye_state = DyeState.objects.create(dye=self.dye, name="test state") self.spectrum = models.Spectrum.objects.get_or_create( category=models.Spectrum.DYE, subtype=models.Spectrum.EM, @@ -169,7 +169,8 @@ def test_spectra(self): content = json.loads(response.content) last_spectrum = content["data"]["spectra"][-1] self.assertEqual(last_spectrum["id"], str(self.spectrum.id)) - self.assertEqual(last_spectrum["owner"]["name"], self.dye_state.label) + # Owner name should be the parent dye's name, not the state's name + self.assertEqual(last_spectrum["owner"]["name"], self.dye.name) def test_protein(self): response = self.query( diff --git a/backend/tests/test_proteins/test_ajax_views.py b/backend/tests/test_proteins/test_ajax_views.py index ac92935d5..8eb07e248 100644 --- a/backend/tests/test_proteins/test_ajax_views.py +++ b/backend/tests/test_proteins/test_ajax_views.py @@ -55,7 +55,6 @@ def setUpTestData(cls): dye=dye, name=f"SimilarDye{i} state", slug=f"similardye{i}-state", - label=f"SimilarDye{i}", ex_max=488, em_max=520, ) From caeb8dc9154722a51c957dbcddb15b6068ddd372 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Mon, 24 Nov 2025 14:06:19 -0500 Subject: [PATCH 26/57] undo changes to old migrations --- backend/proteins/migrations/0001_initial.py | 857 ++++-------------- .../migrations/0002_auto_20180312_0156.py | 54 +- .../proteins/migrations/0003_protein_oser.py | 11 +- .../migrations/0004_auto_20180315_1323.py | 9 +- .../migrations/0005_auto_20180328_1614.py | 127 +-- ...8_1614_squashed_0010_auto_20180501_1547.py | 640 +++---------- .../migrations/0006_auto_20180401_2009.py | 356 ++------ .../migrations/0006_auto_20180512_0058.py | 7 +- .../migrations/0007_auto_20180403_0140.py | 67 +- .../migrations/0007_auto_20180513_2346.py | 101 +-- .../migrations/0008_auto_20180430_0308.py | 58 +- .../migrations/0008_auto_20180515_1659.py | 19 +- .../migrations/0009_auto_20180430_0335.py | 15 +- .../migrations/0009_auto_20180525_1640.py | 27 +- .../migrations/0010_auto_20180501_1547.py | 50 +- .../migrations/0010_auto_20180525_1840.py | 53 +- .../migrations/0011_auto_20180525_2349.py | 38 +- .../migrations/0012_auto_20180708_1811.py | 29 +- .../migrations/0013_auto_20180718_1717.py | 354 +++----- .../migrations/0014_auto_20180718_2340.py | 29 +- .../migrations/0015_auto_20180720_1921.py | 31 +- .../migrations/0016_auto_20180722_1314.py | 31 +- .../migrations/0017_auto_20180722_1626.py | 31 +- .../migrations/0018_protein_cofactor.py | 9 +- .../migrations/0019_auto_20180723_2200.py | 14 +- .../migrations/0020_auto_20180729_0234.py | 50 +- .../migrations/0021_auto_20180804_0203.py | 31 +- .../migrations/0022_osermeasurement.py | 160 +--- .../migrations/0023_spectrum_reference.py | 19 +- .../migrations/0024_auto_20181011_1659.py | 15 +- .../migrations/0025_auto_20181011_1715.py | 33 +- .../0026_bleachmeasurement_cell_type.py | 9 +- .../migrations/0027_auto_20181011_1754.py | 9 +- .../migrations/0028_auto_20181012_2011.py | 98 +- .../migrations/0029_auto_20181014_1241.py | 15 +- backend/proteins/migrations/0030_lineage.py | 66 +- .../migrations/0031_auto_20181103_1531.py | 9 +- .../migrations/0032_auto_20181107_2015.py | 18 +- .../migrations/0033_auto_20181107_2119.py | 29 +- .../migrations/0034_lineage_rootmut.py | 7 +- .../migrations/0035_auto_20181110_0103.py | 9 +- .../migrations/0036_lineage_root_node.py | 17 +- .../migrations/0037_auto_20181205_2035.py | 14 +- .../migrations/0038_auto_20181205_2044.py | 14 +- .../migrations/0039_auto_20181206_0009.py | 19 +- .../migrations/0040_auto_20181210_0345.py | 14 +- .../migrations/0041_auto_20181216_1743.py | 31 +- .../migrations/0042_auto_20181216_1744.py | 9 +- .../migrations/0043_remove_excerpt_protein.py | 7 +- .../migrations/0044_auto_20181218_1310.py | 9 +- .../proteins/migrations/0045_dye_is_dark.py | 11 +- .../migrations/0046_auto_20190121_1341.py | 36 +- .../migrations/0047_auto_20190319_1525.py | 89 +- .../migrations/0048_change_protein_uuid.py | 23 +- .../migrations/0049_auto_20190323_1947.py | 17 +- .../migrations/0050_auto_20190714_1318.py | 5 +- ...r_bleachmeasurement_created_by_and_more.py | 70 +- .../0053_alter_protein_chromophore.py | 1 - ...spectrum_status_spectrum_status_changed.py | 3 +- ...trum_spectrum_state_status_idx_and_more.py | 13 +- .../migrations/0057_add_status_index.py | 9 +- ...apgeneplasmid_protein_snapgene_plasmids.py | 36 +- 62 files changed, 1094 insertions(+), 2947 deletions(-) diff --git a/backend/proteins/migrations/0001_initial.py b/backend/proteins/migrations/0001_initial.py index 48d52dce1..e6f3f2d1c 100644 --- a/backend/proteins/migrations/0001_initial.py +++ b/backend/proteins/migrations/0001_initial.py @@ -1,799 +1,256 @@ +# -*- coding: utf-8 -*- # Generated by Django 1.11.9 on 2018-02-13 01:08 +from __future__ import unicode_literals -import uuid - +from django.conf import settings import django.contrib.postgres.fields import django.core.validators +from django.db import migrations, models +from django.contrib.postgres.operations import TrigramExtension import django.db.models.deletion import django.utils.timezone import model_utils.fields -from django.conf import settings -from django.contrib.postgres.operations import TrigramExtension -from django.db import migrations, models - import proteins.fields import proteins.validators +import uuid class Migration(migrations.Migration): + initial = True dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("references", "0001_initial"), + ('references', '0001_initial'), ] operations = [ TrigramExtension(), migrations.CreateModel( - name="BleachMeasurement", + name='BleachMeasurement', fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), - ), - ( - "rate", - models.FloatField( - help_text="Photobleaching half-life (s)", - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(3000), - ], - verbose_name="Bleach Rate", - ), - ), - ( - "power", - models.FloatField( - blank=True, - help_text="If not reported, use '-1'", - null=True, - validators=[django.core.validators.MinValueValidator(-1)], - verbose_name="Illumination Power", - ), - ), - ( - "units", - models.CharField(blank=True, help_text="e.g. W/cm2", max_length=100, verbose_name="Power Unit"), - ), - ( - "light", - models.CharField( - blank=True, - choices=[("a", "Arc-lamp"), ("la", "Laser"), ("le", "LED"), ("o", "Other")], - max_length=2, - verbose_name="Light Source", - ), - ), - ( - "modality", - models.CharField( - blank=True, - choices=[ - ("wf", "Widefield"), - ("ps", "Point Scanning Confocal"), - ("sd", "Spinning Disc Confocal"), - ("s", "Spectrophotometer"), - ("t", "TIRF"), - ("o", "Other"), - ], - max_length=2, - verbose_name="Imaging Modality", - ), - ), - ("temp", models.FloatField(blank=True, null=True, verbose_name="Temperature")), - ( - "fusion", - models.CharField( - blank=True, help_text="(if applicable)", max_length=60, verbose_name="Fusion Protein" - ), - ), - ( - "in_cell", - models.IntegerField( - blank=True, - choices=[(-1, "Unkown"), (0, "No"), (1, "Yes")], - default=-1, - help_text="protein expressed in living cells", - verbose_name="In cells?", - ), - ), - ( - "created_by", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="bleachmeasurement_author", - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "reference", - models.ForeignKey( - blank=True, - help_text="Reference where the measurement was made", - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="bleach_measurements", - to="references.Reference", - verbose_name="Measurement Reference", - ), - ), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('rate', models.FloatField(help_text='Photobleaching half-life (s)', validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(3000)], verbose_name='Bleach Rate')), + ('power', models.FloatField(blank=True, help_text="If not reported, use '-1'", null=True, validators=[django.core.validators.MinValueValidator(-1)], verbose_name='Illumination Power')), + ('units', models.CharField(blank=True, help_text='e.g. W/cm2', max_length=100, verbose_name='Power Unit')), + ('light', models.CharField(blank=True, choices=[('a', 'Arc-lamp'), ('la', 'Laser'), ('le', 'LED'), ('o', 'Other')], max_length=2, verbose_name='Light Source')), + ('modality', models.CharField(blank=True, choices=[('wf', 'Widefield'), ('ps', 'Point Scanning Confocal'), ('sd', 'Spinning Disc Confocal'), ('s', 'Spectrophotometer'), ('t', 'TIRF'), ('o', 'Other')], max_length=2, verbose_name='Imaging Modality')), + ('temp', models.FloatField(blank=True, null=True, verbose_name='Temperature')), + ('fusion', models.CharField(blank=True, help_text='(if applicable)', max_length=60, verbose_name='Fusion Protein')), + ('in_cell', models.IntegerField(blank=True, choices=[(-1, 'Unkown'), (0, 'No'), (1, 'Yes')], default=-1, help_text='protein expressed in living cells', verbose_name='In cells?')), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='bleachmeasurement_author', to=settings.AUTH_USER_MODEL)), + ('reference', models.ForeignKey(blank=True, help_text='Reference where the measurement was made', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bleach_measurements', to='references.Reference', verbose_name='Measurement Reference')), ], options={ - "abstract": False, + 'abstract': False, }, ), migrations.CreateModel( - name="FRETpair", + name='FRETpair', fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), - ), - ("radius", models.FloatField(blank=True, null=True)), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('radius', models.FloatField(blank=True, null=True)), ], options={ - "verbose_name": "FRET Pair", + 'verbose_name': 'FRET Pair', }, ), migrations.CreateModel( - name="Mutation", + name='Mutation', fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ( - "mutations", - django.contrib.postgres.fields.ArrayField( - base_field=models.CharField(max_length=5), - size=None, - validators=[ - django.core.validators.RegexValidator( - "^[ACDEFGHIKLMNPQRSTVWY-][1-9][0-9]{0,2}[ACDEFGHIKLMNPQRSTVWY]$", - "not a valid mutation code: eg S65T", - ) - ], - ), - ), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('mutations', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=5), size=None, validators=[django.core.validators.RegexValidator('^[ACDEFGHIKLMNPQRSTVWY-][1-9][0-9]{0,2}[ACDEFGHIKLMNPQRSTVWY]$', 'not a valid mutation code: eg S65T')])), ], ), migrations.CreateModel( - name="Organism", + name='Organism', fields=[ - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), - ), - ( - "id", - models.PositiveIntegerField( - help_text="NCBI Taxonomy ID", primary_key=True, serialize=False, verbose_name="Taxonomy ID" - ), - ), - ("scientific_name", models.CharField(blank=True, max_length=128)), - ("division", models.CharField(blank=True, max_length=128)), - ("common_name", models.CharField(blank=True, max_length=128)), - ("species", models.CharField(blank=True, max_length=128)), - ("genus", models.CharField(blank=True, max_length=128)), - ("rank", models.CharField(blank=True, max_length=128)), - ( - "created_by", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="organism_author", - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "updated_by", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="organism_modifier", - to=settings.AUTH_USER_MODEL, - ), - ), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('id', models.PositiveIntegerField(help_text='NCBI Taxonomy ID', primary_key=True, serialize=False, verbose_name='Taxonomy ID')), + ('scientific_name', models.CharField(blank=True, max_length=128)), + ('division', models.CharField(blank=True, max_length=128)), + ('common_name', models.CharField(blank=True, max_length=128)), + ('species', models.CharField(blank=True, max_length=128)), + ('genus', models.CharField(blank=True, max_length=128)), + ('rank', models.CharField(blank=True, max_length=128)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='organism_author', to=settings.AUTH_USER_MODEL)), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='organism_modifier', to=settings.AUTH_USER_MODEL)), ], options={ - "verbose_name": "Organism", - "ordering": ["scientific_name"], + 'verbose_name': 'Organism', + 'ordering': ['scientific_name'], }, ), migrations.CreateModel( - name="Protein", + name='Protein', fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), - ), - ( - "status", - model_utils.fields.StatusField( - choices=[("pending", "pending"), ("approved", "approved"), ("hidden", "hidden")], - default="pending", - max_length=100, - no_check_for_status=True, - verbose_name="status", - ), - ), - ( - "status_changed", - model_utils.fields.MonitorField( - default=django.utils.timezone.now, monitor="status", verbose_name="status changed" - ), - ), - ("uuid", models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), - ("name", models.CharField(db_index=True, help_text="Name of the fluorescent protein", max_length=128)), - ("slug", models.SlugField(help_text="URL slug for the protein", max_length=64, unique=True)), - ("base_name", models.CharField(max_length=128)), - ( - "aliases", - django.contrib.postgres.fields.ArrayField( - base_field=models.CharField(max_length=200), blank=True, null=True, size=None - ), - ), - ("chromophore", models.CharField(blank=True, max_length=5, null=True)), - ( - "seq", - models.CharField( - blank=True, - help_text="Amino acid sequence (IPG ID is preferred)", - max_length=1024, - null=True, - unique=True, - validators=[proteins.validators.protein_sequence_validator], - verbose_name="Sequence", - ), - ), - ( - "pdb", - django.contrib.postgres.fields.ArrayField( - base_field=models.CharField(max_length=4), - blank=True, - null=True, - size=None, - verbose_name="Protein DataBank ID", - ), - ), - ( - "genbank", - models.CharField( - blank=True, - help_text="NCBI Genbank Accession", - max_length=12, - null=True, - unique=True, - verbose_name="Genbank Accession", - ), - ), - ( - "uniprot", - models.CharField( - blank=True, - max_length=10, - null=True, - unique=True, - validators=[ - django.core.validators.RegexValidator( - "[OPQ][0-9][A-Z0-9]{3}[0-9]|[A-NR-Z][0-9]([A-Z][A-Z0-9]{2}[0-9]){1,2}", - "Not a valid UniProt Accession", - ) - ], - verbose_name="UniProtKB Accession", - ), - ), - ( - "ipg_id", - models.CharField( - blank=True, - help_text="Identical Protein Group ID at Pubmed", - max_length=12, - null=True, - unique=True, - verbose_name="IPG ID", - ), - ), - ("mw", models.FloatField(blank=True, help_text="Molecular Weight", null=True)), - ( - "agg", - models.CharField( - blank=True, - choices=[ - ("m", "Monomer"), - ("d", "Dimer"), - ("td", "Tandem dimer"), - ("wd", "Weak dimer"), - ("t", "Tetramer"), - ], - help_text="Oligomerization tendency", - max_length=2, - ), - ), - ( - "switch_type", - models.CharField( - blank=True, - choices=[ - ("b", "Basic"), - ("pa", "Photoactivatable"), - ("ps", "Photoswitchable"), - ("pc", "Photoconvertible"), - ("t", "Timer"), - ("o", "Multistate"), - ], - help_text="Photoswitching type (basic if none)", - max_length=2, - verbose_name="Type", - ), - ), - ("blurb", models.CharField(blank=True, help_text="Brief descriptive blurb", max_length=512)), - ( - "FRET_partner", - models.ManyToManyField(blank=True, through="proteins.FRETpair", to="proteins.Protein"), - ), - ( - "created_by", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="protein_author", - to=settings.AUTH_USER_MODEL, - ), - ), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('status', model_utils.fields.StatusField(choices=[('pending', 'pending'), ('approved', 'approved'), ('hidden', 'hidden')], default='pending', max_length=100, no_check_for_status=True, verbose_name='status')), + ('status_changed', model_utils.fields.MonitorField(default=django.utils.timezone.now, monitor='status', verbose_name='status changed')), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('name', models.CharField(db_index=True, help_text='Name of the fluorescent protein', max_length=128)), + ('slug', models.SlugField(help_text='URL slug for the protein', max_length=64, unique=True)), + ('base_name', models.CharField(max_length=128)), + ('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=200), blank=True, null=True, size=None)), + ('chromophore', models.CharField(blank=True, max_length=5, null=True)), + ('seq', models.CharField(blank=True, help_text='Amino acid sequence (IPG ID is preferred)', max_length=1024, null=True, unique=True, validators=[proteins.validators.protein_sequence_validator], verbose_name='Sequence')), + ('pdb', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=4), blank=True, null=True, size=None, verbose_name='Protein DataBank ID')), + ('genbank', models.CharField(blank=True, help_text='NCBI Genbank Accession', max_length=12, null=True, unique=True, verbose_name='Genbank Accession')), + ('uniprot', models.CharField(blank=True, max_length=10, null=True, unique=True, validators=[django.core.validators.RegexValidator('[OPQ][0-9][A-Z0-9]{3}[0-9]|[A-NR-Z][0-9]([A-Z][A-Z0-9]{2}[0-9]){1,2}', 'Not a valid UniProt Accession')], verbose_name='UniProtKB Accession')), + ('ipg_id', models.CharField(blank=True, help_text='Identical Protein Group ID at Pubmed', max_length=12, null=True, unique=True, verbose_name='IPG ID')), + ('mw', models.FloatField(blank=True, help_text='Molecular Weight', null=True)), + ('agg', models.CharField(blank=True, choices=[('m', 'Monomer'), ('d', 'Dimer'), ('td', 'Tandem dimer'), ('wd', 'Weak dimer'), ('t', 'Tetramer')], help_text='Oligomerization tendency', max_length=2)), + ('switch_type', models.CharField(blank=True, choices=[('b', 'Basic'), ('pa', 'Photoactivatable'), ('ps', 'Photoswitchable'), ('pc', 'Photoconvertible'), ('t', 'Timer'), ('o', 'Multistate')], help_text='Photoswitching type (basic if none)', max_length=2, verbose_name='Type')), + ('blurb', models.CharField(blank=True, help_text='Brief descriptive blurb', max_length=512)), + ('FRET_partner', models.ManyToManyField(blank=True, through='proteins.FRETpair', to='proteins.Protein')), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='protein_author', to=settings.AUTH_USER_MODEL)), ], options={ - "ordering": ["name"], + 'ordering': ['name'], }, ), migrations.CreateModel( - name="ProteinCollection", + name='ProteinCollection', fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), - ), - ("name", models.CharField(max_length=100)), - ("description", models.CharField(blank=True, max_length=512)), - ( - "private", - models.BooleanField( - default=False, - help_text="Private collections can not be seen by or shared with other users", - verbose_name="Private Collection", - ), - ), - ( - "owner", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="collections", - to=settings.AUTH_USER_MODEL, - verbose_name="Protein Collection", - ), - ), - ("proteins", models.ManyToManyField(related_name="collection_memberships", to="proteins.Protein")), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('name', models.CharField(max_length=100)), + ('description', models.CharField(blank=True, max_length=512)), + ('private', models.BooleanField(default=False, help_text='Private collections can not be seen by or shared with other users', verbose_name='Private Collection')), + ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='collections', to=settings.AUTH_USER_MODEL, verbose_name='Protein Collection')), + ('proteins', models.ManyToManyField(related_name='collection_memberships', to='proteins.Protein')), ], ), migrations.CreateModel( - name="State", + name='State', fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), - ), - ("name", models.CharField(default="default", max_length=64)), - ("slug", models.SlugField(help_text="Unique slug for the state", max_length=128, unique=True)), - ( - "is_dark", - models.BooleanField( - default=False, help_text="This state does not fluorescence", verbose_name="Dark State" - ), - ), - ( - "ex_max", - models.PositiveSmallIntegerField( - blank=True, - db_index=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(300), - django.core.validators.MaxValueValidator(900), - ], - ), - ), - ( - "em_max", - models.PositiveSmallIntegerField( - blank=True, - db_index=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(300), - django.core.validators.MaxValueValidator(1000), - ], - ), - ), - ( - "ex_spectra", - proteins.fields.SpectrumField( - blank=True, help_text="List of [[wavelength, value],...] pairs", null=True - ), - ), - ( - "em_spectra", - proteins.fields.SpectrumField( - blank=True, help_text="List of [[wavelength, value],...] pairs", null=True - ), - ), - ( - "ext_coeff", - models.IntegerField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(300000), - ], - verbose_name="Extinction Coefficient", - ), - ), - ( - "qy", - models.FloatField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(1), - ], - verbose_name="Quantum Yield", - ), - ), - ("brightness", models.FloatField(blank=True, editable=False, null=True)), - ( - "pka", - models.FloatField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(2), - django.core.validators.MaxValueValidator(12), - ], - verbose_name="pKa", - ), - ), - ( - "maturation", - models.FloatField( - blank=True, - help_text="Maturation time (min)", - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(1600), - ], - ), - ), - ( - "lifetime", - models.FloatField( - blank=True, - help_text="Lifetime (ns)", - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(20), - ], - ), - ), - ( - "created_by", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="state_author", - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "protein", - models.ForeignKey( - help_text="The protein to which this state belongs", - on_delete=django.db.models.deletion.CASCADE, - related_name="states", - to="proteins.Protein", - ), - ), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('name', models.CharField(default='default', max_length=64)), + ('slug', models.SlugField(help_text='Unique slug for the state', max_length=128, unique=True)), + ('is_dark', models.BooleanField(default=False, help_text='This state does not fluorescence', verbose_name='Dark State')), + ('ex_max', models.PositiveSmallIntegerField(blank=True, db_index=True, null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(900)])), + ('em_max', models.PositiveSmallIntegerField(blank=True, db_index=True, null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1000)])), + ('ex_spectra', proteins.fields.SpectrumField(blank=True, help_text='List of [[wavelength, value],...] pairs', null=True)), + ('em_spectra', proteins.fields.SpectrumField(blank=True, help_text='List of [[wavelength, value],...] pairs', null=True)), + ('ext_coeff', models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(300000)], verbose_name='Extinction Coefficient')), + ('qy', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='Quantum Yield')), + ('brightness', models.FloatField(blank=True, editable=False, null=True)), + ('pka', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(2), django.core.validators.MaxValueValidator(12)], verbose_name='pKa')), + ('maturation', models.FloatField(blank=True, help_text='Maturation time (min)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1600)])), + ('lifetime', models.FloatField(blank=True, help_text='Lifetime (ns)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(20)])), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='state_author', to=settings.AUTH_USER_MODEL)), + ('protein', models.ForeignKey(help_text='The protein to which this state belongs', on_delete=django.db.models.deletion.CASCADE, related_name='states', to='proteins.Protein')), ], options={ - "verbose_name": "State", + 'verbose_name': 'State', }, ), migrations.CreateModel( - name="StateTransition", + name='StateTransition', fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), - ), - ( - "trans_wave", - models.PositiveSmallIntegerField( - blank=True, - help_text="Wavelength required", - null=True, - validators=[ - django.core.validators.MinValueValidator(300), - django.core.validators.MaxValueValidator(1000), - ], - verbose_name="Transition Wavelength", - ), - ), - ( - "from_state", - models.ForeignKey( - help_text="The initial state ", - on_delete=django.db.models.deletion.CASCADE, - related_name="transitions_from", - to="proteins.State", - verbose_name="From state", - ), - ), - ( - "protein", - models.ForeignKey( - help_text="The protein that demonstrates this transition", - on_delete=django.db.models.deletion.CASCADE, - related_name="transitions", - to="proteins.Protein", - verbose_name="Protein Transitioning", - ), - ), - ( - "to_state", - models.ForeignKey( - help_text="The state after transition", - on_delete=django.db.models.deletion.CASCADE, - related_name="transitions_to", - to="proteins.State", - verbose_name="To state", - ), - ), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('trans_wave', models.PositiveSmallIntegerField(blank=True, help_text='Wavelength required', null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1000)], verbose_name='Transition Wavelength')), + ('from_state', models.ForeignKey(help_text='The initial state ', on_delete=django.db.models.deletion.CASCADE, related_name='transitions_from', to='proteins.State', verbose_name='From state')), + ('protein', models.ForeignKey(help_text='The protein that demonstrates this transition', on_delete=django.db.models.deletion.CASCADE, related_name='transitions', to='proteins.Protein', verbose_name='Protein Transitioning')), + ('to_state', models.ForeignKey(help_text='The state after transition', on_delete=django.db.models.deletion.CASCADE, related_name='transitions_to', to='proteins.State', verbose_name='To state')), ], options={ - "abstract": False, + 'abstract': False, }, ), migrations.AddField( - model_name="state", - name="transitions", - field=models.ManyToManyField( - blank=True, - related_name="transition_state", - through="proteins.StateTransition", - to="proteins.State", - verbose_name="State Transitions", - ), + model_name='state', + name='transitions', + field=models.ManyToManyField(blank=True, related_name='transition_state', through='proteins.StateTransition', to='proteins.State', verbose_name='State Transitions'), ), migrations.AddField( - model_name="state", - name="updated_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="state_modifier", - to=settings.AUTH_USER_MODEL, - ), + model_name='state', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='state_modifier', to=settings.AUTH_USER_MODEL), ), migrations.AddField( - model_name="protein", - name="default_state", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="default_for", - to="proteins.State", - ), + model_name='protein', + name='default_state', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_for', to='proteins.State'), ), migrations.AddField( - model_name="protein", - name="parent_organism", - field=models.ForeignKey( - blank=True, - help_text="Organism from which the protein was engineered", - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="proteins", - to="proteins.Organism", - verbose_name="Parental organism", - ), + model_name='protein', + name='parent_organism', + field=models.ForeignKey(blank=True, help_text='Organism from which the protein was engineered', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='proteins', to='proteins.Organism', verbose_name='Parental organism'), ), migrations.AddField( - model_name="protein", - name="primary_reference", - field=models.ForeignKey( - blank=True, - help_text="Preferably the publication that introduced the protein", - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="primary_proteins", - to="references.Reference", - verbose_name="Primary Reference", - ), + model_name='protein', + name='primary_reference', + field=models.ForeignKey(blank=True, help_text='Preferably the publication that introduced the protein', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_proteins', to='references.Reference', verbose_name='Primary Reference'), ), migrations.AddField( - model_name="protein", - name="references", - field=models.ManyToManyField(blank=True, related_name="proteins", to="references.Reference"), + model_name='protein', + name='references', + field=models.ManyToManyField(blank=True, related_name='proteins', to='references.Reference'), ), migrations.AddField( - model_name="protein", - name="updated_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="protein_modifier", - to=settings.AUTH_USER_MODEL, - ), + model_name='protein', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='protein_modifier', to=settings.AUTH_USER_MODEL), ), migrations.AddField( - model_name="mutation", - name="parent", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="proteins", - to="proteins.Protein", - verbose_name="Parent Protein", - ), + model_name='mutation', + name='parent', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='proteins', to='proteins.Protein', verbose_name='Parent Protein'), ), migrations.AddField( - model_name="fretpair", - name="acceptor", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="FK_FRETacceptor_protein", - to="proteins.Protein", - verbose_name="acceptor", - ), + model_name='fretpair', + name='acceptor', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='FK_FRETacceptor_protein', to='proteins.Protein', verbose_name='acceptor'), ), migrations.AddField( - model_name="fretpair", - name="created_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="fretpair_author", - to=settings.AUTH_USER_MODEL, - ), + model_name='fretpair', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='fretpair_author', to=settings.AUTH_USER_MODEL), ), migrations.AddField( - model_name="fretpair", - name="donor", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="FK_FRETdonor_protein", - to="proteins.Protein", - verbose_name="donor", - ), + model_name='fretpair', + name='donor', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='FK_FRETdonor_protein', to='proteins.Protein', verbose_name='donor'), ), migrations.AddField( - model_name="fretpair", - name="pair_references", - field=models.ManyToManyField(blank=True, related_name="FK_FRETpair_reference", to="references.Reference"), + model_name='fretpair', + name='pair_references', + field=models.ManyToManyField(blank=True, related_name='FK_FRETpair_reference', to='references.Reference'), ), migrations.AddField( - model_name="fretpair", - name="updated_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="fretpair_modifier", - to=settings.AUTH_USER_MODEL, - ), + model_name='fretpair', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='fretpair_modifier', to=settings.AUTH_USER_MODEL), ), migrations.AddField( - model_name="bleachmeasurement", - name="state", - field=models.ForeignKey( - help_text="The state on which this measurement was made", - on_delete=django.db.models.deletion.CASCADE, - related_name="bleach_measurements", - to="proteins.State", - verbose_name="Protein (state)", - ), + model_name='bleachmeasurement', + name='state', + field=models.ForeignKey(help_text='The state on which this measurement was made', on_delete=django.db.models.deletion.CASCADE, related_name='bleach_measurements', to='proteins.State', verbose_name='Protein (state)'), ), migrations.AddField( - model_name="bleachmeasurement", - name="updated_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="bleachmeasurement_modifier", - to=settings.AUTH_USER_MODEL, - ), + model_name='bleachmeasurement', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='bleachmeasurement_modifier', to=settings.AUTH_USER_MODEL), ), migrations.AlterUniqueTogether( - name="state", - unique_together={("protein", "ex_max", "em_max", "ext_coeff", "qy")}, + name='state', + unique_together=set([('protein', 'ex_max', 'em_max', 'ext_coeff', 'qy')]), ), migrations.AlterUniqueTogether( - name="proteincollection", - unique_together={("owner", "name")}, + name='proteincollection', + unique_together=set([('owner', 'name')]), ), ] diff --git a/backend/proteins/migrations/0002_auto_20180312_0156.py b/backend/proteins/migrations/0002_auto_20180312_0156.py index d2c657c43..05fc23d29 100644 --- a/backend/proteins/migrations/0002_auto_20180312_0156.py +++ b/backend/proteins/migrations/0002_auto_20180312_0156.py @@ -1,59 +1,37 @@ +# -*- coding: utf-8 -*- # Generated by Django 1.11.9 on 2018-03-12 01:56 +from __future__ import unicode_literals import django.core.validators from django.db import migrations, models - import proteins.fields class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0001_initial"), + ('proteins', '0001_initial'), ] operations = [ migrations.AddField( - model_name="state", - name="twop_ex_max", - field=models.PositiveSmallIntegerField( - blank=True, - db_index=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(700), - django.core.validators.MaxValueValidator(1600), - ], - verbose_name="Peak 2P excitation", - ), + model_name='state', + name='twop_ex_max', + field=models.PositiveSmallIntegerField(blank=True, db_index=True, null=True, validators=[django.core.validators.MinValueValidator(700), django.core.validators.MaxValueValidator(1600)], verbose_name='Peak 2P excitation'), ), migrations.AddField( - model_name="state", - name="twop_ex_spectra", - field=proteins.fields.SpectrumField( - blank=True, help_text="List of [[wavelength, value],...] pairs", null=True - ), + model_name='state', + name='twop_ex_spectra', + field=proteins.fields.SpectrumField(blank=True, help_text='List of [[wavelength, value],...] pairs', null=True), ), migrations.AddField( - model_name="state", - name="twop_peakGM", - field=models.FloatField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(200), - ], - verbose_name="Peak 2P cross-section of S0->S1 (GM)", - ), + model_name='state', + name='twop_peakGM', + field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(200)], verbose_name='Peak 2P cross-section of S0->S1 (GM)'), ), migrations.AddField( - model_name="state", - name="twop_qy", - field=models.FloatField( - blank=True, - null=True, - validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], - verbose_name="2P Quantum Yield", - ), + model_name='state', + name='twop_qy', + field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='2P Quantum Yield'), ), ] diff --git a/backend/proteins/migrations/0003_protein_oser.py b/backend/proteins/migrations/0003_protein_oser.py index 9bf837ff8..d0c665dd2 100644 --- a/backend/proteins/migrations/0003_protein_oser.py +++ b/backend/proteins/migrations/0003_protein_oser.py @@ -1,17 +1,20 @@ +# -*- coding: utf-8 -*- # Generated by Django 1.11.9 on 2018-03-12 22:26 +from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0002_auto_20180312_0156"), + ('proteins', '0002_auto_20180312_0156'), ] operations = [ migrations.AddField( - model_name="protein", - name="oser", - field=models.FloatField(blank=True, help_text="OSER score", null=True), + model_name='protein', + name='oser', + field=models.FloatField(blank=True, help_text='OSER score', null=True), ), ] diff --git a/backend/proteins/migrations/0004_auto_20180315_1323.py b/backend/proteins/migrations/0004_auto_20180315_1323.py index e51d59851..d81658d8d 100644 --- a/backend/proteins/migrations/0004_auto_20180315_1323.py +++ b/backend/proteins/migrations/0004_auto_20180315_1323.py @@ -1,16 +1,19 @@ +# -*- coding: utf-8 -*- # Generated by Django 1.11.9 on 2018-03-15 13:23 +from __future__ import unicode_literals from django.db import migrations class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0003_protein_oser"), + ('proteins', '0003_protein_oser'), ] operations = [ migrations.AlterUniqueTogether( - name="fretpair", - unique_together={("donor", "acceptor")}, + name='fretpair', + unique_together=set([('donor', 'acceptor')]), ), ] diff --git a/backend/proteins/migrations/0005_auto_20180328_1614.py b/backend/proteins/migrations/0005_auto_20180328_1614.py index cacc46453..f252bcc03 100644 --- a/backend/proteins/migrations/0005_auto_20180328_1614.py +++ b/backend/proteins/migrations/0005_auto_20180328_1614.py @@ -1,124 +1,67 @@ +# -*- coding: utf-8 -*- # Generated by Django 1.11.11 on 2018-03-28 16:14 +from __future__ import unicode_literals -import django.db.models.deletion from django.conf import settings from django.db import migrations, models +import django.db.models.deletion class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0004_auto_20180315_1323"), + ('proteins', '0004_auto_20180315_1323'), ] operations = [ migrations.AlterField( - model_name="bleachmeasurement", - name="created_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="bleachmeasurement_author", - to=settings.AUTH_USER_MODEL, - ), + model_name='bleachmeasurement', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bleachmeasurement_author', to=settings.AUTH_USER_MODEL), ), migrations.AlterField( - model_name="bleachmeasurement", - name="updated_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="bleachmeasurement_modifier", - to=settings.AUTH_USER_MODEL, - ), + model_name='bleachmeasurement', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bleachmeasurement_modifier', to=settings.AUTH_USER_MODEL), ), migrations.AlterField( - model_name="fretpair", - name="created_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="fretpair_author", - to=settings.AUTH_USER_MODEL, - ), + model_name='fretpair', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='fretpair_author', to=settings.AUTH_USER_MODEL), ), migrations.AlterField( - model_name="fretpair", - name="updated_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="fretpair_modifier", - to=settings.AUTH_USER_MODEL, - ), + model_name='fretpair', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='fretpair_modifier', to=settings.AUTH_USER_MODEL), ), migrations.AlterField( - model_name="organism", - name="created_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="organism_author", - to=settings.AUTH_USER_MODEL, - ), + model_name='organism', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='organism_author', to=settings.AUTH_USER_MODEL), ), migrations.AlterField( - model_name="organism", - name="updated_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="organism_modifier", - to=settings.AUTH_USER_MODEL, - ), + model_name='organism', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='organism_modifier', to=settings.AUTH_USER_MODEL), ), migrations.AlterField( - model_name="protein", - name="created_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="protein_author", - to=settings.AUTH_USER_MODEL, - ), + model_name='protein', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='protein_author', to=settings.AUTH_USER_MODEL), ), migrations.AlterField( - model_name="protein", - name="updated_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="protein_modifier", - to=settings.AUTH_USER_MODEL, - ), + model_name='protein', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='protein_modifier', to=settings.AUTH_USER_MODEL), ), migrations.AlterField( - model_name="state", - name="created_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="state_author", - to=settings.AUTH_USER_MODEL, - ), + model_name='state', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='state_author', to=settings.AUTH_USER_MODEL), ), migrations.AlterField( - model_name="state", - name="updated_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="state_modifier", - to=settings.AUTH_USER_MODEL, - ), + model_name='state', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='state_modifier', to=settings.AUTH_USER_MODEL), ), ] diff --git a/backend/proteins/migrations/0005_auto_20180328_1614_squashed_0010_auto_20180501_1547.py b/backend/proteins/migrations/0005_auto_20180328_1614_squashed_0010_auto_20180501_1547.py index 6ee936cc7..14b0c98d9 100644 --- a/backend/proteins/migrations/0005_auto_20180328_1614_squashed_0010_auto_20180501_1547.py +++ b/backend/proteins/migrations/0005_auto_20180328_1614_squashed_0010_auto_20180501_1547.py @@ -1,40 +1,41 @@ +# -*- coding: utf-8 -*- # Generated by Django 1.11.11 on 2018-05-08 13:24 +from __future__ import unicode_literals +from django.conf import settings import django.contrib.postgres.fields import django.core.validators +from django.db import migrations, models import django.db.models.deletion import django.utils.timezone import model_utils.fields -from django.conf import settings -from django.db import migrations, models - import proteins.models.spectrum def move_spectra(apps, schema_editor): States = apps.get_model("proteins", "State") Spectrum = apps.get_model("proteins", "Spectrum") - it = "p" + it = 'p' for state in States.objects.all(): if state.ex_spectra: Spectrum.objects.create( data=state.ex_spectra.data, owner_state=state, - subtype="ex", + subtype='ex', category=it, ) if state.em_spectra: Spectrum.objects.create( data=state.em_spectra.data, owner_state=state, - subtype="em", + subtype='em', category=it, ) if state.twop_ex_spectra: spectrum = Spectrum( data=state.twop_ex_spectra.data, owner_state=state, - subtype="2p", + subtype='2p', category=it, ) spectrum.save() @@ -44,434 +45,142 @@ def undo_move_spectra(apps, schema_editor): Spectrum = apps.get_model("proteins", "Spectrum") for spectrum in Spectrum.objects.all(): if spectrum.subtype and spectrum.owner_state: - if spectrum.subtype == "ex": + if spectrum.subtype == 'ex': spectrum.owner_state.ex_spectra = spectrum.data - elif spectrum.subtype == "em": + elif spectrum.subtype == 'em': spectrum.owner_state.em_spectra = spectrum.data - elif spectrum.subtype == "2p": + elif spectrum.subtype == '2p': spectrum.owner_state.twop_ex_spectra = spectrum.data class Migration(migrations.Migration): - replaces = [ - ("proteins", "0005_auto_20180328_1614"), - ("proteins", "0006_auto_20180401_2009"), - ("proteins", "0007_auto_20180403_0140"), - ("proteins", "0008_auto_20180430_0308"), - ("proteins", "0009_auto_20180430_0335"), - ("proteins", "0010_auto_20180501_1547"), - ] + + replaces = [('proteins', '0005_auto_20180328_1614'), ('proteins', '0006_auto_20180401_2009'), ('proteins', '0007_auto_20180403_0140'), ('proteins', '0008_auto_20180430_0308'), ('proteins', '0009_auto_20180430_0335'), ('proteins', '0010_auto_20180501_1547')] dependencies = [ - ("proteins", "0004_auto_20180315_1323"), + ('proteins', '0004_auto_20180315_1323'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.AlterField( - model_name="bleachmeasurement", - name="created_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="bleachmeasurement_author", - to=settings.AUTH_USER_MODEL, - ), + model_name='bleachmeasurement', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bleachmeasurement_author', to=settings.AUTH_USER_MODEL), ), migrations.AlterField( - model_name="bleachmeasurement", - name="updated_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="bleachmeasurement_modifier", - to=settings.AUTH_USER_MODEL, - ), + model_name='bleachmeasurement', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bleachmeasurement_modifier', to=settings.AUTH_USER_MODEL), ), migrations.AlterField( - model_name="fretpair", - name="created_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="fretpair_author", - to=settings.AUTH_USER_MODEL, - ), + model_name='fretpair', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='fretpair_author', to=settings.AUTH_USER_MODEL), ), migrations.AlterField( - model_name="fretpair", - name="updated_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="fretpair_modifier", - to=settings.AUTH_USER_MODEL, - ), + model_name='fretpair', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='fretpair_modifier', to=settings.AUTH_USER_MODEL), ), migrations.AlterField( - model_name="organism", - name="created_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="organism_author", - to=settings.AUTH_USER_MODEL, - ), + model_name='organism', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='organism_author', to=settings.AUTH_USER_MODEL), ), migrations.AlterField( - model_name="organism", - name="updated_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="organism_modifier", - to=settings.AUTH_USER_MODEL, - ), + model_name='organism', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='organism_modifier', to=settings.AUTH_USER_MODEL), ), migrations.AlterField( - model_name="protein", - name="created_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="protein_author", - to=settings.AUTH_USER_MODEL, - ), + model_name='protein', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='protein_author', to=settings.AUTH_USER_MODEL), ), migrations.AlterField( - model_name="protein", - name="updated_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="protein_modifier", - to=settings.AUTH_USER_MODEL, - ), + model_name='protein', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='protein_modifier', to=settings.AUTH_USER_MODEL), ), migrations.AlterField( - model_name="state", - name="created_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="state_author", - to=settings.AUTH_USER_MODEL, - ), + model_name='state', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='state_author', to=settings.AUTH_USER_MODEL), ), migrations.AlterField( - model_name="state", - name="updated_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="state_modifier", - to=settings.AUTH_USER_MODEL, - ), + model_name='state', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='state_modifier', to=settings.AUTH_USER_MODEL), ), migrations.CreateModel( - name="Camera", + name='Camera', fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ("slug", models.SlugField(help_text="Unique slug for the %(class)", max_length=128, unique=True)), - ("name", models.CharField(max_length=100)), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('slug', models.SlugField(help_text='Unique slug for the %(class)', max_length=128, unique=True)), + ('name', models.CharField(max_length=100)), ], ), migrations.CreateModel( - name="Dye", + name='Dye', fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), - ), - ( - "ex_max", - models.PositiveSmallIntegerField( - blank=True, - db_index=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(300), - django.core.validators.MaxValueValidator(900), - ], - ), - ), - ( - "em_max", - models.PositiveSmallIntegerField( - blank=True, - db_index=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(300), - django.core.validators.MaxValueValidator(1000), - ], - ), - ), - ( - "twop_ex_max", - models.PositiveSmallIntegerField( - blank=True, - db_index=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(700), - django.core.validators.MaxValueValidator(1600), - ], - verbose_name="Peak 2P excitation", - ), - ), - ( - "ext_coeff", - models.IntegerField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(300000), - ], - verbose_name="Extinction Coefficient", - ), - ), - ( - "twop_peakGM", - models.FloatField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(200), - ], - verbose_name="Peak 2P cross-section of S0->S1 (GM)", - ), - ), - ( - "qy", - models.FloatField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(1), - ], - verbose_name="Quantum Yield", - ), - ), - ( - "twop_qy", - models.FloatField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(1), - ], - verbose_name="2P Quantum Yield", - ), - ), - ("brightness", models.FloatField(blank=True, editable=False, null=True)), - ( - "pka", - models.FloatField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(2), - django.core.validators.MaxValueValidator(12), - ], - verbose_name="pKa", - ), - ), - ( - "lifetime", - models.FloatField( - blank=True, - help_text="Lifetime (ns)", - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(20), - ], - ), - ), - ("slug", models.SlugField(help_text="Unique slug for the %(class)", max_length=128, unique=True)), - ("name", models.CharField(max_length=100)), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('ex_max', models.PositiveSmallIntegerField(blank=True, db_index=True, null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(900)])), + ('em_max', models.PositiveSmallIntegerField(blank=True, db_index=True, null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1000)])), + ('twop_ex_max', models.PositiveSmallIntegerField(blank=True, db_index=True, null=True, validators=[django.core.validators.MinValueValidator(700), django.core.validators.MaxValueValidator(1600)], verbose_name='Peak 2P excitation')), + ('ext_coeff', models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(300000)], verbose_name='Extinction Coefficient')), + ('twop_peakGM', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(200)], verbose_name='Peak 2P cross-section of S0->S1 (GM)')), + ('qy', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='Quantum Yield')), + ('twop_qy', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='2P Quantum Yield')), + ('brightness', models.FloatField(blank=True, editable=False, null=True)), + ('pka', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(2), django.core.validators.MaxValueValidator(12)], verbose_name='pKa')), + ('lifetime', models.FloatField(blank=True, help_text='Lifetime (ns)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(20)])), + ('slug', models.SlugField(help_text='Unique slug for the %(class)', max_length=128, unique=True)), + ('name', models.CharField(max_length=100)), ], options={ - "abstract": False, + 'abstract': False, }, ), migrations.CreateModel( - name="Filter", + name='Filter', fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ("slug", models.SlugField(help_text="Unique slug for the %(class)", max_length=128, unique=True)), - ("name", models.CharField(max_length=100)), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('slug', models.SlugField(help_text='Unique slug for the %(class)', max_length=128, unique=True)), + ('name', models.CharField(max_length=100)), ], ), migrations.CreateModel( - name="Light", + name='Light', fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ("slug", models.SlugField(help_text="Unique slug for the %(class)", max_length=128, unique=True)), - ("name", models.CharField(max_length=100)), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('slug', models.SlugField(help_text='Unique slug for the %(class)', max_length=128, unique=True)), + ('name', models.CharField(max_length=100)), ], ), migrations.CreateModel( - name="Spectrum", + name='Spectrum', fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), - ), - ( - "data", - proteins.models.spectrum.SpectrumData( - base_field=django.contrib.postgres.fields.ArrayField( - base_field=models.FloatField(max_length=10), size=2 - ), - size=None, - ), - ), - ( - "category", - models.CharField( - blank=True, - choices=[ - ("d", "Dye"), - ("p", "Protein"), - ("l", "Light Source"), - ("f", "Filter"), - ("c", "Camera"), - ], - db_index=True, - max_length=1, - verbose_name="Item Type", - ), - ), - ( - "subtype", - models.CharField( - choices=[ - ("ex", "excitation"), - ("ab", "absorption"), - ("em", "emission"), - ("2p", "two photon absorption"), - ("bx", "bandpass (excitation)"), - ("bm", "bandpass (emission)"), - ("sp", "shortpass"), - ("lp", "longpass"), - ("bs", "beamsplitter"), - ("qe", "Quantum Efficiency"), - ("pd", "Power Distribution"), - ], - db_index=True, - max_length=2, - verbose_name="Spectra Subtype", - ), - ), - ("ph", models.FloatField(blank=True, null=True, verbose_name="pH")), - ("solvent", models.CharField(blank=True, max_length=128)), - ( - "created_by", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="spectrum_author", - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "owner_camera", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="spectra", - to="proteins.Camera", - ), - ), - ( - "owner_dye", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="spectra", - to="proteins.Dye", - ), - ), - ( - "owner_filter", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="spectra", - to="proteins.Filter", - ), - ), - ( - "owner_light", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="spectra", - to="proteins.Light", - ), - ), - ( - "owner_state", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="spectra", - to="proteins.State", - ), - ), - ( - "updated_by", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="spectrum_modifier", - to=settings.AUTH_USER_MODEL, - ), - ), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('data', proteins.models.spectrum.SpectrumData(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(max_length=10), size=2), size=None)), + ('category', models.CharField(blank=True, choices=[('d', 'Dye'), ('p', 'Protein'), ('l', 'Light Source'), ('f', 'Filter'), ('c', 'Camera')], db_index=True, max_length=1, verbose_name='Item Type')), + ('subtype', models.CharField(choices=[('ex', 'excitation'), ('ab', 'absorption'), ('em', 'emission'), ('2p', 'two photon absorption'), ('bx', 'bandpass (excitation)'), ('bm', 'bandpass (emission)'), ('sp', 'shortpass'), ('lp', 'longpass'), ('bs', 'beamsplitter'), ('qe', 'Quantum Efficiency'), ('pd', 'Power Distribution')], db_index=True, max_length=2, verbose_name='Spectra Subtype')), + ('ph', models.FloatField(blank=True, null=True, verbose_name='pH')), + ('solvent', models.CharField(blank=True, max_length=128)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='spectrum_author', to=settings.AUTH_USER_MODEL)), + ('owner_camera', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectra', to='proteins.Camera')), + ('owner_dye', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectra', to='proteins.Dye')), + ('owner_filter', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectra', to='proteins.Filter')), + ('owner_light', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectra', to='proteins.Light')), + ('owner_state', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectra', to='proteins.State')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='spectrum_modifier', to=settings.AUTH_USER_MODEL)), ], options={ - "abstract": False, + 'abstract': False, }, ), migrations.RunPython( @@ -479,171 +188,100 @@ class Migration(migrations.Migration): reverse_code=undo_move_spectra, ), migrations.RemoveField( - model_name="state", - name="em_spectra", + model_name='state', + name='em_spectra', ), migrations.RemoveField( - model_name="state", - name="ex_spectra", + model_name='state', + name='ex_spectra', ), migrations.RemoveField( - model_name="state", - name="twop_ex_spectra", + model_name='state', + name='twop_ex_spectra', ), migrations.AddField( - model_name="filter", - name="aoi", - field=models.PositiveSmallIntegerField( - blank=True, - null=True, - validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(90)], - ), + model_name='filter', + name='aoi', + field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(90)]), ), migrations.AddField( - model_name="filter", - name="bandcenter", - field=models.PositiveSmallIntegerField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(200), - django.core.validators.MaxValueValidator(1600), - ], - ), + model_name='filter', + name='bandcenter', + field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(200), django.core.validators.MaxValueValidator(1600)]), ), migrations.AddField( - model_name="filter", - name="bandwidth", - field=models.PositiveSmallIntegerField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(300), - django.core.validators.MaxValueValidator(900), - ], - ), + model_name='filter', + name='bandwidth', + field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(900)]), ), migrations.AddField( - model_name="filter", - name="edge", - field=models.FloatField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(200), - ], - ), + model_name='filter', + name='edge', + field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(200)]), ), migrations.AddField( - model_name="filter", - name="tavg", - field=models.FloatField( - blank=True, - null=True, - validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], - ), + model_name='filter', + name='tavg', + field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), ), migrations.AddField( - model_name="camera", - name="manufacturer", + model_name='camera', + name='manufacturer', field=models.CharField(blank=True, max_length=128), ), migrations.AddField( - model_name="filter", - name="manufacturer", + model_name='filter', + name='manufacturer', field=models.CharField(blank=True, max_length=128), ), migrations.AddField( - model_name="light", - name="manufacturer", + model_name='light', + name='manufacturer', field=models.CharField(blank=True, max_length=128), ), migrations.AlterField( - model_name="spectrum", - name="category", - field=models.CharField( - choices=[("d", "Dye"), ("p", "Protein"), ("l", "Light Source"), ("f", "Filter"), ("c", "Camera")], - db_index=True, - max_length=1, - verbose_name="Item Type", - ), + model_name='spectrum', + name='category', + field=models.CharField(choices=[('d', 'Dye'), ('p', 'Protein'), ('l', 'Light Source'), ('f', 'Filter'), ('c', 'Camera')], db_index=True, max_length=1, verbose_name='Item Type'), ), migrations.AlterField( - model_name="spectrum", - name="subtype", - field=models.CharField( - blank=True, - choices=[ - ("ex", "excitation"), - ("ab", "absorption"), - ("em", "emission"), - ("2p", "two photon absorption"), - ("bx", "bandpass (excitation)"), - ("bm", "bandpass (emission)"), - ("sp", "shortpass"), - ("lp", "longpass"), - ("bs", "beamsplitter"), - ("qe", "Quantum Efficiency"), - ("pd", "Power Distribution"), - ], - db_index=True, - max_length=2, - verbose_name="Spectra Subtype", - ), + model_name='spectrum', + name='subtype', + field=models.CharField(blank=True, choices=[('ex', 'excitation'), ('ab', 'absorption'), ('em', 'emission'), ('2p', 'two photon absorption'), ('bx', 'bandpass (excitation)'), ('bm', 'bandpass (emission)'), ('sp', 'shortpass'), ('lp', 'longpass'), ('bs', 'beamsplitter'), ('qe', 'Quantum Efficiency'), ('pd', 'Power Distribution')], db_index=True, max_length=2, verbose_name='Spectra Subtype'), ), migrations.AlterField( - model_name="state", - name="slug", - field=models.SlugField(help_text="Unique slug for the %(class)", max_length=128, unique=True), + model_name='state', + name='slug', + field=models.SlugField(help_text='Unique slug for the %(class)', max_length=128, unique=True), ), migrations.AddField( - model_name="camera", - name="part", + model_name='camera', + name='part', field=models.CharField(blank=True, max_length=128), ), migrations.AddField( - model_name="dye", - name="manufacturer", + model_name='dye', + name='manufacturer', field=models.CharField(blank=True, max_length=128), ), migrations.AddField( - model_name="dye", - name="part", + model_name='dye', + name='part', field=models.CharField(blank=True, max_length=128), ), migrations.AddField( - model_name="filter", - name="part", + model_name='filter', + name='part', field=models.CharField(blank=True, max_length=128), ), migrations.AddField( - model_name="light", - name="part", + model_name='light', + name='part', field=models.CharField(blank=True, max_length=128), ), migrations.AlterField( - model_name="spectrum", - name="subtype", - field=models.CharField( - blank=True, - choices=[ - ("ex", "Excitation"), - ("ab", "Absorption"), - ("em", "Emission"), - ("2p", "Two Photon Absorption"), - ("bp", "Bandpass"), - ("bx", "Bandpass (Excitation)"), - ("bm", "Bandpass (Emission)"), - ("sp", "Shortpass"), - ("lp", "Longpass"), - ("bs", "Beamsplitter"), - ("qe", "Quantum Efficiency"), - ("pd", "Power Distribution"), - ], - db_index=True, - max_length=2, - verbose_name="Spectra Subtype", - ), + model_name='spectrum', + name='subtype', + field=models.CharField(blank=True, choices=[('ex', 'Excitation'), ('ab', 'Absorption'), ('em', 'Emission'), ('2p', 'Two Photon Absorption'), ('bp', 'Bandpass'), ('bx', 'Bandpass (Excitation)'), ('bm', 'Bandpass (Emission)'), ('sp', 'Shortpass'), ('lp', 'Longpass'), ('bs', 'Beamsplitter'), ('qe', 'Quantum Efficiency'), ('pd', 'Power Distribution')], db_index=True, max_length=2, verbose_name='Spectra Subtype'), ), ] diff --git a/backend/proteins/migrations/0006_auto_20180401_2009.py b/backend/proteins/migrations/0006_auto_20180401_2009.py index 10ddb38b9..7003c9e81 100644 --- a/backend/proteins/migrations/0006_auto_20180401_2009.py +++ b/backend/proteins/migrations/0006_auto_20180401_2009.py @@ -1,40 +1,42 @@ +# -*- coding: utf-8 -*- # Generated by Django 1.11.9 on 2018-04-01 20:09 +from __future__ import unicode_literals +from django.conf import settings import django.contrib.postgres.fields import django.core.validators +from django.db import migrations, models import django.db.models.deletion import django.utils.timezone import model_utils.fields -from django.conf import settings -from django.db import migrations, models - import proteins.models.spectrum def move_spectra(apps, schema_editor): States = apps.get_model("proteins", "State") Spectrum = apps.get_model("proteins", "Spectrum") - it = "p" + it = 'p' for state in States.objects.all(): + prot_name = state.protein.name if state.ex_spectrum: Spectrum.objects.create( data=state.ex_spectrum.data, owner_state=state, - subtype="ex", + subtype='ex', category=it, ) if state.em_spectrum: Spectrum.objects.create( data=state.em_spectrum.data, owner_state=state, - subtype="em", + subtype='em', category=it, ) if state.twop_ex_spectrum: spectrum = Spectrum( data=state.twop_ex_spectrum.data, owner_state=state, - subtype="2p", + subtype='2p', category=it, ) spectrum.save() @@ -44,315 +46,90 @@ def undo_move_spectra(apps, schema_editor): Spectrum = apps.get_model("proteins", "Spectrum") for spectrum in Spectrum.objects.all(): if spectrum.subtype and spectrum.owner_state: - if spectrum.subtype == "ex": + if spectrum.subtype == 'ex': spectrum.owner_state.ex_spectra = spectrum.data - elif spectrum.subtype == "em": + elif spectrum.subtype == 'em': spectrum.owner_state.em_spectra = spectrum.data - elif spectrum.subtype == "2p": + elif spectrum.subtype == '2p': spectrum.owner_state.twop_ex_spectra = spectrum.data class Migration(migrations.Migration): + dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("proteins", "0005_auto_20180328_1614"), + ('proteins', '0005_auto_20180328_1614'), ] operations = [ migrations.CreateModel( - name="Camera", + name='Camera', fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ("slug", models.SlugField(help_text="Unique slug for the %(class)", max_length=128, unique=True)), - ("name", models.CharField(max_length=100)), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('slug', models.SlugField(help_text='Unique slug for the %(class)', max_length=128, unique=True)), + ('name', models.CharField(max_length=100)) ], ), migrations.CreateModel( - name="Dye", + name='Dye', fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), - ), - ( - "ex_max", - models.PositiveSmallIntegerField( - blank=True, - db_index=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(300), - django.core.validators.MaxValueValidator(900), - ], - ), - ), - ( - "em_max", - models.PositiveSmallIntegerField( - blank=True, - db_index=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(300), - django.core.validators.MaxValueValidator(1000), - ], - ), - ), - ( - "twop_ex_max", - models.PositiveSmallIntegerField( - blank=True, - db_index=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(700), - django.core.validators.MaxValueValidator(1600), - ], - verbose_name="Peak 2P excitation", - ), - ), - ( - "ext_coeff", - models.IntegerField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(300000), - ], - verbose_name="Extinction Coefficient", - ), - ), - ( - "twop_peakGM", - models.FloatField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(200), - ], - verbose_name="Peak 2P cross-section of S0->S1 (GM)", - ), - ), - ( - "qy", - models.FloatField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(1), - ], - verbose_name="Quantum Yield", - ), - ), - ( - "twop_qy", - models.FloatField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(1), - ], - verbose_name="2P Quantum Yield", - ), - ), - ("brightness", models.FloatField(blank=True, editable=False, null=True)), - ( - "pka", - models.FloatField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(2), - django.core.validators.MaxValueValidator(12), - ], - verbose_name="pKa", - ), - ), - ( - "lifetime", - models.FloatField( - blank=True, - help_text="Lifetime (ns)", - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(20), - ], - ), - ), - ("slug", models.SlugField(help_text="Unique slug for the %(class)", max_length=128, unique=True)), - ("name", models.CharField(max_length=100)), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('ex_max', models.PositiveSmallIntegerField(blank=True, db_index=True, null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(900)])), + ('em_max', models.PositiveSmallIntegerField(blank=True, db_index=True, null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1000)])), + ('twop_ex_max', models.PositiveSmallIntegerField(blank=True, db_index=True, null=True, validators=[django.core.validators.MinValueValidator(700), django.core.validators.MaxValueValidator(1600)], verbose_name='Peak 2P excitation')), + ('ext_coeff', models.IntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(300000)], verbose_name='Extinction Coefficient')), + ('twop_peakGM', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(200)], verbose_name='Peak 2P cross-section of S0->S1 (GM)')), + ('qy', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='Quantum Yield')), + ('twop_qy', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='2P Quantum Yield')), + ('brightness', models.FloatField(blank=True, editable=False, null=True)), + ('pka', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(2), django.core.validators.MaxValueValidator(12)], verbose_name='pKa')), + ('lifetime', models.FloatField(blank=True, help_text='Lifetime (ns)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(20)])), + ('slug', models.SlugField(help_text='Unique slug for the %(class)', max_length=128, unique=True)), + ('name', models.CharField(max_length=100)) ], options={ - "abstract": False, + 'abstract': False, }, ), migrations.CreateModel( - name="Filter", + name='Filter', fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ("slug", models.SlugField(help_text="Unique slug for the %(class)", max_length=128, unique=True)), - ("name", models.CharField(max_length=100)), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('slug', models.SlugField(help_text='Unique slug for the %(class)', max_length=128, unique=True)), + ('name', models.CharField(max_length=100)) ], ), migrations.CreateModel( - name="Light", + name='Light', fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ("slug", models.SlugField(help_text="Unique slug for the %(class)", max_length=128, unique=True)), - ("name", models.CharField(max_length=100)), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('slug', models.SlugField(help_text='Unique slug for the %(class)', max_length=128, unique=True)), + ('name', models.CharField(max_length=100)) ], ), migrations.CreateModel( - name="Spectrum", + name='Spectrum', fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), - ), - ( - "data", - proteins.models.spectrum.SpectrumData( - base_field=django.contrib.postgres.fields.ArrayField( - base_field=models.FloatField(max_length=10), size=2 - ), - size=None, - ), - ), - ( - "category", - models.CharField( - blank=True, - choices=[ - ("d", "Dye"), - ("p", "Protein"), - ("l", "Light Source"), - ("f", "Filter"), - ("c", "Camera"), - ], - db_index=True, - max_length=1, - verbose_name="Item Type", - ), - ), - ( - "subtype", - models.CharField( - choices=[ - ("ex", "excitation"), - ("ab", "absorption"), - ("em", "emission"), - ("2p", "two photon absorption"), - ("bx", "bandpass (excitation)"), - ("bm", "bandpass (emission)"), - ("sp", "shortpass"), - ("lp", "longpass"), - ("bs", "beamsplitter"), - ("qe", "Quantum Efficiency"), - ("pd", "Power Distribution"), - ], - db_index=True, - max_length=2, - verbose_name="Spectra Subtype", - ), - ), - ("ph", models.FloatField(blank=True, null=True, verbose_name="pH")), - ("solvent", models.CharField(blank=True, max_length=128)), - ( - "created_by", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="spectrum_author", - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "owner_camera", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="spectra", - to="proteins.Camera", - ), - ), - ( - "owner_dye", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="spectra", - to="proteins.Dye", - ), - ), - ( - "owner_filter", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="spectra", - to="proteins.Filter", - ), - ), - ( - "owner_light", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="spectra", - to="proteins.Light", - ), - ), - ( - "owner_state", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="spectra", - to="proteins.State", - ), - ), - ( - "updated_by", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="spectrum_modifier", - to=settings.AUTH_USER_MODEL, - ), - ), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('data', proteins.models.spectrum.SpectrumData(base_field=django.contrib.postgres.fields.ArrayField(base_field=models.FloatField(max_length=10), size=2), size=None)), + ('category', models.CharField(blank=True, choices=[('d', 'Dye'), ('p', 'Protein'), ('l', 'Light Source'), ('f', 'Filter'), ('c', 'Camera')], db_index=True, max_length=1, verbose_name='Item Type')), + ('subtype', models.CharField(choices=[('ex', 'excitation'), ('ab', 'absorption'), ('em', 'emission'), ('2p', 'two photon absorption'), ('bx', 'bandpass (excitation)'), ('bm', 'bandpass (emission)'), ('sp', 'shortpass'), ('lp', 'longpass'), ('bs', 'beamsplitter'), ('qe', 'Quantum Efficiency'), ('pd', 'Power Distribution')], db_index=True, max_length=2, verbose_name='Spectra Subtype')), + ('ph', models.FloatField(blank=True, null=True, verbose_name='pH')), + ('solvent', models.CharField(blank=True, max_length=128)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='spectrum_author', to=settings.AUTH_USER_MODEL)), + ('owner_camera', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectra', to='proteins.Camera')), + ('owner_dye', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectra', to='proteins.Dye')), + ('owner_filter', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectra', to='proteins.Filter')), + ('owner_light', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectra', to='proteins.Light')), + ('owner_state', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectra', to='proteins.State')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='spectrum_modifier', to=settings.AUTH_USER_MODEL)) ], options={ - "abstract": False, + 'abstract': False, }, ), migrations.RunPython( @@ -360,15 +137,16 @@ class Migration(migrations.Migration): reverse_code=undo_move_spectra, ), migrations.RemoveField( - model_name="state", - name="em_spectra", + model_name='state', + name='em_spectra', ), migrations.RemoveField( - model_name="state", - name="ex_spectra", + model_name='state', + name='ex_spectra', ), migrations.RemoveField( - model_name="state", - name="twop_ex_spectra", + model_name='state', + name='twop_ex_spectra', ), ] + diff --git a/backend/proteins/migrations/0006_auto_20180512_0058.py b/backend/proteins/migrations/0006_auto_20180512_0058.py index 31d0b226b..aa2c13e30 100644 --- a/backend/proteins/migrations/0006_auto_20180512_0058.py +++ b/backend/proteins/migrations/0006_auto_20180512_0058.py @@ -4,13 +4,14 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0005_auto_20180328_1614_squashed_0010_auto_20180501_1547"), + ('proteins', '0005_auto_20180328_1614_squashed_0010_auto_20180501_1547'), ] operations = [ migrations.AlterModelOptions( - name="spectrum", - options={"verbose_name_plural": "spectra"}, + name='spectrum', + options={'verbose_name_plural': 'spectra'}, ), ] diff --git a/backend/proteins/migrations/0007_auto_20180403_0140.py b/backend/proteins/migrations/0007_auto_20180403_0140.py index 39e67da2a..ea5ae33af 100644 --- a/backend/proteins/migrations/0007_auto_20180403_0140.py +++ b/backend/proteins/migrations/0007_auto_20180403_0140.py @@ -1,70 +1,41 @@ +# -*- coding: utf-8 -*- # Generated by Django 1.11.9 on 2018-04-03 01:40 +from __future__ import unicode_literals import django.core.validators from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0006_auto_20180401_2009"), + ('proteins', '0006_auto_20180401_2009'), ] operations = [ migrations.AddField( - model_name="filter", - name="aoi", - field=models.PositiveSmallIntegerField( - blank=True, - null=True, - validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(90)], - ), + model_name='filter', + name='aoi', + field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(90)]), ), migrations.AddField( - model_name="filter", - name="bandcenter", - field=models.PositiveSmallIntegerField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(200), - django.core.validators.MaxValueValidator(1600), - ], - ), + model_name='filter', + name='bandcenter', + field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(200), django.core.validators.MaxValueValidator(1600)]), ), migrations.AddField( - model_name="filter", - name="bandwidth", - field=models.PositiveSmallIntegerField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(300), - django.core.validators.MaxValueValidator(900), - ], - ), + model_name='filter', + name='bandwidth', + field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(900)]), ), migrations.AddField( - model_name="filter", - name="edge", - field=models.FloatField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(200), - ], - ), + model_name='filter', + name='edge', + field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(200)]), ), migrations.AddField( - model_name="filter", - name="tavg", - field=models.PositiveSmallIntegerField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(300), - django.core.validators.MaxValueValidator(900), - ], - ), + model_name='filter', + name='tavg', + field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(900)]), ), ] diff --git a/backend/proteins/migrations/0007_auto_20180513_2346.py b/backend/proteins/migrations/0007_auto_20180513_2346.py index 0bbebf40c..783ddaf4e 100644 --- a/backend/proteins/migrations/0007_auto_20180513_2346.py +++ b/backend/proteins/migrations/0007_auto_20180513_2346.py @@ -1,103 +1,56 @@ # Generated by Django 2.0.5 on 2018-05-13 23:46 -import django.db.models.deletion from django.conf import settings from django.db import migrations, models +import django.db.models.deletion class Migration(migrations.Migration): + dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("proteins", "0006_auto_20180512_0058"), + ('proteins', '0006_auto_20180512_0058'), ] operations = [ migrations.AddField( - model_name="camera", - name="created_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="camera_author", - to=settings.AUTH_USER_MODEL, - ), + model_name='camera', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='camera_author', to=settings.AUTH_USER_MODEL), ), migrations.AddField( - model_name="camera", - name="updated_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="camera_modifier", - to=settings.AUTH_USER_MODEL, - ), + model_name='camera', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='camera_modifier', to=settings.AUTH_USER_MODEL), ), migrations.AddField( - model_name="dye", - name="created_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="dye_author", - to=settings.AUTH_USER_MODEL, - ), + model_name='dye', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dye_author', to=settings.AUTH_USER_MODEL), ), migrations.AddField( - model_name="dye", - name="updated_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="dye_modifier", - to=settings.AUTH_USER_MODEL, - ), + model_name='dye', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dye_modifier', to=settings.AUTH_USER_MODEL), ), migrations.AddField( - model_name="filter", - name="created_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="filter_author", - to=settings.AUTH_USER_MODEL, - ), + model_name='filter', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='filter_author', to=settings.AUTH_USER_MODEL), ), migrations.AddField( - model_name="filter", - name="updated_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="filter_modifier", - to=settings.AUTH_USER_MODEL, - ), + model_name='filter', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='filter_modifier', to=settings.AUTH_USER_MODEL), ), migrations.AddField( - model_name="light", - name="created_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="light_author", - to=settings.AUTH_USER_MODEL, - ), + model_name='light', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='light_author', to=settings.AUTH_USER_MODEL), ), migrations.AddField( - model_name="light", - name="updated_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="light_modifier", - to=settings.AUTH_USER_MODEL, - ), + model_name='light', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='light_modifier', to=settings.AUTH_USER_MODEL), ), ] diff --git a/backend/proteins/migrations/0008_auto_20180430_0308.py b/backend/proteins/migrations/0008_auto_20180430_0308.py index 0456efc81..177f243ae 100644 --- a/backend/proteins/migrations/0008_auto_20180430_0308.py +++ b/backend/proteins/migrations/0008_auto_20180430_0308.py @@ -1,65 +1,45 @@ +# -*- coding: utf-8 -*- # Generated by Django 1.11.9 on 2018-04-30 03:08 +from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0007_auto_20180403_0140"), + ('proteins', '0007_auto_20180403_0140'), ] operations = [ migrations.AddField( - model_name="camera", - name="manufacturer", + model_name='camera', + name='manufacturer', field=models.CharField(blank=True, max_length=128), ), migrations.AddField( - model_name="filter", - name="manufacturer", + model_name='filter', + name='manufacturer', field=models.CharField(blank=True, max_length=128), ), migrations.AddField( - model_name="light", - name="manufacturer", + model_name='light', + name='manufacturer', field=models.CharField(blank=True, max_length=128), ), migrations.AlterField( - model_name="spectrum", - name="category", - field=models.CharField( - choices=[("d", "Dye"), ("p", "Protein"), ("l", "Light Source"), ("f", "Filter"), ("c", "Camera")], - db_index=True, - max_length=1, - verbose_name="Item Type", - ), + model_name='spectrum', + name='category', + field=models.CharField(choices=[('d', 'Dye'), ('p', 'Protein'), ('l', 'Light Source'), ('f', 'Filter'), ('c', 'Camera')], db_index=True, max_length=1, verbose_name='Item Type'), ), migrations.AlterField( - model_name="spectrum", - name="subtype", - field=models.CharField( - blank=True, - choices=[ - ("ex", "excitation"), - ("ab", "absorption"), - ("em", "emission"), - ("2p", "two photon absorption"), - ("bx", "bandpass (excitation)"), - ("bm", "bandpass (emission)"), - ("sp", "shortpass"), - ("lp", "longpass"), - ("bs", "beamsplitter"), - ("qe", "Quantum Efficiency"), - ("pd", "Power Distribution"), - ], - db_index=True, - max_length=2, - verbose_name="Spectra Subtype", - ), + model_name='spectrum', + name='subtype', + field=models.CharField(blank=True, choices=[('ex', 'excitation'), ('ab', 'absorption'), ('em', 'emission'), ('2p', 'two photon absorption'), ('bx', 'bandpass (excitation)'), ('bm', 'bandpass (emission)'), ('sp', 'shortpass'), ('lp', 'longpass'), ('bs', 'beamsplitter'), ('qe', 'Quantum Efficiency'), ('pd', 'Power Distribution')], db_index=True, max_length=2, verbose_name='Spectra Subtype'), ), migrations.AlterField( - model_name="state", - name="slug", - field=models.SlugField(help_text="Unique slug for the %(class)", max_length=128, unique=True), + model_name='state', + name='slug', + field=models.SlugField(help_text='Unique slug for the %(class)', max_length=128, unique=True), ), ] diff --git a/backend/proteins/migrations/0008_auto_20180515_1659.py b/backend/proteins/migrations/0008_auto_20180515_1659.py index b4fb8ae8b..cc61bdba3 100644 --- a/backend/proteins/migrations/0008_auto_20180515_1659.py +++ b/backend/proteins/migrations/0008_auto_20180515_1659.py @@ -4,29 +4,30 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0007_auto_20180513_2346"), + ('proteins', '0007_auto_20180513_2346'), ] operations = [ migrations.AddField( - model_name="camera", - name="url", + model_name='camera', + name='url', field=models.URLField(blank=True), ), migrations.AddField( - model_name="dye", - name="url", + model_name='dye', + name='url', field=models.URLField(blank=True), ), migrations.AddField( - model_name="filter", - name="url", + model_name='filter', + name='url', field=models.URLField(blank=True), ), migrations.AddField( - model_name="light", - name="url", + model_name='light', + name='url', field=models.URLField(blank=True), ), ] diff --git a/backend/proteins/migrations/0009_auto_20180430_0335.py b/backend/proteins/migrations/0009_auto_20180430_0335.py index d57f11276..57df2dc01 100644 --- a/backend/proteins/migrations/0009_auto_20180430_0335.py +++ b/backend/proteins/migrations/0009_auto_20180430_0335.py @@ -1,22 +1,21 @@ +# -*- coding: utf-8 -*- # Generated by Django 1.11.9 on 2018-04-30 03:35 +from __future__ import unicode_literals import django.core.validators from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0008_auto_20180430_0308"), + ('proteins', '0008_auto_20180430_0308'), ] operations = [ migrations.AlterField( - model_name="filter", - name="tavg", - field=models.FloatField( - blank=True, - null=True, - validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], - ), + model_name='filter', + name='tavg', + field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)]), ), ] diff --git a/backend/proteins/migrations/0009_auto_20180525_1640.py b/backend/proteins/migrations/0009_auto_20180525_1640.py index 235286172..bbe33f0c9 100644 --- a/backend/proteins/migrations/0009_auto_20180525_1640.py +++ b/backend/proteins/migrations/0009_auto_20180525_1640.py @@ -4,32 +4,15 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0008_auto_20180515_1659"), + ('proteins', '0008_auto_20180515_1659'), ] operations = [ migrations.AlterField( - model_name="spectrum", - name="subtype", - field=models.CharField( - choices=[ - ("ex", "Excitation"), - ("ab", "Absorption"), - ("em", "Emission"), - ("2p", "Two Photon Absorption"), - ("bp", "Bandpass"), - ("bx", "Bandpass (Excitation)"), - ("bm", "Bandpass (Emission)"), - ("sp", "Shortpass"), - ("lp", "Longpass"), - ("bs", "Beamsplitter"), - ("qe", "Quantum Efficiency"), - ("pd", "Power Distribution"), - ], - db_index=True, - max_length=2, - verbose_name="Spectra Subtype", - ), + model_name='spectrum', + name='subtype', + field=models.CharField(choices=[('ex', 'Excitation'), ('ab', 'Absorption'), ('em', 'Emission'), ('2p', 'Two Photon Absorption'), ('bp', 'Bandpass'), ('bx', 'Bandpass (Excitation)'), ('bm', 'Bandpass (Emission)'), ('sp', 'Shortpass'), ('lp', 'Longpass'), ('bs', 'Beamsplitter'), ('qe', 'Quantum Efficiency'), ('pd', 'Power Distribution')], db_index=True, max_length=2, verbose_name='Spectra Subtype'), ), ] diff --git a/backend/proteins/migrations/0010_auto_20180501_1547.py b/backend/proteins/migrations/0010_auto_20180501_1547.py index 1dcac3fc3..088f635c8 100644 --- a/backend/proteins/migrations/0010_auto_20180501_1547.py +++ b/backend/proteins/migrations/0010_auto_20180501_1547.py @@ -1,61 +1,45 @@ +# -*- coding: utf-8 -*- # Generated by Django 1.11.11 on 2018-05-01 15:47 +from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0009_auto_20180430_0335"), + ('proteins', '0009_auto_20180430_0335'), ] operations = [ migrations.AddField( - model_name="camera", - name="part", + model_name='camera', + name='part', field=models.CharField(blank=True, max_length=128), ), migrations.AddField( - model_name="dye", - name="manufacturer", + model_name='dye', + name='manufacturer', field=models.CharField(blank=True, max_length=128), ), migrations.AddField( - model_name="dye", - name="part", + model_name='dye', + name='part', field=models.CharField(blank=True, max_length=128), ), migrations.AddField( - model_name="filter", - name="part", + model_name='filter', + name='part', field=models.CharField(blank=True, max_length=128), ), migrations.AddField( - model_name="light", - name="part", + model_name='light', + name='part', field=models.CharField(blank=True, max_length=128), ), migrations.AlterField( - model_name="spectrum", - name="subtype", - field=models.CharField( - blank=True, - choices=[ - ("ex", "Excitation"), - ("ab", "Absorption"), - ("em", "Emission"), - ("2p", "Two Photon Absorption"), - ("bp", "Bandpass"), - ("bx", "Bandpass (Excitation)"), - ("bm", "Bandpass (Emission)"), - ("sp", "Shortpass"), - ("lp", "Longpass"), - ("bs", "Beamsplitter"), - ("qe", "Quantum Efficiency"), - ("pd", "Power Distribution"), - ], - db_index=True, - max_length=2, - verbose_name="Spectra Subtype", - ), + model_name='spectrum', + name='subtype', + field=models.CharField(blank=True, choices=[('ex', 'Excitation'), ('ab', 'Absorption'), ('em', 'Emission'), ('2p', 'Two Photon Absorption'), ('bp', 'Bandpass'), ('bx', 'Bandpass (Excitation)'), ('bm', 'Bandpass (Emission)'), ('sp', 'Shortpass'), ('lp', 'Longpass'), ('bs', 'Beamsplitter'), ('qe', 'Quantum Efficiency'), ('pd', 'Power Distribution')], db_index=True, max_length=2, verbose_name='Spectra Subtype'), ), ] diff --git a/backend/proteins/migrations/0010_auto_20180525_1840.py b/backend/proteins/migrations/0010_auto_20180525_1840.py index a66784450..469c4add0 100644 --- a/backend/proteins/migrations/0010_auto_20180525_1840.py +++ b/backend/proteins/migrations/0010_auto_20180525_1840.py @@ -1,56 +1,45 @@ # Generated by Django 2.0.5 on 2018-05-25 18:40 +from django.db import migrations import django.utils.timezone import model_utils.fields -from django.db import migrations class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0009_auto_20180525_1640"), + ('proteins', '0009_auto_20180525_1640'), ] operations = [ migrations.AddField( - model_name="camera", - name="created", - field=model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), + model_name='camera', + name='created', + field=model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created'), ), migrations.AddField( - model_name="camera", - name="modified", - field=model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), + model_name='camera', + name='modified', + field=model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified'), ), migrations.AddField( - model_name="filter", - name="created", - field=model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), + model_name='filter', + name='created', + field=model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created'), ), migrations.AddField( - model_name="filter", - name="modified", - field=model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), + model_name='filter', + name='modified', + field=model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified'), ), migrations.AddField( - model_name="light", - name="created", - field=model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), + model_name='light', + name='created', + field=model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created'), ), migrations.AddField( - model_name="light", - name="modified", - field=model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), + model_name='light', + name='modified', + field=model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified'), ), ] diff --git a/backend/proteins/migrations/0011_auto_20180525_2349.py b/backend/proteins/migrations/0011_auto_20180525_2349.py index 1698f57c0..00e7bcfd8 100644 --- a/backend/proteins/migrations/0011_auto_20180525_2349.py +++ b/backend/proteins/migrations/0011_auto_20180525_2349.py @@ -4,42 +4,20 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0010_auto_20180525_1840"), + ('proteins', '0010_auto_20180525_1840'), ] operations = [ migrations.AlterField( - model_name="spectrum", - name="category", - field=models.CharField( - choices=[("d", "Dye"), ("p", "Protein"), ("l", "Light Source"), ("f", "Filter"), ("c", "Camera")], - db_index=True, - max_length=1, - verbose_name="Owner Type", - ), + model_name='spectrum', + name='category', + field=models.CharField(choices=[('d', 'Dye'), ('p', 'Protein'), ('l', 'Light Source'), ('f', 'Filter'), ('c', 'Camera')], db_index=True, max_length=1, verbose_name='Owner Type'), ), migrations.AlterField( - model_name="spectrum", - name="subtype", - field=models.CharField( - choices=[ - ("ex", "Excitation"), - ("ab", "Absorption"), - ("em", "Emission"), - ("2p", "Two Photon Abs"), - ("bp", "Bandpass"), - ("bx", "Bandpass-Ex"), - ("bm", "Bandpass-Em"), - ("sp", "Shortpass"), - ("lp", "Longpass"), - ("bs", "Beamsplitter"), - ("qe", "Quantum Efficiency"), - ("pd", "Power Distribution"), - ], - db_index=True, - max_length=2, - verbose_name="Spectrum Subtype", - ), + model_name='spectrum', + name='subtype', + field=models.CharField(choices=[('ex', 'Excitation'), ('ab', 'Absorption'), ('em', 'Emission'), ('2p', 'Two Photon Abs'), ('bp', 'Bandpass'), ('bx', 'Bandpass-Ex'), ('bm', 'Bandpass-Em'), ('sp', 'Shortpass'), ('lp', 'Longpass'), ('bs', 'Beamsplitter'), ('qe', 'Quantum Efficiency'), ('pd', 'Power Distribution')], db_index=True, max_length=2, verbose_name='Spectrum Subtype'), ), ] diff --git a/backend/proteins/migrations/0012_auto_20180708_1811.py b/backend/proteins/migrations/0012_auto_20180708_1811.py index 2d7aa3f76..48d9960a6 100644 --- a/backend/proteins/migrations/0012_auto_20180708_1811.py +++ b/backend/proteins/migrations/0012_auto_20180708_1811.py @@ -1,37 +1,26 @@ # Generated by Django 2.0.6 on 2018-07-08 18:11 -import django.db.models.deletion from django.conf import settings from django.db import migrations, models +import django.db.models.deletion class Migration(migrations.Migration): + dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("proteins", "0011_auto_20180525_2349"), + ('proteins', '0011_auto_20180525_2349'), ] operations = [ migrations.AddField( - model_name="statetransition", - name="created_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="statetransition_author", - to=settings.AUTH_USER_MODEL, - ), + model_name='statetransition', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='statetransition_author', to=settings.AUTH_USER_MODEL), ), migrations.AddField( - model_name="statetransition", - name="updated_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="statetransition_modifier", - to=settings.AUTH_USER_MODEL, - ), + model_name='statetransition', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='statetransition_modifier', to=settings.AUTH_USER_MODEL), ), ] diff --git a/backend/proteins/migrations/0013_auto_20180718_1717.py b/backend/proteins/migrations/0013_auto_20180718_1717.py index 088ae3c7f..9c3321369 100644 --- a/backend/proteins/migrations/0013_auto_20180718_1717.py +++ b/backend/proteins/migrations/0013_auto_20180718_1717.py @@ -1,13 +1,12 @@ # Generated by Django 2.0.6 on 2018-07-18 17:17 +from django.conf import settings import django.contrib.postgres.fields import django.core.validators +from django.db import migrations, models import django.db.models.deletion import django.utils.timezone import model_utils.fields -from django.conf import settings -from django.db import migrations, models - import proteins.util.helpers @@ -15,15 +14,18 @@ def resavestuff(apps, schema_editor): Filter = apps.get_model("proteins", "Filter") Light = apps.get_model("proteins", "Light") - for f in Filter.objects.filter(name__icontains="ff0", spectrum__subtype="bs", manufacturer__icontains="semrock"): - f.subtype = "bp" - for l in Light.objects.filter(manufacturer="lumencor"): - l.manufacturer = "Lumencor" + for f in Filter.objects.filter( + name__icontains='ff0', + spectrum__subtype='bs', + manufacturer__icontains='semrock'): + f.subtype = 'bp' + for l in Light.objects.filter(manufacturer='lumencor'): + l.manufacturer = 'Lumencor' l.save() - for f in Filter.objects.filter(manufacturer="lumencor"): - f.manufacturer = "Lumencor" + for f in Filter.objects.filter(manufacturer='lumencor'): + f.manufacturer = 'Lumencor' if f.part[:5].isdigit(): - f.part = f.part[:3] + "/" + f.part[3:] + f.part = f.part[:3] + '/' + f.part[3:] f.save() @@ -32,317 +34,175 @@ def resavestuff_back(apps, schema_editor): class Migration(migrations.Migration): + dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("proteins", "0012_auto_20180708_1811"), + ('proteins', '0012_auto_20180708_1811'), ] operations = [ migrations.CreateModel( - name="FilterPlacement", + name='FilterPlacement', fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ( - "path", - models.CharField( - choices=[("ex", "Excitation Path"), ("em", "Emission Path"), ("bs", "Both Paths")], - max_length=2, - verbose_name="Ex/Em Path", - ), - ), - ( - "reflects", - models.BooleanField( - default=False, help_text="Filter reflects light at this position in the light path" - ), - ), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('path', models.CharField(choices=[('ex', 'Excitation Path'), ('em', 'Emission Path'), ('bs', 'Both Paths')], max_length=2, verbose_name='Ex/Em Path')), + ('reflects', models.BooleanField(default=False, help_text='Filter reflects light at this position in the light path')), ], ), migrations.CreateModel( - name="FluorophoreCollection", + name='FluorophoreCollection', fields=[ - ( - "proteincollection_ptr", - models.OneToOneField( - auto_created=True, - on_delete=django.db.models.deletion.CASCADE, - parent_link=True, - primary_key=True, - serialize=False, - to="proteins.ProteinCollection", - ), - ), - ("dyes", models.ManyToManyField(blank=True, related_name="collection_memberships", to="proteins.Dye")), + ('proteincollection_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='proteins.ProteinCollection')), + ('dyes', models.ManyToManyField(blank=True, related_name='collection_memberships', to='proteins.Dye')), ], options={ - "abstract": False, + 'abstract': False, }, - bases=("proteins.proteincollection",), + bases=('proteins.proteincollection',), ), migrations.CreateModel( - name="Microscope", + name='Microscope', fields=[ - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), - ), - ("name", models.CharField(max_length=100)), - ("description", models.CharField(blank=True, max_length=512)), - ( - "id", - models.CharField( - default=proteins.util.helpers.shortuuid, - editable=False, - max_length=22, - primary_key=True, - serialize=False, - ), - ), - ( - "lasers", - django.contrib.postgres.fields.ArrayField( - base_field=models.PositiveSmallIntegerField( - validators=[ - django.core.validators.MinValueValidator(300), - django.core.validators.MaxValueValidator(1600), - ] - ), - blank=True, - default=list, - size=None, - ), - ), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('name', models.CharField(max_length=100)), + ('description', models.CharField(blank=True, max_length=512)), + ('id', models.CharField(default=proteins.util.helpers.shortuuid, editable=False, max_length=22, primary_key=True, serialize=False)), + ('lasers', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1600)]), blank=True, default=list, size=None)), ], options={ - "abstract": False, + 'abstract': False, }, ), migrations.CreateModel( - name="OpticalConfig", + name='OpticalConfig', fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), - ), - ("name", models.CharField(max_length=100)), - ("description", models.CharField(blank=True, max_length=512)), - ( - "laser", - models.PositiveSmallIntegerField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(300), - django.core.validators.MaxValueValidator(1600), - ], - ), - ), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('name', models.CharField(max_length=100)), + ('description', models.CharField(blank=True, max_length=512)), + ('laser', models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1600)])), ], options={ - "ordering": ["name"], + 'ordering': ['name'], }, ), migrations.AlterModelOptions( - name="camera", - options={"ordering": ["name"]}, + name='camera', + options={'ordering': ['name']}, ), migrations.AlterModelOptions( - name="filter", - options={"ordering": ["bandcenter"]}, + name='filter', + options={'ordering': ['bandcenter']}, ), migrations.AlterModelOptions( - name="light", - options={"ordering": ["name"]}, + name='light', + options={'ordering': ['name']}, ), migrations.AlterField( - model_name="proteincollection", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="proteincollections", - to=settings.AUTH_USER_MODEL, - ), + model_name='proteincollection', + name='owner', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='proteincollections', to=settings.AUTH_USER_MODEL), ), migrations.AlterField( - model_name="spectrum", - name="category", - field=models.CharField( - choices=[("d", "Dye"), ("p", "Protein"), ("l", "Light Source"), ("f", "Filter"), ("c", "Camera")], - db_index=True, - max_length=1, - verbose_name="Spectrum Type", - ), + model_name='spectrum', + name='category', + field=models.CharField(choices=[('d', 'Dye'), ('p', 'Protein'), ('l', 'Light Source'), ('f', 'Filter'), ('c', 'Camera')], db_index=True, max_length=1, verbose_name='Spectrum Type'), ), migrations.AlterField( - model_name="spectrum", - name="owner_camera", - field=models.OneToOneField( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="spectrum", - to="proteins.Camera", - ), + model_name='spectrum', + name='owner_camera', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectrum', to='proteins.Camera'), ), migrations.AlterField( - model_name="spectrum", - name="owner_filter", - field=models.OneToOneField( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="spectrum", - to="proteins.Filter", - ), + model_name='spectrum', + name='owner_filter', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectrum', to='proteins.Filter'), ), migrations.AlterField( - model_name="spectrum", - name="owner_light", - field=models.OneToOneField( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="spectrum", - to="proteins.Light", - ), + model_name='spectrum', + name='owner_light', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='spectrum', to='proteins.Light'), ), migrations.AddField( - model_name="opticalconfig", - name="camera", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="optical_configs", - to="proteins.Camera", - ), + model_name='opticalconfig', + name='camera', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='optical_configs', to='proteins.Camera'), ), migrations.AddField( - model_name="opticalconfig", - name="filters", - field=models.ManyToManyField( - blank=True, related_name="optical_configs", through="proteins.FilterPlacement", to="proteins.Filter" - ), + model_name='opticalconfig', + name='filters', + field=models.ManyToManyField(blank=True, related_name='optical_configs', through='proteins.FilterPlacement', to='proteins.Filter'), ), migrations.AddField( - model_name="opticalconfig", - name="light", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="optical_configs", - to="proteins.Light", - ), + model_name='opticalconfig', + name='light', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='optical_configs', to='proteins.Light'), ), migrations.AddField( - model_name="opticalconfig", - name="microscope", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, related_name="optical_configs", to="proteins.Microscope" - ), + model_name='opticalconfig', + name='microscope', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='optical_configs', to='proteins.Microscope'), ), migrations.AddField( - model_name="opticalconfig", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="opticalconfigs", - to=settings.AUTH_USER_MODEL, - ), + model_name='opticalconfig', + name='owner', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='opticalconfigs', to=settings.AUTH_USER_MODEL), ), migrations.AddField( - model_name="microscope", - name="bs_filters", - field=models.ManyToManyField(blank=True, related_name="as_bs_filter", to="proteins.Filter"), + model_name='microscope', + name='bs_filters', + field=models.ManyToManyField(blank=True, related_name='as_bs_filter', to='proteins.Filter'), ), migrations.AddField( - model_name="microscope", - name="cameras", - field=models.ManyToManyField(blank=True, related_name="microscopes", to="proteins.Camera"), + model_name='microscope', + name='cameras', + field=models.ManyToManyField(blank=True, related_name='microscopes', to='proteins.Camera'), ), migrations.AddField( - model_name="microscope", - name="collection", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="on_scope", - to="proteins.ProteinCollection", - ), + model_name='microscope', + name='collection', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='on_scope', to='proteins.ProteinCollection'), ), migrations.AddField( - model_name="microscope", - name="em_filters", - field=models.ManyToManyField(blank=True, related_name="as_em_filter", to="proteins.Filter"), + model_name='microscope', + name='em_filters', + field=models.ManyToManyField(blank=True, related_name='as_em_filter', to='proteins.Filter'), ), migrations.AddField( - model_name="microscope", - name="ex_filters", - field=models.ManyToManyField(blank=True, related_name="as_ex_filter", to="proteins.Filter"), + model_name='microscope', + name='ex_filters', + field=models.ManyToManyField(blank=True, related_name='as_ex_filter', to='proteins.Filter'), ), migrations.AddField( - model_name="microscope", - name="fluors", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="fluor_on_scope", - to="proteins.FluorophoreCollection", - ), + model_name='microscope', + name='fluors', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='fluor_on_scope', to='proteins.FluorophoreCollection'), ), migrations.AddField( - model_name="microscope", - name="lights", - field=models.ManyToManyField(blank=True, related_name="microscopes", to="proteins.Light"), + model_name='microscope', + name='lights', + field=models.ManyToManyField(blank=True, related_name='microscopes', to='proteins.Light'), ), migrations.AddField( - model_name="microscope", - name="owner", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="microscopes", - to=settings.AUTH_USER_MODEL, - ), + model_name='microscope', + name='owner', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='microscopes', to=settings.AUTH_USER_MODEL), ), migrations.AddField( - model_name="filterplacement", - name="config", - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="proteins.OpticalConfig"), + model_name='filterplacement', + name='config', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='proteins.OpticalConfig'), ), migrations.AddField( - model_name="filterplacement", - name="filter", - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="proteins.Filter"), + model_name='filterplacement', + name='filter', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='proteins.Filter'), ), migrations.AlterUniqueTogether( - name="opticalconfig", - unique_together={("name", "microscope")}, + name='opticalconfig', + unique_together={('name', 'microscope')}, ), migrations.RunPython(resavestuff, resavestuff_back), ] diff --git a/backend/proteins/migrations/0014_auto_20180718_2340.py b/backend/proteins/migrations/0014_auto_20180718_2340.py index 0c8b48bf6..b5b7f1f3c 100644 --- a/backend/proteins/migrations/0014_auto_20180718_2340.py +++ b/backend/proteins/migrations/0014_auto_20180718_2340.py @@ -1,35 +1,24 @@ # Generated by Django 2.0.7 on 2018-07-18 23:40 -import django.db.models.deletion from django.db import migrations, models +import django.db.models.deletion class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0013_auto_20180718_1717"), + ('proteins', '0013_auto_20180718_1717'), ] operations = [ migrations.AlterField( - model_name="opticalconfig", - name="camera", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="optical_configs", - to="proteins.Camera", - ), + model_name='opticalconfig', + name='camera', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='optical_configs', to='proteins.Camera'), ), migrations.AlterField( - model_name="opticalconfig", - name="light", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="optical_configs", - to="proteins.Light", - ), + model_name='opticalconfig', + name='light', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='optical_configs', to='proteins.Light'), ), ] diff --git a/backend/proteins/migrations/0015_auto_20180720_1921.py b/backend/proteins/migrations/0015_auto_20180720_1921.py index d6da9353a..c45c08e3c 100644 --- a/backend/proteins/migrations/0015_auto_20180720_1921.py +++ b/backend/proteins/migrations/0015_auto_20180720_1921.py @@ -5,34 +5,29 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0014_auto_20180718_2340"), + ('proteins', '0014_auto_20180718_2340'), ] operations = [ migrations.AlterModelOptions( - name="microscope", - options={"ordering": ["created"]}, + name='microscope', + options={'ordering': ['created']}, ), migrations.AddField( - model_name="microscope", - name="managers", - field=django.contrib.postgres.fields.ArrayField( - base_field=models.EmailField(max_length=254), blank=True, default=list, size=None - ), + model_name='microscope', + name='managers', + field=django.contrib.postgres.fields.ArrayField(base_field=models.EmailField(max_length=254), blank=True, default=list, size=None), ), migrations.AddField( - model_name="opticalconfig", - name="managers", - field=django.contrib.postgres.fields.ArrayField( - base_field=models.EmailField(max_length=254), blank=True, default=list, size=None - ), + model_name='opticalconfig', + name='managers', + field=django.contrib.postgres.fields.ArrayField(base_field=models.EmailField(max_length=254), blank=True, default=list, size=None), ), migrations.AddField( - model_name="proteincollection", - name="managers", - field=django.contrib.postgres.fields.ArrayField( - base_field=models.EmailField(max_length=254), blank=True, default=list, size=None - ), + model_name='proteincollection', + name='managers', + field=django.contrib.postgres.fields.ArrayField(base_field=models.EmailField(max_length=254), blank=True, default=list, size=None), ), ] diff --git a/backend/proteins/migrations/0016_auto_20180722_1314.py b/backend/proteins/migrations/0016_auto_20180722_1314.py index 3b2300ee7..d106dec6e 100644 --- a/backend/proteins/migrations/0016_auto_20180722_1314.py +++ b/backend/proteins/migrations/0016_auto_20180722_1314.py @@ -4,38 +4,39 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0015_auto_20180720_1921"), + ('proteins', '0015_auto_20180720_1921'), ] operations = [ migrations.RemoveField( - model_name="microscope", - name="bs_filters", + model_name='microscope', + name='bs_filters', ), migrations.RemoveField( - model_name="microscope", - name="cameras", + model_name='microscope', + name='cameras', ), migrations.RemoveField( - model_name="microscope", - name="em_filters", + model_name='microscope', + name='em_filters', ), migrations.RemoveField( - model_name="microscope", - name="ex_filters", + model_name='microscope', + name='ex_filters', ), migrations.RemoveField( - model_name="microscope", - name="lasers", + model_name='microscope', + name='lasers', ), migrations.RemoveField( - model_name="microscope", - name="lights", + model_name='microscope', + name='lights', ), migrations.AddField( - model_name="opticalconfig", - name="comments", + model_name='opticalconfig', + name='comments', field=models.CharField(blank=True, max_length=256), ), ] diff --git a/backend/proteins/migrations/0017_auto_20180722_1626.py b/backend/proteins/migrations/0017_auto_20180722_1626.py index b2e949377..c401d4bb0 100644 --- a/backend/proteins/migrations/0017_auto_20180722_1626.py +++ b/backend/proteins/migrations/0017_auto_20180722_1626.py @@ -6,34 +6,25 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0016_auto_20180722_1314"), + ('proteins', '0016_auto_20180722_1314'), ] operations = [ migrations.AddField( - model_name="microscope", - name="extra_cameras", - field=models.ManyToManyField(blank=True, related_name="microscopes", to="proteins.Camera"), + model_name='microscope', + name='extra_cameras', + field=models.ManyToManyField(blank=True, related_name='microscopes', to='proteins.Camera'), ), migrations.AddField( - model_name="microscope", - name="extra_lasers", - field=django.contrib.postgres.fields.ArrayField( - base_field=models.PositiveSmallIntegerField( - validators=[ - django.core.validators.MinValueValidator(300), - django.core.validators.MaxValueValidator(1600), - ] - ), - blank=True, - default=list, - size=None, - ), + model_name='microscope', + name='extra_lasers', + field=django.contrib.postgres.fields.ArrayField(base_field=models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1600)]), blank=True, default=list, size=None), ), migrations.AddField( - model_name="microscope", - name="extra_lights", - field=models.ManyToManyField(blank=True, related_name="microscopes", to="proteins.Light"), + model_name='microscope', + name='extra_lights', + field=models.ManyToManyField(blank=True, related_name='microscopes', to='proteins.Light'), ), ] diff --git a/backend/proteins/migrations/0018_protein_cofactor.py b/backend/proteins/migrations/0018_protein_cofactor.py index 6d9a69d40..edfe0b6dc 100644 --- a/backend/proteins/migrations/0018_protein_cofactor.py +++ b/backend/proteins/migrations/0018_protein_cofactor.py @@ -4,14 +4,15 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0017_auto_20180722_1626"), + ('proteins', '0017_auto_20180722_1626'), ] operations = [ migrations.AddField( - model_name="protein", - name="cofactor", - field=models.CharField(blank=True, choices=[("bv", "Biliverdin")], max_length=2), + model_name='protein', + name='cofactor', + field=models.CharField(blank=True, choices=[('bv', 'Biliverdin')], max_length=2), ), ] diff --git a/backend/proteins/migrations/0019_auto_20180723_2200.py b/backend/proteins/migrations/0019_auto_20180723_2200.py index 27efede00..37ac856b2 100644 --- a/backend/proteins/migrations/0019_auto_20180723_2200.py +++ b/backend/proteins/migrations/0019_auto_20180723_2200.py @@ -4,19 +4,15 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0018_protein_cofactor"), + ('proteins', '0018_protein_cofactor'), ] operations = [ migrations.AlterField( - model_name="protein", - name="cofactor", - field=models.CharField( - blank=True, - choices=[("bv", "Biliverdin"), ("fl", "Flavin")], - help_text="Required for fluorescence", - max_length=2, - ), + model_name='protein', + name='cofactor', + field=models.CharField(blank=True, choices=[('bv', 'Biliverdin'), ('fl', 'Flavin')], help_text='Required for fluorescence', max_length=2), ), ] diff --git a/backend/proteins/migrations/0020_auto_20180729_0234.py b/backend/proteins/migrations/0020_auto_20180729_0234.py index eaf6756e8..11ae115d4 100644 --- a/backend/proteins/migrations/0020_auto_20180729_0234.py +++ b/backend/proteins/migrations/0020_auto_20180729_0234.py @@ -2,57 +2,35 @@ import django.core.validators from django.db import migrations, models - import proteins.models.protein import proteins.validators class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0019_auto_20180723_2200"), + ('proteins', '0019_auto_20180723_2200'), ] operations = [ migrations.AddField( - model_name="protein", - name="seq_validated", - field=models.BooleanField(default=False, help_text="Sequence has been validated by a moderator"), + model_name='protein', + name='seq_validated', + field=models.BooleanField(default=False, help_text='Sequence has been validated by a moderator'), ), migrations.AlterField( - model_name="filter", - name="bandwidth", - field=models.PositiveSmallIntegerField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(900), - ], - ), + model_name='filter', + name='bandwidth', + field=models.PositiveSmallIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(900)]), ), migrations.AlterField( - model_name="filter", - name="edge", - field=models.FloatField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(300), - django.core.validators.MaxValueValidator(1600), - ], - ), + model_name='filter', + name='edge', + field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(300), django.core.validators.MaxValueValidator(1600)]), ), migrations.AlterField( - model_name="protein", - name="seq", - field=proteins.models.protein.SequenceField( - blank=True, - help_text="Amino acid sequence (IPG ID is preferred)", - max_length=1024, - null=True, - unique=True, - validators=[proteins.validators.protein_sequence_validator], - verbose_name="Sequence", - ), + model_name='protein', + name='seq', + field=proteins.models.protein.SequenceField(blank=True, help_text='Amino acid sequence (IPG ID is preferred)', max_length=1024, null=True, unique=True, validators=[proteins.validators.protein_sequence_validator], verbose_name='Sequence'), ), ] diff --git a/backend/proteins/migrations/0021_auto_20180804_0203.py b/backend/proteins/migrations/0021_auto_20180804_0203.py index 0679f4b18..efe7910f9 100644 --- a/backend/proteins/migrations/0021_auto_20180804_0203.py +++ b/backend/proteins/migrations/0021_auto_20180804_0203.py @@ -4,40 +4,41 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0020_auto_20180729_0234"), + ('proteins', '0020_auto_20180729_0234'), ] operations = [ migrations.AlterUniqueTogether( - name="fretpair", + name='fretpair', unique_together=set(), ), migrations.RemoveField( - model_name="fretpair", - name="acceptor", + model_name='fretpair', + name='acceptor', ), migrations.RemoveField( - model_name="fretpair", - name="created_by", + model_name='fretpair', + name='created_by', ), migrations.RemoveField( - model_name="fretpair", - name="donor", + model_name='fretpair', + name='donor', ), migrations.RemoveField( - model_name="fretpair", - name="pair_references", + model_name='fretpair', + name='pair_references', ), migrations.RemoveField( - model_name="fretpair", - name="updated_by", + model_name='fretpair', + name='updated_by', ), migrations.RemoveField( - model_name="protein", - name="FRET_partner", + model_name='protein', + name='FRET_partner', ), migrations.DeleteModel( - name="FRETpair", + name='FRETpair', ), ] diff --git a/backend/proteins/migrations/0022_osermeasurement.py b/backend/proteins/migrations/0022_osermeasurement.py index 40a6de31f..d53324b36 100644 --- a/backend/proteins/migrations/0022_osermeasurement.py +++ b/backend/proteins/migrations/0022_osermeasurement.py @@ -1,159 +1,43 @@ # Generated by Django 2.0.9 on 2018-10-08 18:11 +from django.conf import settings import django.core.validators +from django.db import migrations, models import django.db.models.deletion import django.utils.timezone import model_utils.fields -from django.conf import settings -from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("references", "0003_auto_20180804_0203"), - ("proteins", "0021_auto_20180804_0203"), + ('references', '0003_auto_20180804_0203'), + ('proteins', '0021_auto_20180804_0203'), ] operations = [ migrations.CreateModel( - name="OSERMeasurement", + name='OSERMeasurement', fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), - ), - ( - "percent", - models.FloatField( - blank=True, - help_text="Photobleaching half-life (s)", - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(100), - ], - verbose_name="Percent Normal Cells", - ), - ), - ( - "percent_stddev", - models.FloatField( - blank=True, - help_text="Standard deviation of percent normal cells (if applicable)", - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(100), - ], - verbose_name="StdDev", - ), - ), - ( - "percent_ncells", - models.IntegerField( - blank=True, - help_text="Number of cells analyzed in percent normal for this FP", - null=True, - verbose_name="Number of cells for percent measurement", - ), - ), - ( - "oserne", - models.FloatField( - blank=True, - help_text="Ratio of OSER to nuclear envelope (NE) fluorescence intensities", - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(100), - ], - verbose_name="OSER/NE ratio", - ), - ), - ( - "oserne_stddev", - models.FloatField( - blank=True, - help_text="Standard deviation of OSER/NE ratio (if applicable)", - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(100), - ], - verbose_name="OSER/NE StdDev", - ), - ), - ( - "oserne_ncells", - models.IntegerField( - blank=True, - help_text="Number of cells analyzed in OSER/NE this FP", - null=True, - verbose_name="Number of cells for OSER/NE measurement", - ), - ), - ( - "celltype", - models.CharField( - blank=True, help_text="e.g. COS-7, HeLa", max_length=64, verbose_name="Cell Type" - ), - ), - ("temp", models.FloatField(blank=True, null=True, verbose_name="Temperature")), - ( - "created_by", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="osermeasurement_author", - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "protein", - models.ForeignKey( - help_text="The protein on which this measurement was made", - on_delete=django.db.models.deletion.CASCADE, - related_name="oser_measurements", - to="proteins.Protein", - verbose_name="Protein", - ), - ), - ( - "reference", - models.ForeignKey( - blank=True, - help_text="Reference where the measurement was made", - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="oser_measurements", - to="references.Reference", - verbose_name="Measurement Reference", - ), - ), - ( - "updated_by", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="osermeasurement_modifier", - to=settings.AUTH_USER_MODEL, - ), - ), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('percent', models.FloatField(blank=True, help_text='Photobleaching half-life (s)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)], verbose_name='Percent Normal Cells')), + ('percent_stddev', models.FloatField(blank=True, help_text='Standard deviation of percent normal cells (if applicable)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)], verbose_name='StdDev')), + ('percent_ncells', models.IntegerField(blank=True, help_text='Number of cells analyzed in percent normal for this FP', null=True, verbose_name='Number of cells for percent measurement')), + ('oserne', models.FloatField(blank=True, help_text='Ratio of OSER to nuclear envelope (NE) fluorescence intensities', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)], verbose_name='OSER/NE ratio')), + ('oserne_stddev', models.FloatField(blank=True, help_text='Standard deviation of OSER/NE ratio (if applicable)', null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)], verbose_name='OSER/NE StdDev')), + ('oserne_ncells', models.IntegerField(blank=True, help_text='Number of cells analyzed in OSER/NE this FP', null=True, verbose_name='Number of cells for OSER/NE measurement')), + ('celltype', models.CharField(blank=True, help_text='e.g. COS-7, HeLa', max_length=64, verbose_name='Cell Type')), + ('temp', models.FloatField(blank=True, null=True, verbose_name='Temperature')), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='osermeasurement_author', to=settings.AUTH_USER_MODEL)), + ('protein', models.ForeignKey(help_text='The protein on which this measurement was made', on_delete=django.db.models.deletion.CASCADE, related_name='oser_measurements', to='proteins.Protein', verbose_name='Protein')), + ('reference', models.ForeignKey(blank=True, help_text='Reference where the measurement was made', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='oser_measurements', to='references.Reference', verbose_name='Measurement Reference')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='osermeasurement_modifier', to=settings.AUTH_USER_MODEL)), ], options={ - "abstract": False, + 'abstract': False, }, ), ] diff --git a/backend/proteins/migrations/0023_spectrum_reference.py b/backend/proteins/migrations/0023_spectrum_reference.py index 730b7926d..a8cfc1f9f 100644 --- a/backend/proteins/migrations/0023_spectrum_reference.py +++ b/backend/proteins/migrations/0023_spectrum_reference.py @@ -1,25 +1,20 @@ # Generated by Django 2.0.9 on 2018-10-09 19:58 -import django.db.models.deletion from django.db import migrations, models +import django.db.models.deletion class Migration(migrations.Migration): + dependencies = [ - ("references", "0003_auto_20180804_0203"), - ("proteins", "0022_osermeasurement"), + ('references', '0003_auto_20180804_0203'), + ('proteins', '0022_osermeasurement'), ] operations = [ migrations.AddField( - model_name="spectrum", - name="reference", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="spectra", - to="references.Reference", - ), + model_name='spectrum', + name='reference', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='spectra', to='references.Reference'), ), ] diff --git a/backend/proteins/migrations/0024_auto_20181011_1659.py b/backend/proteins/migrations/0024_auto_20181011_1659.py index 4efbc378f..da868bf20 100644 --- a/backend/proteins/migrations/0024_auto_20181011_1659.py +++ b/backend/proteins/migrations/0024_auto_20181011_1659.py @@ -4,19 +4,20 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0023_spectrum_reference"), + ('proteins', '0023_spectrum_reference'), ] operations = [ migrations.AlterField( - model_name="bleachmeasurement", - name="temp", - field=models.FloatField(blank=True, null=True, verbose_name="Temp (˚C)"), + model_name='bleachmeasurement', + name='temp', + field=models.FloatField(blank=True, null=True, verbose_name='Temp (˚C)'), ), migrations.AlterField( - model_name="bleachmeasurement", - name="units", - field=models.CharField(blank=True, help_text="e.g. W/cm2", max_length=100, verbose_name="Power Units"), + model_name='bleachmeasurement', + name='units', + field=models.CharField(blank=True, help_text='e.g. W/cm2', max_length=100, verbose_name='Power Units'), ), ] diff --git a/backend/proteins/migrations/0025_auto_20181011_1715.py b/backend/proteins/migrations/0025_auto_20181011_1715.py index c383d19fe..d657684ef 100644 --- a/backend/proteins/migrations/0025_auto_20181011_1715.py +++ b/backend/proteins/migrations/0025_auto_20181011_1715.py @@ -5,37 +5,20 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0024_auto_20181011_1659"), + ('proteins', '0024_auto_20181011_1659'), ] operations = [ migrations.AddField( - model_name="bleachmeasurement", - name="bandcenter", - field=models.PositiveSmallIntegerField( - blank=True, - help_text="Band center of excitation light filter", - null=True, - validators=[ - django.core.validators.MinValueValidator(200), - django.core.validators.MaxValueValidator(1600), - ], - verbose_name="Band Center (nm)", - ), + model_name='bleachmeasurement', + name='bandcenter', + field=models.PositiveSmallIntegerField(blank=True, help_text='Band center of excitation light filter', null=True, validators=[django.core.validators.MinValueValidator(200), django.core.validators.MaxValueValidator(1600)], verbose_name='Band Center (nm)'), ), migrations.AddField( - model_name="bleachmeasurement", - name="bandwidth", - field=models.PositiveSmallIntegerField( - blank=True, - help_text="Bandwidth of excitation light filter", - null=True, - validators=[ - django.core.validators.MinValueValidator(1), - django.core.validators.MaxValueValidator(1000), - ], - verbose_name="Bandwidth (nm)", - ), + model_name='bleachmeasurement', + name='bandwidth', + field=models.PositiveSmallIntegerField(blank=True, help_text='Bandwidth of excitation light filter', null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(1000)], verbose_name='Bandwidth (nm)'), ), ] diff --git a/backend/proteins/migrations/0026_bleachmeasurement_cell_type.py b/backend/proteins/migrations/0026_bleachmeasurement_cell_type.py index b599fb69c..9b127517b 100644 --- a/backend/proteins/migrations/0026_bleachmeasurement_cell_type.py +++ b/backend/proteins/migrations/0026_bleachmeasurement_cell_type.py @@ -4,14 +4,15 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0025_auto_20181011_1715"), + ('proteins', '0025_auto_20181011_1715'), ] operations = [ migrations.AddField( - model_name="bleachmeasurement", - name="cell_type", - field=models.CharField(blank=True, help_text="e.g. HeLa", max_length=60, verbose_name="Cell Type"), + model_name='bleachmeasurement', + name='cell_type', + field=models.CharField(blank=True, help_text='e.g. HeLa', max_length=60, verbose_name='Cell Type'), ), ] diff --git a/backend/proteins/migrations/0027_auto_20181011_1754.py b/backend/proteins/migrations/0027_auto_20181011_1754.py index 83edc1016..31062389f 100644 --- a/backend/proteins/migrations/0027_auto_20181011_1754.py +++ b/backend/proteins/migrations/0027_auto_20181011_1754.py @@ -4,14 +4,15 @@ class Migration(migrations.Migration): + dependencies = [ - ("references", "0003_auto_20180804_0203"), - ("proteins", "0026_bleachmeasurement_cell_type"), + ('references', '0003_auto_20180804_0203'), + ('proteins', '0026_bleachmeasurement_cell_type'), ] operations = [ migrations.AlterUniqueTogether( - name="osermeasurement", - unique_together={("protein", "reference")}, + name='osermeasurement', + unique_together={('protein', 'reference')}, ), ] diff --git a/backend/proteins/migrations/0028_auto_20181012_2011.py b/backend/proteins/migrations/0028_auto_20181012_2011.py index 822bc1e42..4b6f58267 100644 --- a/backend/proteins/migrations/0028_auto_20181012_2011.py +++ b/backend/proteins/migrations/0028_auto_20181012_2011.py @@ -1,100 +1,54 @@ # Generated by Django 2.0.9 on 2018-10-12 20:11 +from django.conf import settings +from django.db import migrations, models import django.db.models.deletion import django.utils.timezone import model_utils.fields -from django.conf import settings -from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("references", "0003_auto_20180804_0203"), - ("proteins", "0027_auto_20181011_1754"), + ('references', '0003_auto_20180804_0203'), + ('proteins', '0027_auto_20181011_1754'), ] operations = [ migrations.CreateModel( - name="Excerpt", + name='Excerpt', fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), - ), - ( - "status", - model_utils.fields.StatusField( - choices=[("approved", "approved"), ("flagged", "flagged"), ("rejected", "rejected")], - default="approved", - max_length=100, - no_check_for_status=True, - verbose_name="status", - ), - ), - ( - "status_changed", - model_utils.fields.MonitorField( - default=django.utils.timezone.now, monitor="status", verbose_name="status changed" - ), - ), - ("content", models.TextField(help_text="Brief excerpt describing this protein", max_length=1024)), - ( - "created_by", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="excerpt_author", - to=settings.AUTH_USER_MODEL, - ), - ), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('status', model_utils.fields.StatusField(choices=[('approved', 'approved'), ('flagged', 'flagged'), ('rejected', 'rejected')], default='approved', max_length=100, no_check_for_status=True, verbose_name='status')), + ('status_changed', model_utils.fields.MonitorField(default=django.utils.timezone.now, monitor='status', verbose_name='status changed')), + ('content', models.TextField(help_text='Brief excerpt describing this protein', max_length=1024)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='excerpt_author', to=settings.AUTH_USER_MODEL)), ], options={ - "abstract": False, + 'abstract': False, }, ), migrations.AlterField( - model_name="protein", - name="blurb", - field=models.TextField(blank=True, help_text="Brief descriptive blurb", max_length=512), + model_name='protein', + name='blurb', + field=models.TextField(blank=True, help_text='Brief descriptive blurb', max_length=512), ), migrations.AddField( - model_name="excerpt", - name="protein", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, related_name="excerpts", to="proteins.Protein" - ), + model_name='excerpt', + name='protein', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='excerpts', to='proteins.Protein'), ), migrations.AddField( - model_name="excerpt", - name="reference", - field=models.ForeignKey( - help_text="Source of this excerpt", - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="excerpt", - to="references.Reference", - ), + model_name='excerpt', + name='reference', + field=models.ForeignKey(help_text='Source of this excerpt', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='excerpt', to='references.Reference'), ), migrations.AddField( - model_name="excerpt", - name="updated_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="excerpt_modifier", - to=settings.AUTH_USER_MODEL, - ), + model_name='excerpt', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='excerpt_modifier', to=settings.AUTH_USER_MODEL), ), ] diff --git a/backend/proteins/migrations/0029_auto_20181014_1241.py b/backend/proteins/migrations/0029_auto_20181014_1241.py index b167b1966..a4846c7cf 100644 --- a/backend/proteins/migrations/0029_auto_20181014_1241.py +++ b/backend/proteins/migrations/0029_auto_20181014_1241.py @@ -4,20 +4,19 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0028_auto_20181012_2011"), + ('proteins', '0028_auto_20181012_2011'), ] operations = [ migrations.AlterModelOptions( - name="excerpt", - options={"ordering": ["reference__year", "created"]}, + name='excerpt', + options={'ordering': ['reference__year', 'created']}, ), migrations.AddField( - model_name="protein", - name="seq_comment", - field=models.CharField( - blank=True, help_text="if necessary, comment on source of sequence", max_length=512 - ), + model_name='protein', + name='seq_comment', + field=models.CharField(blank=True, help_text='if necessary, comment on source of sequence', max_length=512), ), ] diff --git a/backend/proteins/migrations/0030_lineage.py b/backend/proteins/migrations/0030_lineage.py index be475d816..98496cc5e 100644 --- a/backend/proteins/migrations/0030_lineage.py +++ b/backend/proteins/migrations/0030_lineage.py @@ -1,69 +1,37 @@ # Generated by Django 2.1.2 on 2018-10-28 22:20 +from django.db import migrations, models import django.db.models.deletion import django.utils.timezone import model_utils.fields import mptt.fields -from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [ - ("references", "0004_auto_20181026_1547"), - ("proteins", "0029_auto_20181014_1241"), + ('references', '0004_auto_20181026_1547'), + ('proteins', '0029_auto_20181014_1241'), ] operations = [ migrations.CreateModel( - name="Lineage", + name='Lineage', fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), - ), - ("mutation", models.CharField(blank=True, max_length=400)), - ("lft", models.PositiveIntegerField(db_index=True, editable=False)), - ("rght", models.PositiveIntegerField(db_index=True, editable=False)), - ("tree_id", models.PositiveIntegerField(db_index=True, editable=False)), - ("level", models.PositiveIntegerField(db_index=True, editable=False)), - ( - "parent", - mptt.fields.TreeForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="children", - to="proteins.Lineage", - ), - ), - ( - "protein", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, related_name="lineage", to="proteins.Protein" - ), - ), - ( - "reference", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="lineages", - to="references.Reference", - ), - ), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('mutation', models.CharField(blank=True, max_length=400)), + ('lft', models.PositiveIntegerField(db_index=True, editable=False)), + ('rght', models.PositiveIntegerField(db_index=True, editable=False)), + ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)), + ('level', models.PositiveIntegerField(db_index=True, editable=False)), + ('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='proteins.Lineage')), + ('protein', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='lineage', to='proteins.Protein')), + ('reference', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='lineages', to='references.Reference')), ], options={ - "abstract": False, + 'abstract': False, }, ), ] diff --git a/backend/proteins/migrations/0031_auto_20181103_1531.py b/backend/proteins/migrations/0031_auto_20181103_1531.py index 3a16e9640..77707cc93 100644 --- a/backend/proteins/migrations/0031_auto_20181103_1531.py +++ b/backend/proteins/migrations/0031_auto_20181103_1531.py @@ -4,14 +4,15 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0030_lineage"), + ('proteins', '0030_lineage'), ] operations = [ migrations.AlterField( - model_name="excerpt", - name="content", - field=models.TextField(help_text="Brief excerpt describing this protein", max_length=1200), + model_name='excerpt', + name='content', + field=models.TextField(help_text='Brief excerpt describing this protein', max_length=1200), ), ] diff --git a/backend/proteins/migrations/0032_auto_20181107_2015.py b/backend/proteins/migrations/0032_auto_20181107_2015.py index ccf7e3e3f..4d53aae1f 100644 --- a/backend/proteins/migrations/0032_auto_20181107_2015.py +++ b/backend/proteins/migrations/0032_auto_20181107_2015.py @@ -2,29 +2,25 @@ import django.contrib.postgres.fields from django.db import migrations, models - import proteins.models.lineage import proteins.validators class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0031_auto_20181103_1531"), + ('proteins', '0031_auto_20181103_1531'), ] operations = [ migrations.AlterField( - model_name="lineage", - name="mutation", + model_name='lineage', + name='mutation', field=proteins.models.lineage.MutationSetField(blank=True, max_length=400), ), migrations.AlterField( - model_name="mutation", - name="mutations", - field=django.contrib.postgres.fields.ArrayField( - base_field=models.CharField(max_length=5), - size=None, - validators=[proteins.validators.validate_mutation], - ), + model_name='mutation', + name='mutations', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=5), size=None, validators=[proteins.validators.validate_mutation]), ), ] diff --git a/backend/proteins/migrations/0033_auto_20181107_2119.py b/backend/proteins/migrations/0033_auto_20181107_2119.py index 9405ab3f8..1a5486f0a 100644 --- a/backend/proteins/migrations/0033_auto_20181107_2119.py +++ b/backend/proteins/migrations/0033_auto_20181107_2119.py @@ -1,37 +1,26 @@ # Generated by Django 2.1.2 on 2018-11-07 21:19 -import django.db.models.deletion from django.conf import settings from django.db import migrations, models +import django.db.models.deletion class Migration(migrations.Migration): + dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("proteins", "0032_auto_20181107_2015"), + ('proteins', '0032_auto_20181107_2015'), ] operations = [ migrations.AddField( - model_name="lineage", - name="created_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="lineage_author", - to=settings.AUTH_USER_MODEL, - ), + model_name='lineage', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='lineage_author', to=settings.AUTH_USER_MODEL), ), migrations.AddField( - model_name="lineage", - name="updated_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="lineage_modifier", - to=settings.AUTH_USER_MODEL, - ), + model_name='lineage', + name='updated_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='lineage_modifier', to=settings.AUTH_USER_MODEL), ), ] diff --git a/backend/proteins/migrations/0034_lineage_rootmut.py b/backend/proteins/migrations/0034_lineage_rootmut.py index 101c120bc..15202887f 100644 --- a/backend/proteins/migrations/0034_lineage_rootmut.py +++ b/backend/proteins/migrations/0034_lineage_rootmut.py @@ -4,14 +4,15 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0033_auto_20181107_2119"), + ('proteins', '0033_auto_20181107_2119'), ] operations = [ migrations.AddField( - model_name="lineage", - name="rootmut", + model_name='lineage', + name='rootmut', field=models.CharField(blank=True, max_length=400), ), ] diff --git a/backend/proteins/migrations/0035_auto_20181110_0103.py b/backend/proteins/migrations/0035_auto_20181110_0103.py index d9c8c7dfb..2c3421e68 100644 --- a/backend/proteins/migrations/0035_auto_20181110_0103.py +++ b/backend/proteins/migrations/0035_auto_20181110_0103.py @@ -4,16 +4,17 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0034_lineage_rootmut"), + ('proteins', '0034_lineage_rootmut'), ] operations = [ migrations.RemoveField( - model_name="mutation", - name="parent", + model_name='mutation', + name='parent', ), migrations.DeleteModel( - name="Mutation", + name='Mutation', ), ] diff --git a/backend/proteins/migrations/0036_lineage_root_node.py b/backend/proteins/migrations/0036_lineage_root_node.py index e097b0536..6fa7951d1 100644 --- a/backend/proteins/migrations/0036_lineage_root_node.py +++ b/backend/proteins/migrations/0036_lineage_root_node.py @@ -1,24 +1,19 @@ # Generated by Django 2.1.2 on 2018-12-02 15:58 -import django.db.models.deletion from django.db import migrations, models +import django.db.models.deletion class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0035_auto_20181110_0103"), + ('proteins', '0035_auto_20181110_0103'), ] operations = [ migrations.AddField( - model_name="lineage", - name="root_node", - field=models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="descendants", - to="proteins.Lineage", - verbose_name="Root Node", - ), + model_name='lineage', + name='root_node', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='descendants', to='proteins.Lineage', verbose_name='Root Node'), ), ] diff --git a/backend/proteins/migrations/0037_auto_20181205_2035.py b/backend/proteins/migrations/0037_auto_20181205_2035.py index b6aece3df..5fb290afb 100644 --- a/backend/proteins/migrations/0037_auto_20181205_2035.py +++ b/backend/proteins/migrations/0037_auto_20181205_2035.py @@ -4,19 +4,15 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0036_lineage_root_node"), + ('proteins', '0036_lineage_root_node'), ] operations = [ migrations.AlterField( - model_name="protein", - name="cofactor", - field=models.CharField( - blank=True, - choices=[("bv", "Biliverdin"), ("fl", "Flavin"), ("pcb", "Phycocyanobilin")], - help_text="Required for fluorescence", - max_length=2, - ), + model_name='protein', + name='cofactor', + field=models.CharField(blank=True, choices=[('bv', 'Biliverdin'), ('fl', 'Flavin'), ('pcb', 'Phycocyanobilin')], help_text='Required for fluorescence', max_length=2), ), ] diff --git a/backend/proteins/migrations/0038_auto_20181205_2044.py b/backend/proteins/migrations/0038_auto_20181205_2044.py index 87a91caa4..bf05e5e42 100644 --- a/backend/proteins/migrations/0038_auto_20181205_2044.py +++ b/backend/proteins/migrations/0038_auto_20181205_2044.py @@ -4,19 +4,15 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0037_auto_20181205_2035"), + ('proteins', '0037_auto_20181205_2035'), ] operations = [ migrations.AlterField( - model_name="protein", - name="cofactor", - field=models.CharField( - blank=True, - choices=[("bv", "Biliverdin"), ("fl", "Flavin"), ("pc", "Phycocyanobilin")], - help_text="Required for fluorescence", - max_length=2, - ), + model_name='protein', + name='cofactor', + field=models.CharField(blank=True, choices=[('bv', 'Biliverdin'), ('fl', 'Flavin'), ('pc', 'Phycocyanobilin')], help_text='Required for fluorescence', max_length=2), ), ] diff --git a/backend/proteins/migrations/0039_auto_20181206_0009.py b/backend/proteins/migrations/0039_auto_20181206_0009.py index 8bac7ee42..6ff3f646a 100644 --- a/backend/proteins/migrations/0039_auto_20181206_0009.py +++ b/backend/proteins/migrations/0039_auto_20181206_0009.py @@ -4,29 +4,30 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0038_auto_20181205_2044"), + ('proteins', '0038_auto_20181205_2044'), ] operations = [ migrations.AddField( - model_name="dye", - name="emhex", + model_name='dye', + name='emhex', field=models.CharField(blank=True, max_length=7), ), migrations.AddField( - model_name="dye", - name="exhex", + model_name='dye', + name='exhex', field=models.CharField(blank=True, max_length=7), ), migrations.AddField( - model_name="state", - name="emhex", + model_name='state', + name='emhex', field=models.CharField(blank=True, max_length=7), ), migrations.AddField( - model_name="state", - name="exhex", + model_name='state', + name='exhex', field=models.CharField(blank=True, max_length=7), ), ] diff --git a/backend/proteins/migrations/0040_auto_20181210_0345.py b/backend/proteins/migrations/0040_auto_20181210_0345.py index 06368193a..e6b62ea01 100644 --- a/backend/proteins/migrations/0040_auto_20181210_0345.py +++ b/backend/proteins/migrations/0040_auto_20181210_0345.py @@ -4,19 +4,15 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0039_auto_20181206_0009"), + ('proteins', '0039_auto_20181206_0009'), ] operations = [ migrations.AlterField( - model_name="protein", - name="cofactor", - field=models.CharField( - blank=True, - choices=[("br", "Bilirubin"), ("bv", "Biliverdin"), ("fl", "Flavin"), ("pc", "Phycocyanobilin")], - help_text="Required for fluorescence", - max_length=2, - ), + model_name='protein', + name='cofactor', + field=models.CharField(blank=True, choices=[('br', 'Bilirubin'), ('bv', 'Biliverdin'), ('fl', 'Flavin'), ('pc', 'Phycocyanobilin')], help_text='Required for fluorescence', max_length=2), ), ] diff --git a/backend/proteins/migrations/0041_auto_20181216_1743.py b/backend/proteins/migrations/0041_auto_20181216_1743.py index 3dda36783..d2b42fc39 100644 --- a/backend/proteins/migrations/0041_auto_20181216_1743.py +++ b/backend/proteins/migrations/0041_auto_20181216_1743.py @@ -1,36 +1,29 @@ # Generated by Django 2.1.2 on 2018-12-16 17:43 -import django.db.models.deletion from django.db import migrations, models +import django.db.models.deletion class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0040_auto_20181210_0345"), + ('proteins', '0040_auto_20181210_0345'), ] operations = [ migrations.AddField( - model_name="excerpt", - name="proteins", - field=models.ManyToManyField(related_name="excerpts", to="proteins.Protein"), + model_name='excerpt', + name='proteins', + field=models.ManyToManyField(related_name='excerpts', to='proteins.Protein'), ), migrations.AlterField( - model_name="excerpt", - name="protein", - field=models.ForeignKey( - null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="excerpt", to="proteins.Protein" - ), + model_name='excerpt', + name='protein', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='excerpt', to='proteins.Protein'), ), migrations.AlterField( - model_name="excerpt", - name="reference", - field=models.ForeignKey( - help_text="Source of this excerpt", - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="excerpts", - to="references.Reference", - ), + model_name='excerpt', + name='reference', + field=models.ForeignKey(help_text='Source of this excerpt', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='excerpts', to='references.Reference'), ), ] diff --git a/backend/proteins/migrations/0042_auto_20181216_1744.py b/backend/proteins/migrations/0042_auto_20181216_1744.py index f76559150..44df33ea2 100644 --- a/backend/proteins/migrations/0042_auto_20181216_1744.py +++ b/backend/proteins/migrations/0042_auto_20181216_1744.py @@ -5,18 +5,19 @@ def make_many_exerpts(apps, schema_editor): """ - Adds the Author object in Book.author to the - many-to-many relationship in Book.authors + Adds the Author object in Book.author to the + many-to-many relationship in Book.authors """ - Excerpt = apps.get_model("proteins", "Excerpt") + Excerpt = apps.get_model('proteins', 'Excerpt') for excerpt in Excerpt.objects.all(): excerpt.proteins.add(excerpt.protein) class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0041_auto_20181216_1743"), + ('proteins', '0041_auto_20181216_1743'), ] operations = [ diff --git a/backend/proteins/migrations/0043_remove_excerpt_protein.py b/backend/proteins/migrations/0043_remove_excerpt_protein.py index 2c234950e..527f7ca0b 100644 --- a/backend/proteins/migrations/0043_remove_excerpt_protein.py +++ b/backend/proteins/migrations/0043_remove_excerpt_protein.py @@ -4,13 +4,14 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0042_auto_20181216_1744"), + ('proteins', '0042_auto_20181216_1744'), ] operations = [ migrations.RemoveField( - model_name="excerpt", - name="protein", + model_name='excerpt', + name='protein', ), ] diff --git a/backend/proteins/migrations/0044_auto_20181218_1310.py b/backend/proteins/migrations/0044_auto_20181218_1310.py index 5d2ccc026..91a731b57 100644 --- a/backend/proteins/migrations/0044_auto_20181218_1310.py +++ b/backend/proteins/migrations/0044_auto_20181218_1310.py @@ -4,14 +4,15 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0043_remove_excerpt_protein"), + ('proteins', '0043_remove_excerpt_protein'), ] operations = [ migrations.AlterField( - model_name="excerpt", - name="proteins", - field=models.ManyToManyField(blank=True, related_name="excerpts", to="proteins.Protein"), + model_name='excerpt', + name='proteins', + field=models.ManyToManyField(blank=True, related_name='excerpts', to='proteins.Protein'), ), ] diff --git a/backend/proteins/migrations/0045_dye_is_dark.py b/backend/proteins/migrations/0045_dye_is_dark.py index f5f5f069a..72ce667ae 100644 --- a/backend/proteins/migrations/0045_dye_is_dark.py +++ b/backend/proteins/migrations/0045_dye_is_dark.py @@ -4,16 +4,15 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0044_auto_20181218_1310"), + ('proteins', '0044_auto_20181218_1310'), ] operations = [ migrations.AddField( - model_name="dye", - name="is_dark", - field=models.BooleanField( - default=False, help_text="This state does not fluorescence", verbose_name="Dark State" - ), + model_name='dye', + name='is_dark', + field=models.BooleanField(default=False, help_text='This state does not fluorescence', verbose_name='Dark State'), ), ] diff --git a/backend/proteins/migrations/0046_auto_20190121_1341.py b/backend/proteins/migrations/0046_auto_20190121_1341.py index 12d8af8cb..7a8978c88 100644 --- a/backend/proteins/migrations/0046_auto_20190121_1341.py +++ b/backend/proteins/migrations/0046_auto_20190121_1341.py @@ -5,40 +5,20 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0045_dye_is_dark"), + ('proteins', '0045_dye_is_dark'), ] operations = [ migrations.AlterField( - model_name="protein", - name="pdb", - field=django.contrib.postgres.fields.ArrayField( - base_field=models.CharField(max_length=4), - blank=True, - null=True, - size=None, - verbose_name="Protein DataBank IDs", - ), + model_name='protein', + name='pdb', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=4), blank=True, null=True, size=None, verbose_name='Protein DataBank IDs'), ), migrations.AlterField( - model_name="protein", - name="switch_type", - field=models.CharField( - blank=True, - choices=[ - ("b", "Basic"), - ("pa", "Photoactivatable"), - ("ps", "Photoswitchable"), - ("pc", "Photoconvertible"), - ("mp", "Multi-photochromic"), - ("o", "Multistate"), - ("t", "Timer"), - ], - default="b", - help_text="Photoswitching type (basic if none)", - max_length=2, - verbose_name="Switching Type", - ), + model_name='protein', + name='switch_type', + field=models.CharField(blank=True, choices=[('b', 'Basic'), ('pa', 'Photoactivatable'), ('ps', 'Photoswitchable'), ('pc', 'Photoconvertible'), ('mp', 'Multi-photochromic'), ('o', 'Multistate'), ('t', 'Timer')], default='b', help_text='Photoswitching type (basic if none)', max_length=2, verbose_name='Switching Type'), ), ] diff --git a/backend/proteins/migrations/0047_auto_20190319_1525.py b/backend/proteins/migrations/0047_auto_20190319_1525.py index 7f75074a8..a3b006738 100644 --- a/backend/proteins/migrations/0047_auto_20190319_1525.py +++ b/backend/proteins/migrations/0047_auto_20190319_1525.py @@ -1,91 +1,38 @@ # Generated by Django 2.1.7 on 2019-03-19 15:25 import django.core.validators +from django.db import migrations, models import django.db.models.deletion import django.utils.timezone import model_utils.fields -from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [ - ("contenttypes", "0002_remove_content_type_name"), - ("proteins", "0046_auto_20190121_1341"), + ('contenttypes', '0002_remove_content_type_name'), + ('proteins', '0046_auto_20190121_1341'), ] operations = [ migrations.CreateModel( - name="OcFluorEff", + name='OcFluorEff', fields=[ - ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), - ), - ("object_id", models.PositiveIntegerField()), - ("fluor_name", models.CharField(blank=True, max_length=100)), - ( - "ex_eff", - models.FloatField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(1), - ], - verbose_name="Excitation Efficiency", - ), - ), - ( - "ex_eff_broad", - models.FloatField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(1), - ], - verbose_name="Excitation Efficiency (Broadband)", - ), - ), - ( - "em_eff", - models.FloatField( - blank=True, - null=True, - validators=[ - django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(1), - ], - verbose_name="Emission Efficiency", - ), - ), - ("brightness", models.FloatField(blank=True, null=True)), - ( - "content_type", - models.ForeignKey( - limit_choices_to=models.Q( - models.Q(("app_label", "proteins"), ("model", "state")), - models.Q(("app_label", "proteins"), ("model", "dye")), - _connector="OR", - ), - on_delete=django.db.models.deletion.CASCADE, - to="contenttypes.ContentType", - ), - ), - ("oc", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="proteins.OpticalConfig")), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('object_id', models.PositiveIntegerField()), + ('fluor_name', models.CharField(blank=True, max_length=100)), + ('ex_eff', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='Excitation Efficiency')), + ('ex_eff_broad', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='Excitation Efficiency (Broadband)')), + ('em_eff', models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(1)], verbose_name='Emission Efficiency')), + ('brightness', models.FloatField(blank=True, null=True)), + ('content_type', models.ForeignKey(limit_choices_to=models.Q(models.Q(('app_label', 'proteins'), ('model', 'state')), models.Q(('app_label', 'proteins'), ('model', 'dye')), _connector='OR'), on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ('oc', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='proteins.OpticalConfig')), ], ), migrations.AlterUniqueTogether( - name="ocfluoreff", - unique_together={("oc", "content_type", "object_id")}, + name='ocfluoreff', + unique_together={('oc', 'content_type', 'object_id')}, ), ] diff --git a/backend/proteins/migrations/0048_change_protein_uuid.py b/backend/proteins/migrations/0048_change_protein_uuid.py index 7a4eabc8e..7e754f0a6 100644 --- a/backend/proteins/migrations/0048_change_protein_uuid.py +++ b/backend/proteins/migrations/0048_change_protein_uuid.py @@ -1,10 +1,8 @@ # Generated by Django 2.1.7 on 2019-03-22 00:59 -import uuid as uuid_lib - from django.db import migrations, models - from proteins.models.protein import prot_uuid +import uuid as uuid_lib def forwards_func(apps, schema_editor): @@ -26,30 +24,31 @@ def stub(a, b): class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0047_auto_20190319_1525"), + ('proteins', '0047_auto_20190319_1525'), ] operations = [ migrations.AlterField( - model_name="protein", - name="uuid", + model_name='protein', + name='uuid', field=models.UUIDField(blank=True, null=True), ), migrations.RunPython(stub, reverse_func), migrations.RemoveField( - model_name="protein", - name="uuid", + model_name='protein', + name='uuid', ), migrations.AddField( - model_name="protein", - name="uuid", + model_name='protein', + name='uuid', field=models.CharField(blank=True, max_length=50, null=True), ), migrations.RunPython(forwards_func, stub), migrations.AlterField( - model_name="protein", - name="uuid", + model_name='protein', + name='uuid', field=models.CharField(db_index=True, default=prot_uuid, editable=False, max_length=5, unique=True), ), ] diff --git a/backend/proteins/migrations/0049_auto_20190323_1947.py b/backend/proteins/migrations/0049_auto_20190323_1947.py index 15531128e..81478f3c3 100644 --- a/backend/proteins/migrations/0049_auto_20190323_1947.py +++ b/backend/proteins/migrations/0049_auto_20190323_1947.py @@ -1,26 +1,19 @@ # Generated by Django 2.1.7 on 2019-03-23 19:47 from django.db import migrations, models - import proteins.models.protein class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0048_change_protein_uuid"), + ('proteins', '0048_change_protein_uuid'), ] operations = [ migrations.AlterField( - model_name="protein", - name="uuid", - field=models.CharField( - db_index=True, - default=proteins.models.protein.prot_uuid, - editable=False, - max_length=5, - unique=True, - verbose_name="FPbase ID", - ), + model_name='protein', + name='uuid', + field=models.CharField(db_index=True, default=proteins.models.protein.prot_uuid, editable=False, max_length=5, unique=True, verbose_name='FPbase ID'), ), ] diff --git a/backend/proteins/migrations/0050_auto_20190714_1318.py b/backend/proteins/migrations/0050_auto_20190714_1318.py index 873cb9da7..93b89aef5 100644 --- a/backend/proteins/migrations/0050_auto_20190714_1318.py +++ b/backend/proteins/migrations/0050_auto_20190714_1318.py @@ -5,13 +5,16 @@ class Migration(migrations.Migration): + dependencies = [("proteins", "0049_auto_20190323_1947")] operations = [ migrations.AddField( model_name="spectrum", name="source", - field=models.CharField(blank=True, help_text="Source of the spectra data", max_length=128), + field=models.CharField( + blank=True, help_text="Source of the spectra data", max_length=128 + ), ), migrations.AlterField( model_name="lineage", diff --git a/backend/proteins/migrations/0051_alter_bleachmeasurement_created_by_and_more.py b/backend/proteins/migrations/0051_alter_bleachmeasurement_created_by_and_more.py index 412d28b01..ed16f1246 100644 --- a/backend/proteins/migrations/0051_alter_bleachmeasurement_created_by_and_more.py +++ b/backend/proteins/migrations/0051_alter_bleachmeasurement_created_by_and_more.py @@ -1,8 +1,8 @@ # Generated by Django 4.2 on 2023-05-14 10:45 -import django.db.models.deletion from django.conf import settings from django.db import migrations, models +import django.db.models.deletion class Migration(migrations.Migration): @@ -26,7 +26,9 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="bleachmeasurement", name="id", - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), migrations.AlterField( model_name="bleachmeasurement", @@ -53,7 +55,9 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="camera", name="id", - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), migrations.AlterField( model_name="camera", @@ -80,7 +84,9 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="dye", name="id", - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), migrations.AlterField( model_name="dye", @@ -107,7 +113,9 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="excerpt", name="id", - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), migrations.AlterField( model_name="excerpt", @@ -134,7 +142,9 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="filter", name="id", - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), migrations.AlterField( model_name="filter", @@ -150,7 +160,9 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="filterplacement", name="id", - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), migrations.AlterField( model_name="filterplacement", @@ -168,7 +180,9 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="filterplacement", name="reflects", - field=models.BooleanField(default=False, help_text="Filter reflects emission (if BS or EM filter)"), + field=models.BooleanField( + default=False, help_text="Filter reflects emission (if BS or EM filter)" + ), ), migrations.AlterField( model_name="light", @@ -184,7 +198,9 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="light", name="id", - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), migrations.AlterField( model_name="light", @@ -211,7 +227,9 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="lineage", name="id", - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), migrations.AlterField( model_name="lineage", @@ -238,12 +256,16 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="ocfluoreff", name="id", - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), migrations.AlterField( model_name="opticalconfig", name="id", - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), migrations.AlterField( model_name="opticalconfig", @@ -292,7 +314,9 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="osermeasurement", name="id", - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), migrations.AlterField( model_name="osermeasurement", @@ -335,7 +359,9 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="protein", name="id", - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), migrations.AlterField( model_name="protein", @@ -351,7 +377,9 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="proteincollection", name="id", - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), migrations.AlterField( model_name="proteincollection", @@ -378,7 +406,9 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="spectrum", name="id", - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), migrations.AlterField( model_name="spectrum", @@ -405,7 +435,9 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="state", name="id", - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), migrations.AlterField( model_name="state", @@ -432,7 +464,9 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="statetransition", name="id", - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"), + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), migrations.AlterField( model_name="statetransition", diff --git a/backend/proteins/migrations/0053_alter_protein_chromophore.py b/backend/proteins/migrations/0053_alter_protein_chromophore.py index 47df7f938..0daaffc98 100644 --- a/backend/proteins/migrations/0053_alter_protein_chromophore.py +++ b/backend/proteins/migrations/0053_alter_protein_chromophore.py @@ -1,7 +1,6 @@ # Generated by Django 4.2.1 on 2023-06-11 16:00 from django.db import migrations - import proteins.models.protein diff --git a/backend/proteins/migrations/0055_spectrum_status_spectrum_status_changed.py b/backend/proteins/migrations/0055_spectrum_status_spectrum_status_changed.py index 7d88a863e..3eb4015a6 100644 --- a/backend/proteins/migrations/0055_spectrum_status_spectrum_status_changed.py +++ b/backend/proteins/migrations/0055_spectrum_status_spectrum_status_changed.py @@ -1,11 +1,12 @@ # Generated by Django 4.2.1 on 2024-10-01 16:26 +from django.db import migrations import django.utils.timezone import model_utils.fields -from django.db import migrations class Migration(migrations.Migration): + dependencies = [ ("proteins", "0054_microscope_cfg_calc_efficiency_and_more"), ] diff --git a/backend/proteins/migrations/0056_spectrum_spectrum_state_status_idx_and_more.py b/backend/proteins/migrations/0056_spectrum_spectrum_state_status_idx_and_more.py index e089af41c..bbbab8532 100644 --- a/backend/proteins/migrations/0056_spectrum_spectrum_state_status_idx_and_more.py +++ b/backend/proteins/migrations/0056_spectrum_spectrum_state_status_idx_and_more.py @@ -5,19 +5,20 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0055_spectrum_status_spectrum_status_changed"), - ("references", "0008_alter_reference_year"), + ('proteins', '0055_spectrum_status_spectrum_status_changed'), + ('references', '0008_alter_reference_year'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.AddIndex( - model_name="spectrum", - index=models.Index(fields=["owner_state_id", "status"], name="spectrum_state_status_idx"), + model_name='spectrum', + index=models.Index(fields=['owner_state_id', 'status'], name='spectrum_state_status_idx'), ), migrations.AddIndex( - model_name="spectrum", - index=models.Index(fields=["status"], name="spectrum_status_idx"), + model_name='spectrum', + index=models.Index(fields=['status'], name='spectrum_status_idx'), ), ] diff --git a/backend/proteins/migrations/0057_add_status_index.py b/backend/proteins/migrations/0057_add_status_index.py index b71eb0407..aa8ed3d53 100644 --- a/backend/proteins/migrations/0057_add_status_index.py +++ b/backend/proteins/migrations/0057_add_status_index.py @@ -5,15 +5,16 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0056_spectrum_spectrum_state_status_idx_and_more"), - ("references", "0008_alter_reference_year"), + ('proteins', '0056_spectrum_spectrum_state_status_idx_and_more'), + ('references', '0008_alter_reference_year'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.AddIndex( - model_name="protein", - index=models.Index(fields=["status"], name="protein_status_idx"), + model_name='protein', + index=models.Index(fields=['status'], name='protein_status_idx'), ), ] diff --git a/backend/proteins/migrations/0058_snapgeneplasmid_protein_snapgene_plasmids.py b/backend/proteins/migrations/0058_snapgeneplasmid_protein_snapgene_plasmids.py index 68d2d0520..8deaec527 100644 --- a/backend/proteins/migrations/0058_snapgeneplasmid_protein_snapgene_plasmids.py +++ b/backend/proteins/migrations/0058_snapgeneplasmid_protein_snapgene_plasmids.py @@ -4,36 +4,32 @@ class Migration(migrations.Migration): + dependencies = [ - ("proteins", "0057_add_status_index"), + ('proteins', '0057_add_status_index'), ] operations = [ migrations.CreateModel( - name="SnapGenePlasmid", + name='SnapGenePlasmid', fields=[ - ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ("plasmid_id", models.CharField(db_index=True, max_length=100, unique=True)), - ("name", models.CharField(max_length=200)), - ("description", models.TextField(blank=True)), - ("author", models.CharField(blank=True, max_length=200)), - ("size", models.IntegerField(blank=True, help_text="Size in base pairs", null=True)), - ("topology", models.CharField(blank=True, max_length=50)), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('plasmid_id', models.CharField(db_index=True, max_length=100, unique=True)), + ('name', models.CharField(max_length=200)), + ('description', models.TextField(blank=True)), + ('author', models.CharField(blank=True, max_length=200)), + ('size', models.IntegerField(blank=True, help_text='Size in base pairs', null=True)), + ('topology', models.CharField(blank=True, max_length=50)), ], options={ - "verbose_name": "SnapGene Plasmid", - "verbose_name_plural": "SnapGene Plasmids", - "ordering": ["name"], + 'verbose_name': 'SnapGene Plasmid', + 'verbose_name_plural': 'SnapGene Plasmids', + 'ordering': ['name'], }, ), migrations.AddField( - model_name="protein", - name="snapgene_plasmids", - field=models.ManyToManyField( - blank=True, - help_text="Associated SnapGene plasmids", - related_name="proteins", - to="proteins.snapgeneplasmid", - ), + model_name='protein', + name='snapgene_plasmids', + field=models.ManyToManyField(blank=True, help_text='Associated SnapGene plasmids', related_name='proteins', to='proteins.snapgeneplasmid'), ), ] From f17ecfed35904d2912535a266966bed57b2ec6bd Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Mon, 24 Nov 2025 14:17:29 -0500 Subject: [PATCH 27/57] remove notes --- .pre-commit-config.yaml | 2 +- backend/proteins/migrations/0059_notes.md | 321 ---------------------- 2 files changed, 1 insertion(+), 322 deletions(-) delete mode 100644 backend/proteins/migrations/0059_notes.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 04aded80f..5501c63ab 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ ci: skip: [] submodules: false -exclude: "^docs/|/migrations/|/migrations_old/" +exclude: "^docs/|/migrations/" default_stages: [pre-commit] repos: diff --git a/backend/proteins/migrations/0059_notes.md b/backend/proteins/migrations/0059_notes.md deleted file mode 100644 index dd02c4cc3..000000000 --- a/backend/proteins/migrations/0059_notes.md +++ /dev/null @@ -1,321 +0,0 @@ -# Schema Overhaul Migration: Full History Preservation (No Squashing) - -## Overview - -This documents the successful migration from the old schema (separate State/Dye models with fluorescence properties) to the new schema (Fluorophore MTI parent with State/DyeState children, Dye containers, and FluorescenceMeasurement tracking). - -All 58 old migrations were preserved, with 3 new migrations added to transform the schema while maintaining the ability to migrate any historical backup. - -## File Structure - -**Final Structure:** - -``` -migrations/ -├── 0001_initial.py through 0058_*.py (preserved from old schema) -├── 0059_add_fluorophore_and_new_models.py (schema transformation) -├── 0060_migrate_data_from_old_schema.py (data migration) -├── 0061_cleanup_old_schema.py (cleanup) -``` - -## Migration Sequence - -### Migration 0059: Add Fluorophore and New Models - -**Purpose:** Create complete new schema in one step while preserving old tables for data migration. - -**Approach:** Used `SeparateDatabaseAndState` to handle the State/Dye table transitions cleanly: -- **Database operations:** Copy old tables to `*_old` versions, then drop originals -- **State operations:** Delete old models from Django's migration state - -**Models Created:** - -1. **Fluorophore** (MTI parent): - - Fields: label, slug (max_length=100), entity_type, ex_max, em_max, qy, brightness, etc. - - Slug increased to 100 chars to accommodate long dye names (e.g., "fluospheres-nile-red-fluorescent-microspheres-default") - - Author tracking via nullable IntegerFields (created_by_id, updated_by_id) to avoid FK dependency on auth - -2. **FluorescenceMeasurement**: - - Tracks individual measurements with reference to source paper - - FK to Fluorophore (required) - - FK to Reference (nullable - some proteins lack primary_reference) - - Fields: all fluorescence properties + date_measured, conditions, is_trusted - -3. **Dye** (container model): - - No fluorescence properties (those moved to DyeState) - - Fields: name, slug (max_length=100), synonyms (ArrayField), structural_status, inchikey, etc. - - Product mixin fields: manufacturer, part, url - - UniqueConstraint on inchikey only for DEFINED status - -4. **DyeState** (MTI child of Fluorophore): - - One DyeState per environmental condition for a Dye - - Fields: dye FK, name, solvent, ph, environment, is_reference - -5. **State** (MTI child of Fluorophore): - - Recreated as MTI child (was previously standalone) - - Fields: protein FK, name, maturation - - Fluorescence properties inherited from Fluorophore parent - -**Foreign Key Additions:** -- Added `owner_fluor` FK to Spectrum (nullable during transition) -- Added `fluor` FK to OcFluorEff (nullable during transition) - -**Key Design Decisions:** -- All additions in one migration for atomic schema change -- Old tables preserved as `*_old` for data migration -- No deletions yet - old schema still accessible via raw SQL - -### Migration 0060: Migrate Data from Old Schema - -**Purpose:** Comprehensive data migration in a single RunPython operation. - -This migration handles all data transformation: State→Fluorophore+State, Dye→Dye+DyeState+Fluorophore, creating measurements, and updating foreign keys. - -**Functions Implemented:** - -#### 1. `migrate_state_data(apps, schema_editor)` - -Transforms each old State record into: -- Fluorophore parent (with materialized fluorescence properties) -- State MTI child (with protein FK, name, maturation) -- FluorescenceMeasurement (if fluorescence data exists) - -**Key Implementation Details:** -- Raw SQL SELECT from `proteins_state_old` table -- Slug generation with uniqueness handling: - - Use existing slug if non-empty - - Fall back to `{protein.slug}-{name}` or `state-{old_id}` - - Deduplicate with counter suffix if conflicts exist -- **MTI child creation via raw SQL INSERT** to avoid Django's save() attempting to update parent: - ```sql - INSERT INTO proteins_state (fluorophore_ptr_id, name, protein_id, maturation) - VALUES (%s, %s, %s, %s) - ``` -- PostgreSQL column quoting: `"twop_peak_gm"` in SELECT, mapped to lowercase variable in Python -- FluorescenceMeasurement created with `reference_id=protein.primary_reference_id` (may be None) - -**Migrated:** 1,055 State records - -#### 2. `migrate_dye_data(apps, schema_editor)` - -Transforms each old Dye record into: -- Dye container (no fluorescence properties) -- Fluorophore parent (with materialized fluorescence properties) -- DyeState MTI child (linking Dye to Fluorophore) -- FluorescenceMeasurement (if fluorescence data exists) - -**Key Implementation Details:** -- Slug generation: `{dye_slug}-default` with uniqueness checks -- All old dyes marked as `structural_status="PROPRIETARY"` (old schema had no chemical structure data) -- Empty `inchikey=""` to avoid unique constraint violations -- **MTI child creation via raw SQL INSERT** (same reason as State) -- hex color fields (`emhex`, `exhex`) omitted - computed by model's `save()` method from wavelengths -- Default environment: `solvent="PBS"`, `ph=7.4`, `environment="FREE"` - -**Migrated:** 950 Dye records → 950 Dye containers + 950 DyeStates - -#### 3. `update_spectrum_ownership(apps, schema_editor)` - -Updates Spectrum foreign keys from old `owner_state`/`owner_dye` to new `owner_fluor`. - -**Implementation via raw SQL for performance:** -```sql --- Update spectra owned by States -UPDATE proteins_spectrum s -SET owner_fluor_id = ( - SELECT f.id FROM proteins_fluorophore f - JOIN proteins_state ns ON ns.fluorophore_ptr_id = f.id - JOIN proteins_state_old os ON os.slug = f.slug - WHERE os.id = s.owner_state_id -) -WHERE s.owner_state_id IS NOT NULL - --- Update spectra owned by Dyes (similar pattern) -``` - -**Updated:** 1,191 spectra from States + 1,849 spectra from Dyes - -#### 4. `update_ocfluoreff(apps, schema_editor)` - -Updates OcFluorEff from GenericForeignKey to direct FK to Fluorophore. - -**Implementation:** Similar SQL pattern matching old content_type/object_id to new Fluorophore records. - -**Updated:** 0 OcFluorEff records (production has no data in this table yet) - -### Migration 0061: Cleanup Old Schema - -**Purpose:** Remove old tables and fields, finalize the schema transformation. - -**Operations:** - -1. **Drop old backup tables:** - - `DROP TABLE proteins_state_old CASCADE` - - `DROP TABLE proteins_dye_old CASCADE` - -2. **Remove deprecated Spectrum fields:** - - `owner_state` FK (data migrated to owner_fluor) - - `owner_dye` FK (data migrated to owner_fluor) - -3. **Remove deprecated OcFluorEff fields:** - - `content_type` (GenericForeignKey component) - - `object_id` (GenericForeignKey component) - -4. **Make fluor FK non-nullable:** - - `OcFluorEff.fluor` now required (was nullable during transition) - -5. **Verification step:** - - SQL check: Fail if any Spectrum records have no owner after migration - - Ensures data integrity before making schema changes - -## Testing Results - -### Test 1: Fresh Install (New Database) - -**Command:** -```bash -uv run pytest --create-db -``` - -**Result:** ✅ **PASSED** - All 88 backend tests passed -- Migrations 0001-0061 applied successfully -- All protein tests passed -- Schema correctly created from scratch - -### Test 2: Production Migration Simulation - -**Command:** -```bash -just pgpull # Drops local DB, pulls from Heroku production, runs migrate -``` - -**Result:** ✅ **PASSED** - Migration completed successfully - -**Migration Output:** -``` -Operations to perform: - Apply all migrations: account, admin, auth, avatar, contenttypes, favit, proteins, references, reversion, sessions, sites, socialaccount, users -Running migrations: - Applying proteins.0059_add_fluorophore_and_new_models... OK - Applying proteins.0060_migrate_data_from_old_schema... - Starting data migration from old schema... - Migrated 1055 State records - Migrated 950 Dye records to Dye containers - Created 950 DyeState records - Updated 1191 spectra owned by States - Updated 1849 spectra owned by Dyes - Updated 0 OcFluorEff records that pointed to States - Updated 0 OcFluorEff records that pointed to Dyes - Data migration complete! - OK - Applying proteins.0061_cleanup_old_schema... OK -``` - -**Full Test Suite:** -```bash -just test # Runs all backend + e2e tests -``` -**Result:** ✅ **PASSED** - All 54 tests passed in 14.94s - -### Test 3: Data Integrity Verification - -Post-migration checks confirmed: -- ✅ All 1,055 States have Fluorophore parents (no orphans) -- ✅ All 950 Dyes converted to Dye containers with DyeStates -- ✅ All 3,040 Spectra migrated to new `owner_fluor` FK -- ✅ No Spectra with null owners -- ✅ Old `owner_state` and `owner_dye` FKs removed -- ✅ Fluorophore slugs unique (longest: 53 chars, handled by max_length=100) - -## Key Technical Challenges & Solutions - -### Challenge 1: MTI Child Creation in Migrations - -**Problem:** Django's MTI `.save()` tries to UPDATE the parent record when creating a child, causing: -``` -IntegrityError: duplicate key value violates unique constraint "proteins_fluorophore_slug_key" -DETAIL: Key (slug)=() already exists. -``` - -**Root Cause:** When calling `State.objects.create(fluorophore_ptr=fluorophore, ...)`, Django's MTI machinery attempts to save the parent with empty field values. - -**Solution:** Use raw SQL INSERT for MTI child tables: -```python -cursor.execute(""" - INSERT INTO proteins_state (fluorophore_ptr_id, name, protein_id, maturation) - VALUES (%s, %s, %s, %s) -""", [fluorophore.pk, name, protein_id, maturation]) -``` - -This bypasses Django's save logic and directly creates the child record. - -### Challenge 2: PostgreSQL Column Case Sensitivity - -**Problem:** Column `twop_peak_gm` created with mixed case, but unquoted identifiers in SQL become lowercase: -``` -UndefinedColumn: column "twop_peak_gm" does not exist -``` - -**Solution:** Quote column names in SELECT statements: -```python -cursor.execute(""" - SELECT ..., "twop_peak_gm", ... - FROM proteins_state_old -""") -``` -Then map to lowercase Python variable, then assign to camelCase model field. - -### Challenge 3: Slug Length Constraints - -**Problem:** SlugField default max_length=50, but production has dye names like: -``` -"FluoSpheres nile red fluorescent microspheres" -→ slug: "fluospheres-nile-red-fluorescent-microspheres-default" (53 chars) -``` - -**Solution:** Increased SlugField max_length to 100 in both Fluorophore and Dye models. - -### Challenge 4: Empty/Duplicate Slugs - -**Problem:** Production data has States with empty or duplicate slugs. - -**Solution:** Comprehensive slug generation with fallbacks: -1. Use existing slug if non-empty -2. Generate from `{protein.slug}-{name}` or fallback to `state-{old_id}` -3. Check for uniqueness, append `-{counter}` if duplicate -4. Final safety check: ensure non-empty before creating - -### Challenge 5: Nullable References - -**Problem:** Some Proteins don't have `primary_reference`, but FluorescenceMeasurement.reference was required. - -**Solution:** Made `reference` FK nullable: `null=True, blank=True` - -### Challenge 6: Dye Chemical Structure Data - -**Problem:** Old Dye schema has no inchikey field, but new schema has `UniqueConstraint(inchikey, condition=Q(structural_status="DEFINED"))`. - -**Solution:** -- Mark all old dyes as `structural_status="PROPRIETARY"` -- Set `inchikey=""` (unique constraint only applies to DEFINED dyes) -- Can be updated later when chemical data is added - -## Implementation Summary - -**Completed Steps:** -1. ✅ Moved all 58 migrations from `migrations_old/` to `migrations/` -2. ✅ Deleted placeholder `0001_initial.py` and `0002_*.py` -3. ✅ Created 0059 (schema), 0060 (data), 0061 (cleanup) -4. ✅ Tested with fresh DB: All 88 tests passed -5. ✅ Tested with pgpull: Production migration successful -6. ✅ Verified data integrity: All records migrated correctly -7. 🚀 Ready for production deployment - -## Benefits Achieved - -- ✅ **Full history preserved:** All 58 original migrations retained -- ✅ **Any backup can migrate:** Historical backups can migrate to latest schema -- ✅ **Atomic migration:** Single `0060` RunPython does all data transformation -- ✅ **Zero data loss:** All 1,055 States and 950 Dyes migrated -- ✅ **Clean schema:** Old fields removed, new MTI structure in place -- ✅ **Tested thoroughly:** Fresh DB, production simulation, and full test suite all pass From be840e53708f075c42b5fb25998aab01e9a238ef Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Mon, 24 Nov 2025 15:15:52 -0500 Subject: [PATCH 28/57] review notes --- backend/proteins/admin.py | 12 ++++- backend/proteins/api/serializers.py | 8 ++-- backend/proteins/models/collection.py | 2 +- backend/proteins/models/fluorophore.py | 45 +++++++++---------- backend/proteins/models/microscope.py | 29 +++++------- backend/proteins/models/protein.py | 6 +-- backend/proteins/models/spectrum.py | 45 ++++++++++++++----- backend/proteins/schema/query.py | 3 ++ backend/references/models.py | 25 ++++++----- .../test_fpbase/test_query_optimization.py | 11 ----- 10 files changed, 101 insertions(+), 85 deletions(-) diff --git a/backend/proteins/admin.py b/backend/proteins/admin.py index 4f81d3116..e0e31bc55 100644 --- a/backend/proteins/admin.py +++ b/backend/proteins/admin.py @@ -15,6 +15,7 @@ BleachMeasurement, Camera, Dye, + DyeState, Excerpt, Filter, FilterPlacement, @@ -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): @@ -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) diff --git a/backend/proteins/api/serializers.py b/backend/proteins/api/serializers.py index 631a098bb..1297b7f93 100644 --- a/backend/proteins/api/serializers.py +++ b/backend/proteins/api/serializers.py @@ -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): @@ -30,13 +30,13 @@ class Meta: def get_protein_name(self, obj): # Check if owner_fluor is a State (has protein attribute) - if obj.owner_fluor and hasattr(obj.owner_fluor, "protein"): + 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): # Check if owner_fluor is a State (has protein attribute) - if obj.owner_fluor and hasattr(obj.owner_fluor, "protein"): + if obj.owner_fluor and obj.owner_fluor.entity_type == Fluorophore.EntityTypes.PROTEIN: return obj.owner_fluor.protein.slug return None diff --git a/backend/proteins/models/collection.py b/backend/proteins/models/collection.py index 10166444f..549a20aa6 100644 --- a/backend/proteins/models/collection.py +++ b/backend/proteins/models/collection.py @@ -65,7 +65,7 @@ class Meta: class FluorophoreCollection(ProteinCollection): if TYPE_CHECKING: - dyes = models.ManyToManyField["Dye", "FluorophoreCollection"] + dyes = models.ManyToManyField["Dye", "FluorophoreCollection"]() fluor_on_scope: models.QuerySet[Microscope] else: dyes = models.ManyToManyField("Dye", blank=True, related_name="collection_memberships") diff --git a/backend/proteins/models/fluorophore.py b/backend/proteins/models/fluorophore.py index 41ecb5d98..3f9f4cc2a 100644 --- a/backend/proteins/models/fluorophore.py +++ b/backend/proteins/models/fluorophore.py @@ -1,24 +1,19 @@ +from __future__ import annotations + from typing import TYPE_CHECKING, Final, Literal from django.db import models -from django.db.models import QuerySet from proteins.models.fluorescence_data import AbstractFluorescenceData if TYPE_CHECKING: from typing import Self + from django.db.models import QuerySet from django.db.models.manager import RelatedManager - from proteins.models import ( # noqa: F401 - Dye, - DyeState, - FluorescenceMeasurement, - OcFluorEff, - Protein, - Spectrum, - State, - ) + from proteins.models import Dye, DyeState, FluorescenceMeasurement, OcFluorEff, Protein, State # noqa: F401 + from proteins.models.spectrum import D3Dict, Spectrum class FluorophoreManager[T: models.Model](models.Manager): @@ -78,7 +73,7 @@ class EntityTypes(models.TextChoices): source_map = models.JSONField(default=dict, blank=True) # Managers - objects: "FluorophoreManager[Self]" = FluorophoreManager() + objects: FluorophoreManager[Self] = FluorophoreManager() if TYPE_CHECKING: spectra = RelatedManager["Spectrum"]() @@ -86,8 +81,8 @@ class EntityTypes(models.TextChoices): oc_effs = RelatedManager["OcFluorEff"]() # these are not *guaranteed* to exist, they come from Django MTI - dyestate: "DyeState" - state: "State" + dyestate: DyeState + state: State class Meta: indexes = [ @@ -109,7 +104,7 @@ def label(self) -> str: return self.owner_name return f"{self.owner_name} ({self.name})" - def as_subclass(self) -> "Self": + def as_subclass(self) -> Self: """Downcast to the specific subclass instance.""" for subclass_name in ["dyestate", "state"]: if hasattr(self, subclass_name): @@ -165,7 +160,7 @@ def fluor_name(self) -> str: return self.name @property - def abs_spectrum(self) -> "Spectrum | None": + def abs_spectrum(self) -> Spectrum | None: spect = [f for f in self.spectra.all() if f.subtype == "ab"] if len(spect) > 1: raise AssertionError(f"multiple ex spectra found for {self}") @@ -174,7 +169,7 @@ def abs_spectrum(self) -> "Spectrum | None": return None @property - def ex_spectrum(self) -> "Spectrum | None": + def ex_spectrum(self) -> Spectrum | None: spect = [f for f in self.spectra.all() if f.subtype == "ex"] if len(spect) > 1: raise AssertionError(f"multiple ex spectra found for {self}") @@ -183,16 +178,16 @@ def ex_spectrum(self) -> "Spectrum | None": return self.abs_spectrum @property - def em_spectrum(self) -> "Spectrum | None": + def em_spectrum(self) -> Spectrum | None: spect = [f for f in self.spectra.all() if f.subtype == "em"] if len(spect) > 1: raise AssertionError(f"multiple em spectra found for {self}") if len(spect): return spect[0] - return self.abs_spectrum + return None @property - def twop_spectrum(self) -> "Spectrum | None": + def twop_spectrum(self) -> Spectrum | None: spect = [f for f in self.spectra.all() if f.subtype == "2p"] if len(spect) > 1: raise AssertionError("multiple 2p spectra found") @@ -238,7 +233,7 @@ def within_em_band(self, value, height=0.7) -> bool: return True return False - def d3_dicts(self) -> list[dict]: + def d3_dicts(self) -> list[D3Dict]: return [spect.d3dict() for spect in self.spectra.all()] def get_absolute_url(self) -> str | None: @@ -247,9 +242,9 @@ def get_absolute_url(self) -> str | None: return owner.get_absolute_url() return None - def _owner(self) -> "Dye | Protein | None": - if hasattr(self, "dye"): - return self.dye - if hasattr(self, "protein"): - return self.protein + def _owner(self) -> Dye | Protein | None: + if hasattr(self, "dyestate"): + return self.dyestate.dye + if hasattr(self, "state"): + return self.state.protein return None diff --git a/backend/proteins/models/microscope.py b/backend/proteins/models/microscope.py index fd0317864..76e762936 100644 --- a/backend/proteins/models/microscope.py +++ b/backend/proteins/models/microscope.py @@ -21,7 +21,8 @@ if TYPE_CHECKING: from django.db.models.manager import RelatedManager - from proteins.models.spectrum import Spectrum + from proteins.models.collection import FluorophoreCollection, ProteinCollection # noqa: F401 + from proteins.models.spectrum import D3Dict, Spectrum class Microscope(OwnedCollection): @@ -36,8 +37,8 @@ class Microscope(OwnedCollection): id = models.CharField(primary_key=True, max_length=22, default=shortuuid, editable=False) if TYPE_CHECKING: - extra_lights = models.ManyToManyField["Light", "Microscope"] - extra_cameras = models.ManyToManyField["Camera", "Microscope"] + extra_lights = models.ManyToManyField["Light", "Microscope"]() + extra_cameras = models.ManyToManyField["Camera", "Microscope"]() else: extra_lights = models.ManyToManyField("Light", blank=True, related_name="microscopes") extra_cameras = models.ManyToManyField("Camera", blank=True, related_name="microscopes") @@ -177,19 +178,13 @@ def bs_filters(self): @cached_property def spectra(self) -> list[Spectrum]: - spectra = [] - for f in ( - self.ex_filters, - self.em_filters, - self.bs_filters, - self.lights, - self.cameras, - ): - for i in f.select_related("spectrum"): - spectra.append(i.spectrum) - return spectra - - def spectra_d3(self): + return [ + obj.spectrum + for qs in (self.ex_filters, self.em_filters, self.bs_filters, self.lights, self.cameras) + for obj in qs.select_related("spectrum") + ] + + def spectra_d3(self) -> list[D3Dict]: return [spec.d3dict() for spec in self.spectra] @@ -244,7 +239,7 @@ class OpticalConfig(OwnedCollection): if TYPE_CHECKING: from proteins.models import OcFluorEff - filters = models.ManyToManyField["Filter", "OpticalConfig"] + filters = models.ManyToManyField["Filter", "OpticalConfig"]() filterplacement_set: models.QuerySet[FilterPlacement] ocfluoreff_set: models.QuerySet[OcFluorEff] else: diff --git a/backend/proteins/models/protein.py b/backend/proteins/models/protein.py index 4ba52c767..a0f687a91 100644 --- a/backend/proteins/models/protein.py +++ b/backend/proteins/models/protein.py @@ -246,7 +246,7 @@ class CofactorChoices(models.TextChoices): help_text="Preferably the publication that introduced the protein", ) if TYPE_CHECKING: - references = models.ManyToManyField["Reference", "Protein"] + references = models.ManyToManyField["Reference", "Protein"]() else: references = models.ManyToManyField(Reference, related_name="proteins", blank=True) @@ -267,7 +267,7 @@ class CofactorChoices(models.TextChoices): oser_measurements: models.QuerySet[OSERMeasurement] collection_memberships: models.QuerySet[ProteinCollection] excerpts: models.QuerySet[Excerpt] - snapgene_plasmids = models.ManyToManyField["SnapGenePlasmid", "Protein"] + snapgene_plasmids = models.ManyToManyField["SnapGenePlasmid", "Protein"]() else: snapgene_plasmids = models.ManyToManyField( "SnapGenePlasmid", @@ -573,7 +573,7 @@ class State(Fluorophore): # TODO: rename to ProteinState ) if TYPE_CHECKING: - transitions = models.ManyToManyField["State", "State"] + transitions = models.ManyToManyField["State", "State"]() transition_state: models.QuerySet["State"] transitions_from: models.QuerySet[StateTransition] transitions_to: models.QuerySet[StateTransition] diff --git a/backend/proteins/models/spectrum.py b/backend/proteins/models/spectrum.py index e875a936e..aeb5e5f8c 100644 --- a/backend/proteins/models/spectrum.py +++ b/backend/proteins/models/spectrum.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import ast import json import logging @@ -26,8 +28,31 @@ from references.models import Reference if TYPE_CHECKING: + from typing import NotRequired, TypedDict + from proteins.models import Fluorophore + class D3Dict(TypedDict): + slug: str + key: str + id: int + values: list[dict[str, float]] + peak: float | bool + minwave: float + maxwave: float + category: str + type: str + color: str + area: bool + url: str | None + classed: str + + scalar: NotRequired[float | None] + ex_max: NotRequired[float | None] + em_max: NotRequired[float | None] + twop_qy: NotRequired[float | None] + + logger = logging.getLogger(__name__) @@ -54,7 +79,7 @@ def save(self, *args, **kwargs): def makeslug(self): return slugify(self.name.replace("/", "-")) - def d3_dicts(self): + def d3_dicts(self) -> list[D3Dict]: return [spect.d3dict() for spect in self.spectra.all()] @@ -62,7 +87,7 @@ class Camera(SpectrumOwner, Product): manufacturer = models.CharField(max_length=128, blank=True) if TYPE_CHECKING: - spectrum: "Spectrum" + spectrum: Spectrum microscopes: models.QuerySet optical_configs: models.QuerySet @@ -71,7 +96,7 @@ class Light(SpectrumOwner, Product): manufacturer = models.CharField(max_length=128, blank=True) if TYPE_CHECKING: - spectrum: "Spectrum" + spectrum: Spectrum microscopes: models.QuerySet optical_configs: models.QuerySet @@ -196,7 +221,7 @@ def fluorlist(self): ) return sorted(out, key=lambda k: k["name"]) - def filter_owner(self, slug): + def filter_owner(self, slug: str) -> QuerySet[Spectrum]: qs = self.none() A = ("owner_fluor", "owner_filter", "owner_light", "owner_camera") for ownerclass in A: @@ -386,7 +411,7 @@ class Spectrum(Authorable, StatusModel, TimeStampedModel, AdminURLMixin): ) source = models.CharField(max_length=128, blank=True, help_text="Source of the spectra data") - objects = SpectrumManager() + objects: SpectrumManager = SpectrumManager() fluorophores = QueryManager(models.Q(category=DYE) | models.Q(category=PROTEIN)) proteins = QueryManager(category=PROTEIN) dyes = QueryManager(category=DYE) @@ -468,7 +493,7 @@ def clean(self): raise ValidationError(errors) @property - def owner_set(self) -> list["Fluorophore | Filter | Light | Camera | None"]: + def owner_set(self) -> list[Fluorophore | Filter | Light | Camera | None]: return [ self.owner_fluor, self.owner_filter, @@ -477,7 +502,7 @@ def owner_set(self) -> list["Fluorophore | Filter | Light | Camera | None"]: ] @property - def owner(self) -> "Fluorophore | Filter | Light | Camera | None": + def owner(self) -> Fluorophore | Filter | Light | Camera | None: return next((x for x in self.owner_set if x), None) # raise AssertionError("No owner is set") @@ -553,8 +578,8 @@ def width(self, height=0.5) -> tuple[float, float] | Literal[False]: except Exception: return False - def d3dict(self): - D = { + def d3dict(self) -> D3Dict: + D: D3Dict = { "slug": self.owner.slug, "key": self.name, "id": self.id, @@ -667,7 +692,7 @@ class Filter(SpectrumOwner, Product): if TYPE_CHECKING: from proteins.models import OpticalConfig - spectrum: "Spectrum" + spectrum: Spectrum optical_configs: models.QuerySet[OpticalConfig] class Meta: diff --git a/backend/proteins/schema/query.py b/backend/proteins/schema/query.py index 856107264..7ab280396 100644 --- a/backend/proteins/schema/query.py +++ b/backend/proteins/schema/query.py @@ -144,6 +144,9 @@ def resolve_opticalConfig(self, info, **kwargs): dyes = graphene.List(types.Dye) dye = graphene.Field(types.Dye, id=graphene.Int(), name=graphene.String()) + # FIXME: + # "dye" is now returning a DyeState, not a Dye... this is backwards compatible + # but incorrect and needs to be fixed with a deprecation cycle. def resolve_dyes(self, info, **kwargs): return gdo.query(models.DyeState.objects.all(), info) diff --git a/backend/references/models.py b/backend/references/models.py index 19b0eb44e..55533fdf8 100644 --- a/backend/references/models.py +++ b/backend/references/models.py @@ -19,6 +19,17 @@ from proteins.extrest import entrez from proteins.validators import validate_doi +if TYPE_CHECKING: + from proteins.models import ( + BleachMeasurement, + Excerpt, + FluorescenceMeasurement, + Lineage, + OSERMeasurement, + Protein, + Spectrum, + ) + User = get_user_model() @@ -31,7 +42,7 @@ class Author(TimeStampedModel): given = models.CharField(max_length=80) initials = models.CharField(max_length=10) if TYPE_CHECKING: - publications = models.ManyToManyField["Reference", "Author"] + publications = models.ManyToManyField["Reference", "Author"]() referenceauthor_set: models.QuerySet[ReferenceAuthor] else: publications = models.ManyToManyField("Reference", through="ReferenceAuthor") @@ -98,17 +109,7 @@ class Reference(TimeStampedModel): help_text="YYYY", ) if TYPE_CHECKING: - from proteins.models import ( - BleachMeasurement, - Excerpt, - FluorescenceMeasurement, - Lineage, - OSERMeasurement, - Protein, - Spectrum, - ) - - authors = models.ManyToManyField["Author", "Reference"] + authors = models.ManyToManyField["Author", "Reference"]() referenceauthor_set: models.QuerySet[ReferenceAuthor] primary_proteins: models.QuerySet[Protein] proteins: models.QuerySet[Protein] diff --git a/backend/tests/test_fpbase/test_query_optimization.py b/backend/tests/test_fpbase/test_query_optimization.py index 5042654f2..7d29df213 100644 --- a/backend/tests/test_fpbase/test_query_optimization.py +++ b/backend/tests/test_fpbase/test_query_optimization.py @@ -201,15 +201,6 @@ def test_proteins_query_is_optimized(self): + "\n".join(f"{i + 1}. {q['sql'][:200]}..." for i, q in enumerate(context.captured_queries)), ) - # For better understanding, print the actual number of queries - print(f"\nQuery optimization test: {num_queries} queries executed") - if num_queries <= 8: - print("✓ Excellent optimization!") - elif num_queries <= 15: - print("✓ Good optimization (could be better)") - else: - print("✗ Poor optimization - likely N+1 problem exists") - def test_single_protein_with_transitions(self): """ Test a more focused query for a single protein with transitions. @@ -266,5 +257,3 @@ def test_single_protein_with_transitions(self): 10, f"Single protein query should be highly optimized, got {num_queries} queries", ) - - print(f"\nSingle protein query: {num_queries} queries executed") From 0052ea8448c30983b576c904a3856e20847505e3 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Mon, 24 Nov 2025 16:16:09 -0500 Subject: [PATCH 29/57] remove unneeded sql --- .../0059_add_fluorophore_and_new_models.py | 386 +++++++++++++----- backend/proteins/models/dye.py | 165 +------- 2 files changed, 302 insertions(+), 249 deletions(-) diff --git a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py index b2424d28e..2b688a675 100644 --- a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py +++ b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py @@ -1,20 +1,19 @@ from __future__ import annotations + +import logging from typing import Any -# Generated manually for schema overhaul -from django.core.validators import MaxValueValidator, MinValueValidator -from django.contrib.postgres.fields import ArrayField -from django.db import migrations, models import django.db.models.deletion -import model_utils.fields import django.utils.timezone -import logging -from django.db import migrations, models -import django.db.models.deletion -from django.db import migrations +import model_utils.fields from django.apps.registry import Apps + +# Generated manually for schema overhaul +from django.core.validators import MaxValueValidator, MinValueValidator +from django.db import migrations, models from django.db.backends.base.schema import BaseDatabaseSchemaEditor from django.db.backends.utils import CursorWrapper + logger = logging.getLogger(__name__) @@ -25,6 +24,7 @@ def _dictfetchall(cursor: CursorWrapper) -> list[dict[str, Any]]: columns = (col.name for col in cursor.description) return [dict(zip(columns, row)) for row in cursor.fetchall()] + def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: """Migrate State data from old schema to new Fluorophore + State MTI structure.""" # Get models from migration state @@ -44,10 +44,28 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N """) for row in cursor.fetchall(): - (old_id, created, modified, name, slug, is_dark, - ex_max, em_max, ext_coeff, qy, brightness, - lifetime, pka, twop_ex_max, twop_peak_gm, twop_qy, - maturation, protein_id, created_by_id, updated_by_id) = row + ( + old_id, + created, + modified, + name, + slug, + is_dark, + ex_max, + em_max, + ext_coeff, + qy, + brightness, + lifetime, + pka, + twop_ex_max, + twop_peak_gm, + twop_qy, + maturation, + protein_id, + created_by_id, + updated_by_id, + ) = row # Get protein for label try: @@ -71,7 +89,6 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N if not state_slug or state_slug.strip() == "": state_slug = f"state-{old_id}" - # Ensure slug uniqueness by checking if it already exists original_slug = state_slug base_slug = state_slug @@ -87,12 +104,16 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N if state_slug != original_slug: logger.warning( - f"Slug collision during State migration: {original_slug} -> {state_slug} " - f"(State ID: {old_id}, Protein: {protein.name})" + "Slug collision during State migration: %s -> %s (State ID: %s, Protein: %s)", + original_slug, + state_slug, + old_id, + protein.name, ) - # Create Fluorophore parent (MTI will link automatically) - fluorophore = Fluorophore.objects.create( + # Create State (MTI child of Fluorophore) + # Django's MTI creates both parent and child records in one operation + state = State.objects.create( id=old_id, # Preserve old ID for easier FK updates later created=created, modified=modified, @@ -113,21 +134,17 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N twop_qy=twop_qy, is_dark=is_dark, created_by_id=created_by_id, - updated_by_id=updated_by_id + updated_by_id=updated_by_id, + # State-specific fields + protein_id=protein_id, + maturation=maturation, ) - # Create State (MTI child) pointing to the Fluorophore - # Use raw SQL to avoid Django MTI trying to update parent with empty values - cursor.execute(""" - INSERT INTO proteins_state (fluorophore_ptr_id, protein_id, maturation) - VALUES (%s, %s, %s) - """, [fluorophore.pk, protein_id, maturation]) - # Create FluorescenceMeasurement from old State data if there's any fluorescence data if any([ex_max, em_max, qy, ext_coeff, lifetime, pka, twop_ex_max, twop_peak_gm, twop_qy]): FluorescenceMeasurement.objects.create( id=old_id, # Preserve old ID - fluorophore=fluorophore, + fluorophore=state, # State is-a Fluorophore (MTI) reference_id=protein.primary_reference_id, ex_max=ex_max, em_max=em_max, @@ -142,7 +159,7 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N is_dark=is_dark, is_trusted=True, # Mark as trusted since it's the original data created_by_id=created_by_id, - updated_by_id=updated_by_id + updated_by_id=updated_by_id, ) print(f"Migrated {State.objects.count()} State records") @@ -186,9 +203,24 @@ def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> Non """) for row in cursor.fetchall(): - (old_id, created, modified, name, slug, is_dark, - ex_max, em_max, ext_coeff, qy, brightness, - lifetime, pka, twop_ex_max, twop_peak_gm, twop_qy) = row + ( + old_id, + created, + modified, + name, + slug, + is_dark, + ex_max, + em_max, + ext_coeff, + qy, + brightness, + lifetime, + pka, + twop_ex_max, + twop_peak_gm, + twop_qy, + ) = row # Handle empty/null slugs if not slug or slug.strip() == "": @@ -211,8 +243,11 @@ def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> Non if dye_slug != original_dye_slug: logger.warning( - f"Slug collision during Dye migration: {original_dye_slug} -> {dye_slug} " - f"(Dye ID: {old_id}, Name: {name})" + "Slug collision during Dye migration: %s -> %s (Dye ID: %s, Name: %s)", + original_dye_slug, + dye_slug, + old_id, + name, ) # Old Dye schema doesn't have chemical structure fields @@ -244,12 +279,17 @@ def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> Non if fluorophore_slug != original_fluor_slug: logger.warning( - f"Fluorophore slug collision during Dye migration: {original_fluor_slug} -> {fluorophore_slug} " - f"(Dye ID: {old_id}, Name: {name})" + "Fluorophore slug collision during Dye migration: %s -> %s (Dye ID: %s, Name: %s)", + original_fluor_slug, + fluorophore_slug, + old_id, + name, ) + # Create DyeState (MTI child of Fluorophore) + # Django's MTI creates both parent and child records in one operation # Don't pass emhex/exhex - the save() method will compute them from wavelengths - fluorophore = Fluorophore.objects.create( + dyestate = DyeState.objects.create( created=created, modified=modified, name="default", @@ -268,21 +308,19 @@ def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> Non twop_peak_gm=twop_peak_gm, twop_qy=twop_qy, is_dark=is_dark, + # DyeState-specific field + dye=dye, ) - # Create DyeState (one per old Dye) - # Use raw SQL to avoid Django MTI trying to update parent with empty values - cursor.execute(""" - INSERT INTO proteins_dyestate (fluorophore_ptr_id, dye_id) - VALUES (%s, %s) - """, [fluorophore.pk, dye.pk]) + dye.default_state = dyestate + dye.save() # Create FluorescenceMeasurement from old Dye data if any([ex_max, em_max, qy, ext_coeff, lifetime, pka]): # For dyes, we don't have a primary_reference concept in old schema # We'll leave reference as None for now FluorescenceMeasurement.objects.create( - fluorophore=fluorophore, + fluorophore=dyestate, # DyeState is-a Fluorophore (MTI) reference_id=None, ex_max=ex_max, em_max=em_max, @@ -304,8 +342,8 @@ def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> Non def update_spectrum_ownership(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: """Update Spectrum foreign keys to point to new Fluorophore records.""" - Spectrum = apps.get_model("proteins", "Spectrum") - Fluorophore = apps.get_model("proteins", "Fluorophore") + apps.get_model("proteins", "Spectrum") + apps.get_model("proteins", "Fluorophore") # We need to map old State/Dye IDs to new Fluorophore IDs # This is tricky because we need to query the old tables @@ -411,11 +449,7 @@ def populate_emhex_exhex(apps, _schema_editor): fluor.exhex = wave_to_hex(fluor.ex_max) fluorophores_to_update.append(fluor) - Fluorophore.objects.bulk_update( - fluorophores_to_update, - ["emhex", "exhex"], - batch_size=500 - ) + Fluorophore.objects.bulk_update(fluorophores_to_update, ["emhex", "exhex"], batch_size=500) print(f"Populated emhex/exhex for {len(fluorophores_to_update)} fluorophores") @@ -458,21 +492,98 @@ def migrate_reverse(_apps, _schema_editor): def abstract_fluorescence_data_fields(): """Return fresh field instances for AbstractFluorescenceData. - + Each model needs its own unique field instances to avoid state conflicts. """ return [ - ("is_dark", models.BooleanField(default=False, verbose_name="Dark State", help_text="This state does not fluorescence")), - ("ex_max", models.PositiveSmallIntegerField(blank=True, null=True, validators=[MinValueValidator(300), MaxValueValidator(900)], db_index=True, help_text="Excitation maximum (nm)")), - ("em_max", models.PositiveSmallIntegerField(blank=True, null=True, validators=[MinValueValidator(300), MaxValueValidator(1000)], db_index=True, help_text="Emission maximum (nm)")), - ("ext_coeff", models.IntegerField(blank=True, null=True, verbose_name="Extinction Coefficient (M-1 cm-1)", validators=[MinValueValidator(0), MaxValueValidator(300000)])), - ("qy", models.FloatField(null=True, blank=True, verbose_name="Quantum Yield", validators=[MinValueValidator(0), MaxValueValidator(1)])), + ( + "is_dark", + models.BooleanField( + default=False, verbose_name="Dark State", help_text="This state does not fluorescence" + ), + ), + ( + "ex_max", + models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[MinValueValidator(300), MaxValueValidator(900)], + db_index=True, + help_text="Excitation maximum (nm)", + ), + ), + ( + "em_max", + models.PositiveSmallIntegerField( + blank=True, + null=True, + validators=[MinValueValidator(300), MaxValueValidator(1000)], + db_index=True, + help_text="Emission maximum (nm)", + ), + ), + ( + "ext_coeff", + models.IntegerField( + blank=True, + null=True, + verbose_name="Extinction Coefficient (M-1 cm-1)", + validators=[MinValueValidator(0), MaxValueValidator(300000)], + ), + ), + ( + "qy", + models.FloatField( + null=True, + blank=True, + verbose_name="Quantum Yield", + validators=[MinValueValidator(0), MaxValueValidator(1)], + ), + ), ("brightness", models.FloatField(null=True, blank=True, editable=False)), - ("lifetime", models.FloatField(null=True, blank=True, help_text="Lifetime (ns)", validators=[MinValueValidator(0), MaxValueValidator(20)])), - ("pka", models.FloatField(null=True, blank=True, verbose_name="pKa", validators=[MinValueValidator(2), MaxValueValidator(12)])), - ("twop_ex_max", models.PositiveSmallIntegerField(blank=True, null=True, verbose_name="Peak 2P excitation", validators=[MinValueValidator(700), MaxValueValidator(1600)], db_index=True)), - ("twop_peak_gm", models.FloatField(null=True, blank=True, verbose_name="Peak 2P cross-section of S0->S1 (GM)", validators=[MinValueValidator(0), MaxValueValidator(200)])), - ("twop_qy", models.FloatField(null=True, blank=True, verbose_name="2P Quantum Yield", validators=[MinValueValidator(0), MaxValueValidator(1)])), + ( + "lifetime", + models.FloatField( + null=True, + blank=True, + help_text="Lifetime (ns)", + validators=[MinValueValidator(0), MaxValueValidator(20)], + ), + ), + ( + "pka", + models.FloatField( + null=True, blank=True, verbose_name="pKa", validators=[MinValueValidator(2), MaxValueValidator(12)] + ), + ), + ( + "twop_ex_max", + models.PositiveSmallIntegerField( + blank=True, + null=True, + verbose_name="Peak 2P excitation", + validators=[MinValueValidator(700), MaxValueValidator(1600)], + db_index=True, + ), + ), + ( + "twop_peak_gm", + models.FloatField( + null=True, + blank=True, + verbose_name="Peak 2P cross-section of S0->S1 (GM)", + validators=[MinValueValidator(0), MaxValueValidator(200)], + ), + ), + ( + "twop_qy", + models.FloatField( + null=True, + blank=True, + verbose_name="2P Quantum Yield", + validators=[MinValueValidator(0), MaxValueValidator(1)], + ), + ), ("emhex", models.CharField(max_length=7, blank=True)), ("exhex", models.CharField(max_length=7, blank=True)), ] @@ -489,8 +600,18 @@ def authorable_mixin_fields(): def timestamped_mixin_fields(): """Return fresh field instances for TimeStampedModel mixin.""" return [ - ("created", model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name="created")), - ("modified", model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name="modified")), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, editable=False, verbose_name="created" + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, editable=False, verbose_name="modified" + ), + ), ] @@ -502,6 +623,7 @@ def product_mixin_fields(): ("url", models.URLField(blank=True)), ] + class Migration(migrations.Migration): dependencies = [ ("proteins", "0058_snapgeneplasmid_protein_snapgene_plasmids"), @@ -535,7 +657,6 @@ class Migration(migrations.Migration): migrations.DeleteModel(name="Dye"), ], ), - # Step 2: Create all new models # Fluorophore is the new "State" base model for MTI # State and DyeState are MTI children of Fluorophore @@ -547,13 +668,29 @@ class Migration(migrations.Migration): *timestamped_mixin_fields(), ("name", models.CharField(max_length=100, db_index=True, default="default")), ("slug", models.SlugField(max_length=200, unique=True)), - ("entity_type", models.CharField(max_length=2, choices=[("p", "Protein"), ("d", "Dye")], db_index=True)), - ("owner_name", models.CharField(max_length=255, db_index=True, blank=True, null=True, help_text="Protein/Dye name (cached for searching)")), - ("owner_slug", models.SlugField(max_length=200, blank=True, null=True, help_text="Protein/Dye slug (cached for URLs)")), + ( + "entity_type", + models.CharField(max_length=2, choices=[("p", "Protein"), ("d", "Dye")], db_index=True), + ), + ( + "owner_name", + models.CharField( + max_length=255, + db_index=True, + blank=True, + null=True, + help_text="Protein/Dye name (cached for searching)", + ), + ), + ( + "owner_slug", + models.SlugField( + max_length=200, blank=True, null=True, help_text="Protein/Dye slug (cached for URLs)" + ), + ), *abstract_fluorescence_data_fields(), ("source_map", models.JSONField(default=dict, blank=True)), *authorable_mixin_fields(), - ], options={ "indexes": [ @@ -564,7 +701,6 @@ class Migration(migrations.Migration): ], }, ), - migrations.CreateModel( name="FluorescenceMeasurement", fields=[ @@ -572,9 +708,22 @@ class Migration(migrations.Migration): *abstract_fluorescence_data_fields(), ("date_measured", models.DateField(null=True, blank=True)), ("conditions", models.TextField(blank=True, help_text="pH, solvent, temp, etc.")), - ("is_trusted", models.BooleanField(default=False, help_text="If True, this measurement overrides others.")), - ("fluorophore", models.ForeignKey("Fluorophore", related_name="measurements", on_delete=django.db.models.deletion.CASCADE)), - ("reference", models.ForeignKey("references.Reference", on_delete=django.db.models.deletion.CASCADE, null=True, blank=True)), + ( + "is_trusted", + models.BooleanField(default=False, help_text="If True, this measurement overrides others."), + ), + ( + "fluorophore", + models.ForeignKey( + "Fluorophore", related_name="measurements", on_delete=django.db.models.deletion.CASCADE + ), + ), + ( + "reference", + models.ForeignKey( + "references.Reference", on_delete=django.db.models.deletion.CASCADE, null=True, blank=True + ), + ), *authorable_mixin_fields(), *timestamped_mixin_fields(), ], @@ -582,23 +731,34 @@ class Migration(migrations.Migration): "abstract": False, }, ), - migrations.CreateModel( name="Dye", fields=[ ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ("name", models.CharField(max_length=255, db_index=True)), - ("slug", models.SlugField(max_length=100, unique=True)), # Increased from default 50 to accommodate long names + ( + "slug", + models.SlugField(max_length=100, unique=True), + ), # Increased from default 50 to accommodate long names *product_mixin_fields(), *authorable_mixin_fields(), *timestamped_mixin_fields(), ], ), - migrations.CreateModel( name="DyeState", fields=[ - ("fluorophore_ptr", models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to="proteins.fluorophore")), + ( + "fluorophore_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="proteins.fluorophore", + ), + ), ("dye", models.ForeignKey("Dye", on_delete=django.db.models.deletion.CASCADE, related_name="states")), ], options={ @@ -606,22 +766,66 @@ class Migration(migrations.Migration): }, bases=("proteins.fluorophore",), ), - + migrations.AddField( + model_name="dye", + name="default_state", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="default_for", + to="proteins.DyeState", + ), + ), # Re-create State model as MTI child of Fluorophore migrations.CreateModel( name="State", fields=[ - ("fluorophore_ptr", models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to="proteins.fluorophore")), - ("protein", models.ForeignKey("Protein", related_name="states", help_text="The protein to which this state belongs", on_delete=django.db.models.deletion.CASCADE)), - ("maturation", models.FloatField(null=True, blank=True, help_text="Maturation time (min)", validators=[MinValueValidator(0), MaxValueValidator(1600)])), - ("transitions", models.ManyToManyField(blank=True, related_name="transition_state", through="proteins.StateTransition", to="proteins.state", verbose_name="State Transitions")), + ( + "fluorophore_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="proteins.fluorophore", + ), + ), + ( + "protein", + models.ForeignKey( + "Protein", + related_name="states", + help_text="The protein to which this state belongs", + on_delete=django.db.models.deletion.CASCADE, + ), + ), + ( + "maturation", + models.FloatField( + null=True, + blank=True, + help_text="Maturation time (min)", + validators=[MinValueValidator(0), MaxValueValidator(1600)], + ), + ), + ( + "transitions", + models.ManyToManyField( + blank=True, + related_name="transition_state", + through="proteins.StateTransition", + to="proteins.state", + verbose_name="State Transitions", + ), + ), ], options={ "abstract": False, }, bases=("proteins.fluorophore",), ), - # Add owner_fluor to Spectrum (nullable for now) migrations.AddField( model_name="spectrum", @@ -634,7 +838,6 @@ class Migration(migrations.Migration): to="proteins.fluorophore", ), ), - # Add fluor FK to OcFluorEff (nullable for now) migrations.AddField( model_name="ocfluoreff", @@ -647,20 +850,15 @@ class Migration(migrations.Migration): to="proteins.fluorophore", ), ), - # Add index for spectrum owner_fluor lookups migrations.AddIndex( model_name="spectrum", index=models.Index(fields=["owner_fluor_id", "status"], name="spectrum_fluor_status_idx"), ), - # ---------------------------------------------- - # Perform manual data migration steps migrations.RunPython(migrate_forward, migrate_reverse), - # ---------------------------------------------- - # Step 1: Drop old tables that we renamed in 0059 migrations.RunSQL( sql="DROP TABLE IF EXISTS proteins_state_old CASCADE;", @@ -670,7 +868,6 @@ class Migration(migrations.Migration): sql="DROP TABLE IF EXISTS proteins_dye_old CASCADE;", reverse_sql=migrations.RunSQL.noop, ), - # Step 2: Remove old foreign keys from Spectrum migrations.RemoveField( model_name="spectrum", @@ -680,21 +877,25 @@ class Migration(migrations.Migration): model_name="spectrum", name="owner_dye", ), - # Step 3: Make owner_fluor non-nullable now that all data is migrated # First, verify no nulls exist (will fail if there are any) migrations.RunSQL( sql=""" DO $$ BEGIN - IF EXISTS (SELECT 1 FROM proteins_spectrum WHERE owner_fluor_id IS NULL AND (owner_filter_id IS NULL AND owner_light_id IS NULL AND owner_camera_id IS NULL)) THEN + IF EXISTS ( + SELECT 1 FROM proteins_spectrum + WHERE owner_fluor_id IS NULL + AND (owner_filter_id IS NULL + AND owner_light_id IS NULL + AND owner_camera_id IS NULL) + ) THEN RAISE EXCEPTION 'Found Spectrum records with no owner after migration!'; END IF; END $$; """, reverse_sql=migrations.RunSQL.noop, ), - # Step 4: Remove GenericForeignKey fields from OcFluorEff migrations.RemoveField( model_name="ocfluoreff", @@ -704,7 +905,6 @@ class Migration(migrations.Migration): model_name="ocfluoreff", name="object_id", ), - # Step 5: Make fluor FK on OcFluorEff non-nullable migrations.AlterField( model_name="ocfluoreff", diff --git a/backend/proteins/models/dye.py b/backend/proteins/models/dye.py index abfdbc93d..e58d66ab7 100644 --- a/backend/proteins/models/dye.py +++ b/backend/proteins/models/dye.py @@ -21,64 +21,20 @@ class Dye(Authorable, TimeStampedModel, Product): # TODO: rename to SmallMolecu name = models.CharField(max_length=255, db_index=True) slug = models.SlugField(unique=True) - # Synonyms allow users to find "FITC" when searching "Fluorescein" - # synonyms = ArrayField(models.CharField(max_length=255), blank=True, default=list) - - # --- Structural Status (The "Regret-Proof" Field) --- - # Allows entry of proprietary dyes without forcing a fake structure. - # STRUCTURE_STATUS_CHOICES = [ - # ("DEFINED", "Defined Structure"), - # ("PROPRIETARY", "Proprietary / Unknown Structure"), - # ] - # structural_status = models.CharField(max_length=20, choices=STRUCTURE_STATUS_CHOICES, default="DEFINED") - - # --- Chemical Graph Data (Nullable for Proprietary Dyes) --- - # We prioritize MolBlock for rendering, InChIKey for de-duplication. - # canonical_smiles = models.TextField(blank=True) - # inchi = models.TextField(blank=True) - # inchikey = models.CharField(max_length=27, blank=True, db_index=True) - # molblock = models.TextField(blank=True, help_text="V3000 Molfile for precise rendering") - - # --- Hierarchy & Ontology --- - # Handles FITC (Parent) vs 5-FITC (Child) relationship - # parent_mixture_id: int | None - # parent_mixture = models.ForeignKey["Dye | None"]( - # "self", - # on_delete=models.SET_NULL, - # null=True, - # blank=True, - # related_name="isomers", - # ) - - # Automated classification (e.g., "Rhodamine", "Cyanine", "BODIPY") - # Populated via ClassyFire API or manual curation - # chemical_class = models.CharField(max_length=100, blank=True, db_index=True) - - # --- Intrinsic Physics --- - # Critical for fluorogenic dyes (JF dyes, SiR-tubulin) - # Describes the Lactone-Zwitterion equilibrium constant. - # equilibrium_constant_klz = models.FloatField( - # null=True, - # blank=True, - # help_text="Equilibrium constant between non-fluorescent lactone and fluorescent zwitterion.", - # ) + default_state_id: int | None + default_state = models.ForeignKey["DyeState | None"]( + "DyeState", + related_name="default_for", + blank=True, + null=True, + on_delete=models.SET_NULL, + ) if TYPE_CHECKING: states = models.manager.RelatedManager["DyeState"]() - # isomers: models.QuerySet["Dye"] - # derivatives: models.QuerySet[ReactiveDerivative] - # collection_memberships: models.QuerySet class Meta: - ... - # Enforce uniqueness only on defined structures to allow multiple proprietary entries - # constraints = [ - # models.UniqueConstraint( - # fields=["inchikey"], - # name="unique_defined_molecule", - # condition=models.Q(structural_status="DEFINED"), - # ) - # ] + abstract = False def save(self, *args, **kwargs): super().save(*args, **kwargs) @@ -104,21 +60,6 @@ class DyeState(Fluorophore): dye_id: int dye = models.ForeignKey["Dye"](Dye, on_delete=models.CASCADE, related_name="states") - # --- Context --- - # name = models.CharField(max_length=255, help_text="e.g., 'Bound to DNA' or 'In Methanol'") - # solvent = models.CharField(max_length=100, default="PBS") - # ph = models.FloatField(default=7.4) - - # --- Environmental Categorization --- - # Helps the UI decide which spectrum to show for a specific query. - # ENVIRONMENT_CHOICES = [] - # environment = models.CharField(max_length=20, choices=ENVIRONMENT_CHOICES, default="FREE") - - # --- Logic --- - # is_reference = models.BooleanField( - # default=False, help_text="If True, this is the default state shown on the dye summary card." - # ) - if TYPE_CHECKING: fluorophore_ptr: Fluorophore # added by Django MTI @@ -136,91 +77,3 @@ def get_absolute_url(self): from django.urls import reverse return reverse("proteins:spectra") + f"?owner={self.dye.slug}" - - -# This section handles the commercial reality of purchasing dyes. -# -# It separates "Chemist Tools" (Reactive Dyes) from "Biologist Tools" (Antibody Conjugates). - - -# class ReactiveDerivative(models.Model): -# """A sold product derived from the SmallMolecule. - -# e.g., 'Janelia Fluor 549 NHS Ester' or 'JF549-HaloTag Ligand' -# These are the products users buy to perform conjugation -# (e.g., NHS esters, Maleimides, HaloTag Ligands). -# """ - -# core_dye_id: int -# core_dye = models.ForeignKey["Dye"]( -# Dye, -# on_delete=models.CASCADE, -# related_name="derivatives", -# ) - -# # --- Chemistry --- -# REACTIVE_GROUP_CHOICES = [ -# ("NHS_ESTER", "NHS Ester"), -# ("HALO_TAG", "HaloTag Ligand"), -# ("SNAP_TAG", "SNAP-Tag Ligand"), -# ("CLIP_TAG", "CLIP-Tag Ligand"), -# ("MALEIMIDE", "Maleimide"), -# ("AZIDE", "Azide"), -# ("ALKYNE", "Alkyne"), -# ("BIOTIN", "Biotin"), -# ("OTHER", "Other"), -# ] -# reactive_group = models.CharField(max_length=10, choices=REACTIVE_GROUP_CHOICES) - -# # Specific structure of the linker/handle (distinct from core dye) -# full_smiles = models.TextField(blank=True, help_text="Structure of the complete reactive molecule") -# molecular_weight = models.FloatField() - -# # --- Vendor Info --- -# vendor = models.CharField(max_length=100) -# catalog_number = models.CharField(max_length=100) - -# def __str__(self) -> str: -# return f"{self.core_dye.common_name} - {self.reactive_group} ({self.vendor} {self.catalog_number})" - - -# Architectural Decision: We do not create a new DyeState or SmallMolecule for every -# antibody conjugate. Instead, we use a relational link. -# -# When a user views "Alexa 488 Anti-CD4", the system pulls: -# 1. Biology from the Antibody model. -# 2. Physics from the SmallMolecule model (specifically, the DyeState where -# environment='PROTEIN_BOUND'). - - -# class AntibodyConjugate(models.Model): -# """ -# A virtual entity representing a commercial antibody-dye pairing. -# Does NOT store spectral data; inherits it from the Fluorophore. -# """ - -# # The "Ingredients" -# fluorophore_id: int -# fluorophore = models.ForeignKey["Dye"](SmallMolecule, on_delete=models.PROTECT) -# antibody_id: int -# antibody = models.ForeignKey["Antibody"]("Antibody", on_delete=models.PROTECT) # Define Antibody model elsewhere - -# # Product Details -# vendor = models.CharField(max_length=100) -# catalog_number = models.CharField(max_length=100) - -# # Heterogeneity -# f_p_ratio = models.FloatField( -# null=True, blank=True, help_text="Average Fluorophore/Protein ratio (Degree of Labeling)" -# ) - -# def get_display_spectrum(self): -# """ -# Logic to fetch the correct spectrum. -# Prioritizes a specific 'Protein Bound' state if available. -# """ -# protein_state = self.fluorophore.states.filter(environment="PROTEIN_BOUND").first() -# if protein_state: -# return protein_state -# # Fallback to reference state if no specific protein-bound data exists -# return self.fluorophore.get_primary_spectrum() From b7d671d4839aa97fe0a3895af58104bc1911169e Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Mon, 24 Nov 2025 16:52:11 -0500 Subject: [PATCH 30/57] cleanup --- .../0059_add_fluorophore_and_new_models.py | 299 ++++-------------- 1 file changed, 66 insertions(+), 233 deletions(-) diff --git a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py index 2b688a675..7eceab75b 100644 --- a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py +++ b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py @@ -21,14 +21,13 @@ def _dictfetchall(cursor: CursorWrapper) -> list[dict[str, Any]]: """Return all rows from a cursor as a dict. Assume the column names are unique.""" if not cursor.description: return [] - columns = (col.name for col in cursor.description) + columns = [col.name for col in cursor.description] return [dict(zip(columns, row)) for row in cursor.fetchall()] def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: """Migrate State data from old schema to new Fluorophore + State MTI structure.""" # Get models from migration state - Fluorophore = apps.get_model("proteins", "Fluorophore") State = apps.get_model("proteins", "State") Protein = apps.get_model("proteins", "Protein") FluorescenceMeasurement = apps.get_model("proteins", "FluorescenceMeasurement") @@ -42,125 +41,51 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N maturation, protein_id, created_by_id, updated_by_id FROM proteins_state_old """) - - for row in cursor.fetchall(): - ( - old_id, - created, - modified, - name, - slug, - is_dark, - ex_max, - em_max, - ext_coeff, - qy, - brightness, - lifetime, - pka, - twop_ex_max, - twop_peak_gm, - twop_qy, - maturation, - protein_id, - created_by_id, - updated_by_id, - ) = row + for row in _dictfetchall(cursor): + # Extract fields by name (order-independent, safer than positional unpacking) + old_id = row["id"] # Get protein for label - try: - protein = Protein.objects.get(id=protein_id) - except Protein.DoesNotExist: - # note, the db doesn't have any cases of this - print(f"Warning: Protein {protein_id} not found for State {old_id}, skipping") - continue - - # Handle empty/null slugs with guaranteed non-empty fallback - if not slug or (isinstance(slug, str) and slug.strip() == ""): - # Try protein slug + name, or protein slug, or fallback to state ID - if name and name != "default": - state_slug = f"{protein.slug}-{name}" if protein.slug else f"state-{old_id}" - else: - state_slug = protein.slug if protein.slug else f"state-{old_id}" - else: - state_slug = slug - - # Final safety check - ensure slug is not empty - if not state_slug or state_slug.strip() == "": - state_slug = f"state-{old_id}" - - # Ensure slug uniqueness by checking if it already exists - original_slug = state_slug - base_slug = state_slug - counter = 1 - while Fluorophore.objects.filter(slug=state_slug).exists(): - state_slug = f"{base_slug}-{counter}" - counter += 1 - if counter > 100: - raise ValueError( - f"Could not generate unique slug for State {old_id} " - f"after 100 attempts (original: {original_slug})" - ) - - if state_slug != original_slug: - logger.warning( - "Slug collision during State migration: %s -> %s (State ID: %s, Protein: %s)", - original_slug, - state_slug, - old_id, - protein.name, - ) + protein = Protein.objects.get(id=row["protein_id"]) # Create State (MTI child of Fluorophore) - # Django's MTI creates both parent and child records in one operation + # Note: Measurable fields (ex_max, em_max, qy, etc.) are left empty here + # and will be populated by rebuild_attributes() after creating FluorescenceMeasurement state = State.objects.create( id=old_id, # Preserve old ID for easier FK updates later - created=created, - modified=modified, - name=name, - slug=state_slug, + created=row["created"], + modified=row["modified"], + name=row["name"], + slug=row["slug"], entity_type="p", owner_name=protein.name, owner_slug=protein.slug, - ex_max=ex_max, - em_max=em_max, - ext_coeff=ext_coeff, - qy=qy, - brightness=brightness, - lifetime=lifetime, - pka=pka, - twop_ex_max=twop_ex_max, - twop_peak_gm=twop_peak_gm, # Map from SQL result to model field - twop_qy=twop_qy, - is_dark=is_dark, - created_by_id=created_by_id, - updated_by_id=updated_by_id, + created_by_id=row["created_by_id"], + updated_by_id=row["updated_by_id"], # State-specific fields - protein_id=protein_id, - maturation=maturation, + protein_id=protein.id, + maturation=row["maturation"], ) - # Create FluorescenceMeasurement from old State data if there's any fluorescence data - if any([ex_max, em_max, qy, ext_coeff, lifetime, pka, twop_ex_max, twop_peak_gm, twop_qy]): - FluorescenceMeasurement.objects.create( - id=old_id, # Preserve old ID - fluorophore=state, # State is-a Fluorophore (MTI) - reference_id=protein.primary_reference_id, - ex_max=ex_max, - em_max=em_max, - ext_coeff=ext_coeff, - qy=qy, - brightness=brightness, - lifetime=lifetime, - pka=pka, - twop_ex_max=twop_ex_max, - twop_peak_gm=twop_peak_gm, - twop_qy=twop_qy, - is_dark=is_dark, - is_trusted=True, # Mark as trusted since it's the original data - created_by_id=created_by_id, - updated_by_id=updated_by_id, - ) + FluorescenceMeasurement.objects.create( + id=old_id, # Preserve old ID + fluorophore=state, # State is-a Fluorophore (MTI) + reference_id=protein.primary_reference_id, + ex_max=row["ex_max"], + em_max=row["em_max"], + ext_coeff=row["ext_coeff"], + qy=row["qy"], + brightness=row["brightness"], + lifetime=row["lifetime"], + pka=row["pka"], + twop_ex_max=row["twop_ex_max"], + twop_peak_gm=row["twop_peakGM"], # Note: SQL column uses twop_peakGM + twop_qy=row["twop_qy"], + is_dark=row["is_dark"], + is_trusted=True, # Mark as trusted since it's the original data + created_by_id=row["created_by_id"], + updated_by_id=row["updated_by_id"], + ) print(f"Migrated {State.objects.count()} State records") @@ -173,8 +98,7 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N COALESCE((SELECT MAX(id) FROM proteins_fluorophore), 1) ) """) - fluor_seq = cursor.fetchone()[0] - print(f"Reset Fluorophore ID sequence to {fluor_seq}") + print(f"Reset Fluorophore ID sequence to {cursor.fetchone()[0]}") cursor.execute(""" SELECT setval( @@ -182,13 +106,11 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N COALESCE((SELECT MAX(id) FROM proteins_fluorescencemeasurement), 1) ) """) - meas_seq = cursor.fetchone()[0] - print(f"Reset FluorescenceMeasurement ID sequence to {meas_seq}") + print(f"Reset FluorescenceMeasurement ID sequence to {cursor.fetchone()[0]}") def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: """Migrate Dye data from old schema to new Dye container + DyeState structure.""" - Fluorophore = apps.get_model("proteins", "Fluorophore") Dye = apps.get_model("proteins", "Dye") DyeState = apps.get_model("proteins", "DyeState") FluorescenceMeasurement = apps.get_model("proteins", "FluorescenceMeasurement") @@ -196,146 +118,57 @@ def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> Non # Access old Dye data via raw SQL with schema_editor.connection.cursor() as cursor: cursor.execute(""" - SELECT id, created, modified, name, slug, is_dark, + SELECT created, modified, name, slug, is_dark, ex_max, em_max, ext_coeff, qy, brightness, lifetime, pka, twop_ex_max, "twop_peakGM", twop_qy FROM proteins_dye_old """) - for row in cursor.fetchall(): - ( - old_id, - created, - modified, - name, - slug, - is_dark, - ex_max, - em_max, - ext_coeff, - qy, - brightness, - lifetime, - pka, - twop_ex_max, - twop_peak_gm, - twop_qy, - ) = row - - # Handle empty/null slugs - if not slug or slug.strip() == "": - dye_slug = f"dye-{old_id}" # Use old ID as fallback - else: - dye_slug = slug - - # Ensure Dye slug uniqueness - original_dye_slug = dye_slug - base_dye_slug = dye_slug - counter = 1 - while Dye.objects.filter(slug=dye_slug).exists(): - dye_slug = f"{base_dye_slug}-{counter}" - counter += 1 - if counter > 100: - raise ValueError( - f"Could not generate unique slug for Dye {old_id} " - f"after 100 attempts (original: {original_dye_slug})" - ) - - if dye_slug != original_dye_slug: - logger.warning( - "Slug collision during Dye migration: %s -> %s (Dye ID: %s, Name: %s)", - original_dye_slug, - dye_slug, - old_id, - name, - ) - - # Old Dye schema doesn't have chemical structure fields - # Mark all as PROPRIETARY to avoid unique constraint issues - # (Can be updated later with actual chemical data) - + for row in _dictfetchall(cursor): # Create Dye container (without fluorescence properties) dye = Dye.objects.create( - created=created, - modified=modified, - name=name, - slug=dye_slug, + created=row["created"], + modified=row["modified"], + name=row["name"], + slug=row["slug"], ) - # Create Fluorophore for this DyeState - # Ensure unique fluorophore slug - fluorophore_slug = f"{dye_slug}-default" - original_fluor_slug = fluorophore_slug - base_fluor_slug = fluorophore_slug - counter = 1 - while Fluorophore.objects.filter(slug=fluorophore_slug).exists(): - fluorophore_slug = f"{base_fluor_slug}-{counter}" - counter += 1 - if counter > 100: - raise ValueError( - f"Could not generate unique fluorophore slug for Dye {old_id} " - f"after 100 attempts (original: {original_fluor_slug})" - ) - - if fluorophore_slug != original_fluor_slug: - logger.warning( - "Fluorophore slug collision during Dye migration: %s -> %s (Dye ID: %s, Name: %s)", - original_fluor_slug, - fluorophore_slug, - old_id, - name, - ) - # Create DyeState (MTI child of Fluorophore) - # Django's MTI creates both parent and child records in one operation - # Don't pass emhex/exhex - the save() method will compute them from wavelengths + # Note: Measurable fields (ex_max, em_max, qy, etc.) are left empty here + # and will be populated by rebuild_attributes() after creating FluorescenceMeasurement dyestate = DyeState.objects.create( - created=created, - modified=modified, + created=row["created"], + modified=row["modified"], name="default", - slug=fluorophore_slug, + slug=f"{dye.slug}-default", entity_type="d", owner_name=dye.name, owner_slug=dye.slug, - ex_max=ex_max, - em_max=em_max, - ext_coeff=ext_coeff, - qy=qy, - brightness=brightness, - lifetime=lifetime, - pka=pka, - twop_ex_max=twop_ex_max, - twop_peak_gm=twop_peak_gm, - twop_qy=twop_qy, - is_dark=is_dark, - # DyeState-specific field dye=dye, ) + # Create FluorescenceMeasurement from old Dye data + # For dyes, we don't have a primary_reference concept in old schema + FluorescenceMeasurement.objects.create( + fluorophore=dyestate, # DyeState is-a Fluorophore (MTI) + reference_id=None, + ex_max=row["ex_max"], + em_max=row["em_max"], + ext_coeff=row["ext_coeff"], + qy=row["qy"], + brightness=row["brightness"], + lifetime=row["lifetime"], + pka=row["pka"], + twop_ex_max=row["twop_ex_max"], + twop_peak_gm=row["twop_peakGM"], # Note: SQL column uses twop_peakGM + twop_qy=row["twop_qy"], + is_dark=row["is_dark"], + is_trusted=True, + ) + dye.default_state = dyestate dye.save() - # Create FluorescenceMeasurement from old Dye data - if any([ex_max, em_max, qy, ext_coeff, lifetime, pka]): - # For dyes, we don't have a primary_reference concept in old schema - # We'll leave reference as None for now - FluorescenceMeasurement.objects.create( - fluorophore=dyestate, # DyeState is-a Fluorophore (MTI) - reference_id=None, - ex_max=ex_max, - em_max=em_max, - ext_coeff=ext_coeff, - qy=qy, - brightness=brightness, - lifetime=lifetime, - pka=pka, - twop_ex_max=twop_ex_max, - twop_peak_gm=twop_peak_gm, - twop_qy=twop_qy, - is_dark=is_dark, - is_trusted=True, - ) - print(f"Migrated {Dye.objects.count()} Dye records to Dye containers") print(f"Created {DyeState.objects.count()} DyeState records") From d89dde223e6f644c4ea3328fb4425c9fc2c4b2a6 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Mon, 24 Nov 2025 19:14:51 -0500 Subject: [PATCH 31/57] fix data migration --- .../0059_add_fluorophore_and_new_models.py | 62 ++++++++++++------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py index 7eceab75b..91122f0a9 100644 --- a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py +++ b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py @@ -24,6 +24,21 @@ def _dictfetchall(cursor: CursorWrapper) -> list[dict[str, Any]]: columns = [col.name for col in cursor.description] return [dict(zip(columns, row)) for row in cursor.fetchall()] +MEASURABLE_FIELDS = { + "ex_max", + "em_max", + "ext_coeff", + "qy", + "brightness", + "lifetime", + "pka", + "twop_ex_max", + "twop_peakGM", + "twop_qy", + "is_dark", + "emhex", + "exhex", +} def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: """Migrate State data from old schema to new Fluorophore + State MTI structure.""" @@ -38,13 +53,16 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N SELECT id, created, modified, name, slug, is_dark, ex_max, em_max, ext_coeff, qy, brightness, lifetime, pka, twop_ex_max, "twop_peakGM", twop_qy, - maturation, protein_id, created_by_id, updated_by_id + maturation, protein_id, created_by_id, updated_by_id, emhex, exhex FROM proteins_state_old """) for row in _dictfetchall(cursor): # Extract fields by name (order-independent, safer than positional unpacking) old_id = row["id"] + measurables = {field: row[field] for field in MEASURABLE_FIELDS} + measurables['twop_peak_gm'] = measurables.pop('twop_peakGM') + # Get protein for label protein = Protein.objects.get(id=row["protein_id"]) @@ -62,6 +80,7 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N owner_slug=protein.slug, created_by_id=row["created_by_id"], updated_by_id=row["updated_by_id"], + **measurables, # State-specific fields protein_id=protein.id, maturation=row["maturation"], @@ -71,17 +90,7 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N id=old_id, # Preserve old ID fluorophore=state, # State is-a Fluorophore (MTI) reference_id=protein.primary_reference_id, - ex_max=row["ex_max"], - em_max=row["em_max"], - ext_coeff=row["ext_coeff"], - qy=row["qy"], - brightness=row["brightness"], - lifetime=row["lifetime"], - pka=row["pka"], - twop_ex_max=row["twop_ex_max"], - twop_peak_gm=row["twop_peakGM"], # Note: SQL column uses twop_peakGM - twop_qy=row["twop_qy"], - is_dark=row["is_dark"], + **measurables, is_trusted=True, # Mark as trusted since it's the original data created_by_id=row["created_by_id"], updated_by_id=row["updated_by_id"], @@ -120,17 +129,27 @@ def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> Non cursor.execute(""" SELECT created, modified, name, slug, is_dark, ex_max, em_max, ext_coeff, qy, brightness, - lifetime, pka, twop_ex_max, "twop_peakGM", twop_qy + lifetime, pka, twop_ex_max, "twop_peakGM", twop_qy, emhex, exhex, + manufacturer, part, url, created_by_id, updated_by_id FROM proteins_dye_old """) for row in _dictfetchall(cursor): + measurables = {field: row[field] for field in MEASURABLE_FIELDS} + measurables['twop_peak_gm'] = measurables.pop('twop_peakGM') + + # Create Dye container (without fluorescence properties) dye = Dye.objects.create( created=row["created"], + created_by_id=row["created_by_id"], + updated_by_id=row["updated_by_id"], modified=row["modified"], name=row["name"], slug=row["slug"], + manufacturer=row["manufacturer"], + part=row["part"], + url=row["url"], ) # Create DyeState (MTI child of Fluorophore) @@ -139,9 +158,12 @@ def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> Non dyestate = DyeState.objects.create( created=row["created"], modified=row["modified"], + created_by_id=row["created_by_id"], + updated_by_id=row["updated_by_id"], name="default", slug=f"{dye.slug}-default", entity_type="d", + **measurables, owner_name=dye.name, owner_slug=dye.slug, dye=dye, @@ -152,18 +174,10 @@ def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> Non FluorescenceMeasurement.objects.create( fluorophore=dyestate, # DyeState is-a Fluorophore (MTI) reference_id=None, - ex_max=row["ex_max"], - em_max=row["em_max"], - ext_coeff=row["ext_coeff"], - qy=row["qy"], - brightness=row["brightness"], - lifetime=row["lifetime"], - pka=row["pka"], - twop_ex_max=row["twop_ex_max"], - twop_peak_gm=row["twop_peakGM"], # Note: SQL column uses twop_peakGM - twop_qy=row["twop_qy"], - is_dark=row["is_dark"], + **measurables, is_trusted=True, + created_by_id=row["created_by_id"], + updated_by_id=row["updated_by_id"], ) dye.default_state = dyestate From aee348628962c7c950881474be0c9b5174a81e96 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 25 Nov 2025 09:38:47 -0500 Subject: [PATCH 32/57] fix reversion --- .../0059_add_fluorophore_and_new_models.py | 326 ++++++++++++++---- 1 file changed, 254 insertions(+), 72 deletions(-) diff --git a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py index 91122f0a9..a7cd24d7b 100644 --- a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py +++ b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py @@ -1,5 +1,17 @@ +"""Major data migration to new Fluorophore + Measurement schema. + +NON REVERSIBLE. + +1. proteins.State becomes MTI child of new proteins.Fluorophore +2. proteins.Dye becomes container for new proteins.DyeState (MTI child of Fluorophore) +3. proteins.FluorescenceMeasurement created for each old State and DyeState +4. proteins.Spectrum ownership updated to point to new Fluorophore records +5. proteins.OcFluorEff updated to point to new Fluorophore records + +""" from __future__ import annotations +import json import logging from typing import Any @@ -7,8 +19,6 @@ import django.utils.timezone import model_utils.fields from django.apps.registry import Apps - -# Generated manually for schema overhaul from django.core.validators import MaxValueValidator, MinValueValidator from django.db import migrations, models from django.db.backends.base.schema import BaseDatabaseSchemaEditor @@ -24,6 +34,7 @@ def _dictfetchall(cursor: CursorWrapper) -> list[dict[str, Any]]: columns = [col.name for col in cursor.description] return [dict(zip(columns, row)) for row in cursor.fetchall()] + MEASURABLE_FIELDS = { "ex_max", "em_max", @@ -40,6 +51,7 @@ def _dictfetchall(cursor: CursorWrapper) -> list[dict[str, Any]]: "exhex", } + def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: """Migrate State data from old schema to new Fluorophore + State MTI structure.""" # Get models from migration state @@ -61,7 +73,7 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N old_id = row["id"] measurables = {field: row[field] for field in MEASURABLE_FIELDS} - measurables['twop_peak_gm'] = measurables.pop('twop_peakGM') + measurables["twop_peak_gm"] = measurables.pop("twop_peakGM") # Get protein for label protein = Protein.objects.get(id=row["protein_id"]) @@ -118,16 +130,21 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N print(f"Reset FluorescenceMeasurement ID sequence to {cursor.fetchone()[0]}") -def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: - """Migrate Dye data from old schema to new Dye container + DyeState structure.""" +def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> dict[int, int]: + """Migrate Dye data from old schema to new Dye container + DyeState structure. + + Returns a mapping of old Dye ID → new Fluorophore ID for efficient FK updates. + """ Dye = apps.get_model("proteins", "Dye") DyeState = apps.get_model("proteins", "DyeState") FluorescenceMeasurement = apps.get_model("proteins", "FluorescenceMeasurement") + old_to_new_id_map = {} + # Access old Dye data via raw SQL with schema_editor.connection.cursor() as cursor: cursor.execute(""" - SELECT created, modified, name, slug, is_dark, + SELECT id, created, modified, name, slug, is_dark, ex_max, em_max, ext_coeff, qy, brightness, lifetime, pka, twop_ex_max, "twop_peakGM", twop_qy, emhex, exhex, manufacturer, part, url, created_by_id, updated_by_id @@ -135,9 +152,9 @@ def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> Non """) for row in _dictfetchall(cursor): + old_id = row["id"] measurables = {field: row[field] for field in MEASURABLE_FIELDS} - measurables['twop_peak_gm'] = measurables.pop('twop_peakGM') - + measurables["twop_peak_gm"] = measurables.pop("twop_peakGM") # Create Dye container (without fluorescence properties) dye = Dye.objects.create( @@ -183,102 +200,151 @@ def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> Non dye.default_state = dyestate dye.save() + # Store mapping for efficient FK updates + old_to_new_id_map[old_id] = dyestate.fluorophore_ptr_id + print(f"Migrated {Dye.objects.count()} Dye records to Dye containers") print(f"Created {DyeState.objects.count()} DyeState records") + return old_to_new_id_map -def update_spectrum_ownership(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: +def update_spectrum_ownership(apps: Apps, schema_editor: BaseDatabaseSchemaEditor, dye_id_map: dict[int, int]) -> None: """Update Spectrum foreign keys to point to new Fluorophore records.""" - apps.get_model("proteins", "Spectrum") - apps.get_model("proteins", "Fluorophore") - - # We need to map old State/Dye IDs to new Fluorophore IDs - # This is tricky because we need to query the old tables - with schema_editor.connection.cursor() as cursor: - # Update spectra that were owned by States + # Update spectra owned by States - State ID == Fluorophore ID (preserved) cursor.execute(""" - UPDATE proteins_spectrum s - SET owner_fluor_id = ( - SELECT f.id - FROM proteins_fluorophore f - JOIN proteins_state ns ON ns.fluorophore_ptr_id = f.id - JOIN proteins_state_old os ON os.slug = f.slug - WHERE os.id = s.owner_state_id - ) - WHERE s.owner_state_id IS NOT NULL + UPDATE proteins_spectrum + SET owner_fluor_id = owner_state_id + WHERE owner_state_id IS NOT NULL """) - state_count = cursor.rowcount print(f"Updated {state_count} spectra owned by States") - # Update spectra that were owned by Dyes - # Note: DyeState slug is "{dye_slug}-default", so we need to match carefully + # Update spectra owned by Dyes - need mapping from old Dye ID to new Fluorophore ID + # Use temp table for the mapping (reuse pattern from OcFluorEff) cursor.execute(""" - UPDATE proteins_spectrum s - SET owner_fluor_id = ( - SELECT f.id - FROM proteins_fluorophore f - JOIN proteins_dyestate ds ON ds.fluorophore_ptr_id = f.id - JOIN proteins_dye d ON d.id = ds.dye_id - JOIN proteins_dye_old od ON od.slug = d.slug - WHERE od.id = s.owner_dye_id + CREATE TEMP TABLE dye_spectrum_mapping ( + old_id INTEGER PRIMARY KEY, + new_id INTEGER NOT NULL ) - WHERE s.owner_dye_id IS NOT NULL """) + if dye_id_map: + values = ", ".join( + cursor.mogrify("(%s, %s)", (old_id, new_id)).decode() for old_id, new_id in dye_id_map.items() + ) + cursor.execute(f"INSERT INTO dye_spectrum_mapping (old_id, new_id) VALUES {values}") + + cursor.execute(""" + UPDATE proteins_spectrum s + SET owner_fluor_id = m.new_id + FROM dye_spectrum_mapping m + WHERE s.owner_dye_id = m.old_id + """) dye_count = cursor.rowcount print(f"Updated {dye_count} spectra owned by Dyes") + cursor.execute("DROP TABLE dye_spectrum_mapping") + + +def update_ocfluoreff(apps: Apps, schema_editor: BaseDatabaseSchemaEditor, dye_id_map: dict[int, int]) -> None: + """Update OcFluorEff to use direct FK to Fluorophore. -def update_ocfluoreff(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: - """Update OcFluorEff to use direct FK to Fluorophore.""" + Uses efficient bulk updates: + - States: Direct assignment (State ID == Fluorophore ID) + - Dyes: Temp table JOIN (using provided old_id → new_id mapping) + """ with schema_editor.connection.cursor() as cursor: - # Update OcFluorEff records that pointed to States via GenericFK + # Increase work_mem for efficient hash joins on large tables + cursor.execute("SET LOCAL work_mem = '256MB'") + + # Cache content_type IDs to avoid repeated subqueries cursor.execute(""" - UPDATE proteins_ocfluoreff o - SET fluor_id = ( - SELECT f.id - FROM proteins_fluorophore f - JOIN proteins_state s ON s.fluorophore_ptr_id = f.id - WHERE s.fluorophore_ptr_id IN ( - SELECT fluorophore_ptr_id - FROM proteins_state ps - JOIN proteins_state_old os ON os.slug = ( - SELECT slug FROM proteins_fluorophore WHERE id = ps.fluorophore_ptr_id - ) - WHERE os.id = o.object_id::integer - ) + SELECT id FROM django_content_type + WHERE app_label = 'proteins' AND model = 'state' + """) + state_ct_id = cursor.fetchone()[0] + + cursor.execute(""" + SELECT id FROM django_content_type + WHERE app_label = 'proteins' AND model = 'dye' + """) + dye_ct_id = cursor.fetchone()[0] + + # First, clean up orphaned records that point to deleted States/Dyes + # These have object_ids that don't exist in the old tables anymore + cursor.execute( + """ + DELETE FROM proteins_ocfluoreff o + WHERE o.content_type_id = %s + AND NOT EXISTS ( + SELECT 1 FROM proteins_state_old s WHERE s.id = o.object_id ) - WHERE o.content_type_id = ( - SELECT id FROM django_content_type - WHERE app_label = 'proteins' AND model = 'state' + """, + [state_ct_id], + ) + orphaned_states = cursor.rowcount + + cursor.execute( + """ + DELETE FROM proteins_ocfluoreff o + WHERE o.content_type_id = %s + AND NOT EXISTS ( + SELECT 1 FROM proteins_dye_old d WHERE d.id = o.object_id ) - """) + """, + [dye_ct_id], + ) + orphaned_dyes = cursor.rowcount + print(f"Deleted {orphaned_states + orphaned_dyes} orphaned OcFluorEff records") + + # Update OcFluorEff records that pointed to States via GenericFK + # Since State IDs were preserved, object_id directly maps to fluor_id + cursor.execute( + """ + UPDATE proteins_ocfluoreff + SET fluor_id = object_id + WHERE content_type_id = %s + """, + [state_ct_id], + ) state_count = cursor.rowcount print(f"Updated {state_count} OcFluorEff records that pointed to States") # Update OcFluorEff records that pointed to Dyes via GenericFK + # Use temp table with primary key for efficient hash join cursor.execute(""" - UPDATE proteins_ocfluoreff o - SET fluor_id = ( - SELECT f.id - FROM proteins_fluorophore f - JOIN proteins_dyestate ds ON ds.fluorophore_ptr_id = f.id - JOIN proteins_dye d ON d.id = ds.dye_id - JOIN proteins_dye_old od ON od.slug = d.slug - WHERE od.id = o.object_id::integer - ) - WHERE o.content_type_id = ( - SELECT id FROM django_content_type - WHERE app_label = 'proteins' AND model = 'dye' + CREATE TEMP TABLE dye_id_mapping ( + old_id INTEGER PRIMARY KEY, + new_id INTEGER NOT NULL ) """) + # Bulk insert using execute with VALUES - faster than executemany + if dye_id_map: + values = ", ".join( + cursor.mogrify("(%s, %s)", (old_id, new_id)).decode() for old_id, new_id in dye_id_map.items() + ) + cursor.execute(f"INSERT INTO dye_id_mapping (old_id, new_id) VALUES {values}") + + cursor.execute( + """ + UPDATE proteins_ocfluoreff o + SET fluor_id = m.new_id + FROM dye_id_mapping m + WHERE o.object_id = m.old_id + AND o.content_type_id = %s + """, + [dye_ct_id], + ) + dye_count = cursor.rowcount print(f"Updated {dye_count} OcFluorEff records that pointed to Dyes") + # Clean up temp table + cursor.execute("DROP TABLE dye_id_mapping") + def populate_emhex_exhex(apps, _schema_editor): """Populate emhex/exhex for all Fluorophore objects. @@ -300,14 +366,130 @@ def populate_emhex_exhex(apps, _schema_editor): print(f"Populated emhex/exhex for {len(fluorophores_to_update)} fluorophores") +def migrate_reversion_state_versions(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: + """Migrate django-reversion State versions to work with new MTI structure. + + Background: + ----------- + State now inherits from Fluorophore via Multi-Table Inheritance (MTI). + When reversion reverts a revision, it needs Version records for BOTH the parent + (Fluorophore) and child (State) models in the same revision. + + Old State versions stored ALL fields in one record: + {"model": "proteins.state", "fields": {"name": "x", "ex_max": 488, "protein": 1, ...}} + + After migration, we need TWO records per revision: + 1. Fluorophore: {"fields": {"name": "x", "ex_max": 488, ...}} + 2. State: {"fields": {"fluorophore_ptr": 402, "protein": 1, "maturation": 13.0}} + + Both records share the same revision_id, so revision.revert() restores them together. + """ + ContentType = apps.get_model("contenttypes", "ContentType") + + # Fields that remain on State model; everything else moves to Fluorophore parent + state_only_fields = {"protein", "maturation", "transitions"} + + with schema_editor.connection.cursor() as cursor: + state_ct = ContentType.objects.get(app_label="proteins", model="state") + fluor_ct, _ = ContentType.objects.get_or_create(app_label="proteins", model="fluorophore") + + # Build lookup dict for protein name/slug (used for owner_name/owner_slug) + cursor.execute("SELECT id, name, slug FROM proteins_protein") + proteins = {row[0]: (row[1], row[2]) for row in cursor.fetchall()} + + # Fetch all existing State version records + cursor.execute( + "SELECT id, object_id, revision_id, serialized_data, db, format " + "FROM reversion_version WHERE content_type_id = %s", + [state_ct.id], + ) + + fluor_inserts, state_updates = [], [] + + for row in _dictfetchall(cursor): + version_id = row["version_id"] + object_id = row["object_id"] + revision_id = row["revision_id"] + serialized_data = row["serialized_data"] + db = row["db"] + fmt = row["fmt"] + + try: + data = json.loads(serialized_data) + if not data: + continue + first_record: dict = data[0] + # data is a list of versioned objects + old_fields = first_record.get("fields", {}) + pk = first_record.get("pk", object_id) + + # Look up owner (protein) name/slug from the protein FK + protein_id = old_fields.get("protein") + owner_name, owner_slug = proteins.get(protein_id, ("", "")) + + # Split fields: State-only fields stay on State, rest go to Fluorophore + # State needs fluorophore_ptr to link to its MTI parent + state_fields = {"fluorophore_ptr": pk} + # Fluorophore needs new required fields with sensible defaults + fluor_fields = { + "entity_type": "p", + "owner_name": owner_name, + "owner_slug": owner_slug, + "source_map": "{}", + } + + for k, v in old_fields.items(): + (state_fields if k in state_only_fields else fluor_fields)[k] = v + + # Queue NEW Fluorophore version (same revision_id links them together) + fluor_inserts.append( + ( + str(object_id), + revision_id, # Same revision as the State version + fluor_ct.id, + json.dumps([{"model": "proteins.fluorophore", "pk": pk, "fields": fluor_fields}]), + "", # object_repr - not critical for historical versions + db, + fmt, + ) + ) + # Queue UPDATE to existing State version (strip out fields that moved to Fluorophore) + state_updates.append( + ( + json.dumps([{"model": "proteins.state", "pk": pk, "fields": state_fields}]), + version_id, + ) + ) + except (json.JSONDecodeError, KeyError, TypeError): + logger.warning(f"Skipping malformed reversion State version ID {row['id']}") + continue + + # Bulk insert new Fluorophore versions + if fluor_inserts: + cursor.executemany( + "INSERT INTO reversion_version " + "(object_id, revision_id, content_type_id, serialized_data, object_repr, db, format) " + "VALUES (%s, %s, %s, %s, %s, %s, %s)", + fluor_inserts, + ) + # Bulk update existing State versions to only contain State-specific fields + if state_updates: + cursor.executemany( + "UPDATE reversion_version SET serialized_data = %s WHERE id = %s", + state_updates, + ) + print(f"Migrated {len(fluor_inserts)} State versions for MTI") + + def migrate_forward(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: """Run all migration functions.""" print("Starting data migration from old schema...") migrate_state_data(apps, schema_editor) - migrate_dye_data(apps, schema_editor) - update_spectrum_ownership(apps, schema_editor) - update_ocfluoreff(apps, schema_editor) + dye_id_map = migrate_dye_data(apps, schema_editor) + update_spectrum_ownership(apps, schema_editor, dye_id_map) + update_ocfluoreff(apps, schema_editor, dye_id_map) populate_emhex_exhex(apps, schema_editor) + migrate_reversion_state_versions(apps, schema_editor) print("Data migration complete!") From 02061b12404819ace2b69f945318d01fd59e40c5 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 25 Nov 2025 10:05:21 -0500 Subject: [PATCH 33/57] add comments --- .../migrations/0059_add_fluorophore_and_new_models.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py index a7cd24d7b..6547f8e2e 100644 --- a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py +++ b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py @@ -397,22 +397,22 @@ def migrate_reversion_state_versions(apps: Apps, schema_editor: BaseDatabaseSche cursor.execute("SELECT id, name, slug FROM proteins_protein") proteins = {row[0]: (row[1], row[2]) for row in cursor.fetchall()} + fluor_inserts: list[tuple[str, str, str, str, str, str, str]] = [] + state_updates: list[tuple[str, str]] = [] + # Fetch all existing State version records cursor.execute( "SELECT id, object_id, revision_id, serialized_data, db, format " "FROM reversion_version WHERE content_type_id = %s", [state_ct.id], ) - - fluor_inserts, state_updates = [], [] - for row in _dictfetchall(cursor): - version_id = row["version_id"] + version_id = row["id"] object_id = row["object_id"] revision_id = row["revision_id"] serialized_data = row["serialized_data"] db = row["db"] - fmt = row["fmt"] + fmt = row["format"] try: data = json.loads(serialized_data) From 2bfdcadc03bafd7987e1a6e58e12ae8eb11aedf0 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 25 Nov 2025 10:22:47 -0500 Subject: [PATCH 34/57] fix tests --- .../0059_add_fluorophore_and_new_models.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py index 6547f8e2e..7da888c36 100644 --- a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py +++ b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py @@ -263,13 +263,20 @@ def update_ocfluoreff(apps: Apps, schema_editor: BaseDatabaseSchemaEditor, dye_i SELECT id FROM django_content_type WHERE app_label = 'proteins' AND model = 'state' """) - state_ct_id = cursor.fetchone()[0] + result = cursor.fetchone() + if result is None: + # No content types exist yet (fresh database), nothing to migrate + return + state_ct_id = result[0] cursor.execute(""" SELECT id FROM django_content_type WHERE app_label = 'proteins' AND model = 'dye' """) - dye_ct_id = cursor.fetchone()[0] + result = cursor.fetchone() + if result is None: + return + dye_ct_id = result[0] # First, clean up orphaned records that point to deleted States/Dyes # These have object_ids that don't exist in the old tables anymore @@ -390,7 +397,11 @@ def migrate_reversion_state_versions(apps: Apps, schema_editor: BaseDatabaseSche state_only_fields = {"protein", "maturation", "transitions"} with schema_editor.connection.cursor() as cursor: - state_ct = ContentType.objects.get(app_label="proteins", model="state") + try: + state_ct = ContentType.objects.get(app_label="proteins", model="state") + except ContentType.DoesNotExist: + # No content types exist yet (fresh database), nothing to migrate + return fluor_ct, _ = ContentType.objects.get_or_create(app_label="proteins", model="fluorophore") # Build lookup dict for protein name/slug (used for owner_name/owner_slug) From 918bc96f51448871cf97ee12d52b5c3d28636a36 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 25 Nov 2025 10:50:28 -0500 Subject: [PATCH 35/57] inline wavetohex --- .../0059_add_fluorophore_and_new_models.py | 52 ++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py index 7da888c36..304816e30 100644 --- a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py +++ b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py @@ -26,7 +26,6 @@ logger = logging.getLogger(__name__) - def _dictfetchall(cursor: CursorWrapper) -> list[dict[str, Any]]: """Return all rows from a cursor as a dict. Assume the column names are unique.""" if not cursor.description: @@ -359,7 +358,56 @@ def populate_emhex_exhex(apps, _schema_editor): The historical models don't include the save() logic from AbstractFluorescenceData, so we need to manually calculate these fields after creating the objects. """ - from proteins.util.helpers import wave_to_hex + + def wave_to_hex(wavelength, gamma=1): + """This converts a given wavelength into an approximate RGB value. + """ + if not wavelength: + return "#000" + + wavelength = float(wavelength) + if 520 <= wavelength: + wavelength += 40 + + if wavelength < 380: + r = 0.05 + g = 0.0 + b = 0.15 + elif wavelength >= 380 and wavelength <= 440: + attenuation = 0.3 + 0.7 * (wavelength - 380) / (440 - 380) + r = ((-(wavelength - 440) / (440 - 380)) * attenuation) ** gamma + g = 0.0 + b = (1.0 * attenuation) ** gamma + elif wavelength >= 440 and wavelength <= 490: + r = 0.0 + g = ((wavelength - 440) / (490 - 440)) ** gamma + b = 1.0 + elif wavelength >= 490 and wavelength <= 510: + r = 0.0 + g = 1.0 + b = (-(wavelength - 510) / (510 - 490)) ** gamma + elif wavelength >= 510 and wavelength <= 580: + r = ((wavelength - 510) / (580 - 510)) ** gamma + g = 1.0 + b = 0.0 + elif wavelength >= 580 and wavelength <= 645: + r = 1.0 + g = (-(wavelength - 645) / (645 - 580)) ** gamma + b = 0.0 + elif wavelength >= 645 and wavelength <= 750: + attenuation = 0.3 + 0.7 * (770 - wavelength) / (770 - 645) + r = (1.0 * attenuation) ** gamma + g = 0.0 + b = 0.0 + else: + r = 0.18 + g = 0.0 + b = 0.05 + r *= 255 + g *= 255 + b *= 255 + return f"#{int(r):02x}{int(g):02x}{int(b):02x}" + Fluorophore = apps.get_model("proteins", "Fluorophore") From 0663bb3c1e2ef35aff38fd46b6cae9a1abbc8fe4 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 25 Nov 2025 11:40:24 -0500 Subject: [PATCH 36/57] fix dye spectrum submit, add test --- backend/proteins/forms/spectrum.py | 108 ++++++++++++++++++----------- backend/tests_e2e/test_e2e.py | 35 +++++++--- 2 files changed, 96 insertions(+), 47 deletions(-) diff --git a/backend/proteins/forms/spectrum.py b/backend/proteins/forms/spectrum.py index 52d282f30..b311fcd33 100644 --- a/backend/proteins/forms/spectrum.py +++ b/backend/proteins/forms/spectrum.py @@ -8,7 +8,7 @@ from django.utils.safestring import mark_safe from django.utils.text import slugify -from proteins.models import Fluorophore, Spectrum, State +from proteins.models import Dye, DyeState, Fluorophore, Spectrum, State from proteins.util.helpers import zip_wave_data from proteins.util.importers import text_to_spectra from proteins.validators import validate_spectrum @@ -27,9 +27,10 @@ def __init__(self, *args, **kwargs): class SpectrumForm(forms.ModelForm): + # Lookup for non-protein, non-dye categories (filter/camera/light) + # Proteins use owner_fluor Select2 field directly + # Dyes are handled specially in save() and clean_owner() lookup = { - Spectrum.DYE: ("owner_fluor", "DyeState"), - Spectrum.PROTEIN: ("owner_fluor", "State"), Spectrum.FILTER: ("owner_filter", "Filter"), Spectrum.CAMERA: ("owner_camera", "Camera"), Spectrum.LIGHT: ("owner_light", "Light"), @@ -128,12 +129,33 @@ def clean(self): def save(self, commit=True): cat = self.cleaned_data.get("category") - if cat != Spectrum.PROTEIN: + if cat == Spectrum.DYE: + # Dyes require special handling: create Dye first, then DyeState + # This mirrors protein behavior where State belongs to Protein + # TODO: unify dye/protein behavior - currently users can create dyes + # but not proteins. Consider using Select2 for dyes too. + owner_name = self.cleaned_data.get("owner") + dye, created = Dye.objects.get_or_create( + slug=slugify(owner_name), + defaults={"name": owner_name, "created_by": self.user}, + ) + if not created: + dye.updated_by = self.user + dye.save() + # Get or create the default DyeState for this dye + dye_state, _ = DyeState.objects.get_or_create( + dye=dye, + name=Fluorophore.DEFAULT_NAME, + defaults={"created_by": self.user}, + ) + self.instance.owner_fluor = dye_state + elif cat != Spectrum.PROTEIN: + # Filter, Camera, Light - create owner directly owner_model = apps.get_model("proteins", self.lookup[cat][1]) owner_name = self.cleaned_data.get("owner") - owner_obj, c = owner_model.objects.get_or_create(name=owner_name, defaults={"created_by": self.user}) - if not c: - owner_obj.update_by = self.user + owner_obj, created = owner_model.objects.get_or_create(name=owner_name, defaults={"created_by": self.user}) + if not created: + owner_obj.updated_by = self.user owner_obj.save() setattr(self.instance, self.lookup[cat][0], owner_obj) self.instance.created_by = self.user @@ -169,7 +191,9 @@ def clean_file(self): def clean_owner_fluor(self): owner_fluor = self.cleaned_data.get("owner_fluor") stype = self.cleaned_data.get("subtype") - if self.cleaned_data.get("category") in (Spectrum.PROTEIN, Spectrum.DYE): + # Only proteins use the owner_fluor Select2 field + # Dyes use the owner text field and are validated in clean_owner() + if self.cleaned_data.get("category") == Spectrum.PROTEIN and owner_fluor: spectra = Spectrum.objects.all_objects().filter(owner_fluor=owner_fluor, subtype=stype) if spectra.exists(): first = spectra.first() @@ -196,6 +220,26 @@ def clean_owner(self): if cat == Spectrum.PROTEIN: return owner + # Dyes need special handling: look up Dye, then check its default DyeState + if cat == Spectrum.DYE: + try: + dye = Dye.objects.get(slug=slugify(owner)) + except Dye.DoesNotExist: + return owner + dye_state = dye.states.filter(name=Fluorophore.DEFAULT_NAME).first() + if dye_state and dye_state.spectra.filter(subtype=stype).exists(): + stype_display = dye_state.spectra.filter(subtype=stype).first().get_subtype_display() + self.add_error( + "owner", + forms.ValidationError( + "A dye with the name %(name)s already has a %(stype)s spectrum", + params={"name": dye.name, "stype": stype_display}, + code="owner_exists", + ), + ) + return owner + + # Filter, Camera, Light - look up by slug directly try: mod = apps.get_model("proteins", self.lookup[cat][1]) obj = mod.objects.get(slug=slugify(owner)) @@ -206,34 +250,20 @@ def clean_owner(self): # throw an error prior to this point if not cat: raise forms.ValidationError("Category not provided") from e - else: - raise forms.ValidationError("Category not recognized") from e - else: - # object exists... check if it has this type of spectrum - exists = False - if isinstance(obj, Fluorophore): - if obj.spectra.filter(subtype=stype).exists(): - exists = True - stype = obj.spectra.filter(subtype=stype).first().get_subtype_display() - elif hasattr(obj, "spectrum") and obj.spectrum: - exists = True - stype = obj.spectrum.get_subtype_display() - if exists: - self.add_error( - "owner", - forms.ValidationError( - "A %(model)s with the name %(name)s already has a %(stype)s spectrum", - params={ - "model": self.lookup[cat][1].lower(), - "name": obj.name, - "stype": stype, - }, - code="owner_exists", - ), - ) - # raise forms.ValidationError( - # "A %(model)s with the slug %(slug)s already has a spectrum of type %(stype)s.", - # params={'model': self.lookup[cat][1].lower(), 'slug': slugify(owner), 'stype': stype}, - # code='owner_exists') - else: - return owner + raise forms.ValidationError("Category not recognized") from e + + # object exists... check if it has this type of spectrum + if hasattr(obj, "spectrum") and obj.spectrum: + self.add_error( + "owner", + forms.ValidationError( + "A %(model)s with the name %(name)s already has a %(stype)s spectrum", + params={ + "model": self.lookup[cat][1].lower(), + "name": obj.name, + "stype": obj.spectrum.get_subtype_display(), + }, + code="owner_exists", + ), + ) + return owner diff --git a/backend/tests_e2e/test_e2e.py b/backend/tests_e2e/test_e2e.py index d3b5a2c1d..43c3bfea5 100644 --- a/backend/tests_e2e/test_e2e.py +++ b/backend/tests_e2e/test_e2e.py @@ -96,12 +96,28 @@ def test_main_page_loads_with_assets(live_server: LiveServer, page: Page, assert assert_snapshot(page) +@pytest.mark.parametrize( + ("category", "subtype", "owner_name", "uses_select2"), + [ + pytest.param(Spectrum.PROTEIN, Spectrum.EX, "SubmitTestProtein", True, id="protein"), + pytest.param(Spectrum.DYE, Spectrum.EM, "SubmitTestDye", False, id="dye"), + pytest.param(Spectrum.FILTER, Spectrum.LP, "SubmitTestFilter", False, id="filter"), + ], +) def test_spectrum_submission_preview_manual_data( - auth_page: Page, live_server: LiveServer, assert_snapshot: Callable + auth_page: Page, + live_server: LiveServer, + assert_snapshot: Callable, + category: str, + subtype: str, + owner_name: str, + uses_select2: bool, ) -> None: - """Test spectrum submission form with manual data preview.""" - protein = ProteinFactory.create() - protein.default_state.ex_spectrum.delete() + """Test spectrum submission form with manual data preview for different categories.""" + # Create owner object if needed (protein requires existing DB entry for Select2) + if category == Spectrum.PROTEIN: + protein = ProteinFactory.create(name=owner_name) + protein.default_state.ex_spectrum.delete() url = f"{live_server.url}{reverse('proteins:submit-spectra')}" auth_page.goto(url) @@ -109,12 +125,15 @@ def test_spectrum_submission_preview_manual_data( # Wait for form to be fully initialized expect(auth_page.locator("#spectrum-submit-form[data-form-ready='true']")).to_be_attached() - auth_page.locator("#id_category").select_option(Spectrum.PROTEIN) - auth_page.locator("#id_subtype").select_option(Spectrum.EX) + auth_page.locator("#id_category").select_option(category) + auth_page.locator("#id_subtype").select_option(subtype) auth_page.locator("#id_confirmation").check() - # Select2 autocomplete for protein - _select2_enter("#div_id_owner_fluor [role='combobox']", protein.name, auth_page) + # Fill owner field - different methods depending on category + if uses_select2: + _select2_enter("#div_id_owner_fluor [role='combobox']", owner_name, auth_page) + else: + auth_page.locator("#id_owner").fill(owner_name) # Switch to manual data tab and enter data auth_page.locator("#manual-tab").click() From 73ea24335736923f38c5f02c977622254f2dbb5b Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 25 Nov 2025 12:19:40 -0500 Subject: [PATCH 37/57] slugify dye --- backend/proteins/models/dye.py | 2 ++ backend/proteins/models/fluorophore.py | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/backend/proteins/models/dye.py b/backend/proteins/models/dye.py index e58d66ab7..e46f194ed 100644 --- a/backend/proteins/models/dye.py +++ b/backend/proteins/models/dye.py @@ -1,6 +1,7 @@ from typing import TYPE_CHECKING from django.db import models +from django.utils.text import slugify from model_utils.models import TimeStampedModel from proteins.models.fluorophore import Fluorophore @@ -37,6 +38,7 @@ class Meta: abstract = False def save(self, *args, **kwargs): + self.slug = slugify(self.name) # Always regenerate, like Protein.save() super().save(*args, **kwargs) # Update cached owner fields on all states when dye name/slug changes # These fields are cached on Fluorophore for query performance diff --git a/backend/proteins/models/fluorophore.py b/backend/proteins/models/fluorophore.py index 3f9f4cc2a..43bcec929 100644 --- a/backend/proteins/models/fluorophore.py +++ b/backend/proteins/models/fluorophore.py @@ -3,6 +3,7 @@ from typing import TYPE_CHECKING, Final, Literal from django.db import models +from django.utils.text import slugify from proteins.models.fluorescence_data import AbstractFluorescenceData @@ -95,6 +96,12 @@ class Meta: def __str__(self): return self.label + def save(self, *args, **kwargs): + # Auto-generate slug from owner_slug + state name if not set + if not self.slug and self.owner_slug: + self.slug = slugify(f"{self.owner_slug}-{self.name}") + super().save(*args, **kwargs) + @property def label(self) -> str: """Human-readable display name: 'EGFP' or 'mEos3.2 (red)'.""" From a77e36c465d7d2a5e35883cadcb261aaac072bd1 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 25 Nov 2025 12:30:58 -0500 Subject: [PATCH 38/57] fix exband --- backend/proteins/models/fluorophore.py | 14 +++++++++----- backend/proteins/models/spectrum.py | 6 +++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/backend/proteins/models/fluorophore.py b/backend/proteins/models/fluorophore.py index 43bcec929..859daf68a 100644 --- a/backend/proteins/models/fluorophore.py +++ b/backend/proteins/models/fluorophore.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Final, Literal +from typing import TYPE_CHECKING, Final from django.db import models from django.utils.text import slugify @@ -220,11 +220,15 @@ def has_spectra(self) -> bool: return True return False - def ex_band(self, height=0.7) -> tuple[float, float] | Literal[False]: - return self.ex_spectrum.width(height) + def ex_band(self, height=0.7) -> tuple[float, float] | None: + if (spect := self.ex_spectrum) is not None: + return spect.width(height) + return None - def em_band(self, height=0.7) -> tuple[float, float] | Literal[False]: - return self.em_spectrum.width(height) + def em_band(self, height=0.7) -> tuple[float, float] | None: + if (spect := self.em_spectrum) is not None: + return spect.width(height) + return None def within_ex_band(self, value, height=0.7) -> bool: if band := self.ex_band(height): diff --git a/backend/proteins/models/spectrum.py b/backend/proteins/models/spectrum.py index aeb5e5f8c..3cd971bc2 100644 --- a/backend/proteins/models/spectrum.py +++ b/backend/proteins/models/spectrum.py @@ -4,7 +4,7 @@ import json import logging from functools import cached_property -from typing import TYPE_CHECKING, Any, Literal +from typing import TYPE_CHECKING, Any import django.forms import numpy as np @@ -570,13 +570,13 @@ def avg(self, waverange): d = self.waverange(waverange) return np.mean([i[1] for i in d]) - def width(self, height=0.5) -> tuple[float, float] | Literal[False]: + def width(self, height=0.5) -> tuple[float, float] | None: try: upindex = next(x[0] for x in enumerate(self.y) if x[1] > height) downindex = len(self.y) - next(x[0] for x in enumerate(reversed(self.y)) if x[1] > height) return (self.x[upindex], self.x[downindex]) except Exception: - return False + return None def d3dict(self) -> D3Dict: D: D3Dict = { From f7c2a989981860c8144c232d79d13b3b19514ce5 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 25 Nov 2025 12:38:56 -0500 Subject: [PATCH 39/57] fix help text for dark state and update timer/other labels --- .../migrations/0059_add_fluorophore_and_new_models.py | 2 +- backend/proteins/models/fluorescence_data.py | 2 +- backend/proteins/models/protein.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py index 304816e30..8c5170e36 100644 --- a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py +++ b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py @@ -587,7 +587,7 @@ def abstract_fluorescence_data_fields(): ( "is_dark", models.BooleanField( - default=False, verbose_name="Dark State", help_text="This state does not fluorescence" + default=False, verbose_name="Dark State", help_text="This state does not fluoresce" ), ), ( diff --git a/backend/proteins/models/fluorescence_data.py b/backend/proteins/models/fluorescence_data.py index 1ac00b8a7..d8fa2f7d6 100644 --- a/backend/proteins/models/fluorescence_data.py +++ b/backend/proteins/models/fluorescence_data.py @@ -84,7 +84,7 @@ class AbstractFluorescenceData(Authorable, TimeStampedModel, models.Model): is_dark = models.BooleanField( default=False, verbose_name="Dark State", - help_text="This state does not fluorescence", + help_text="This state does not fluoresce", ) class Meta: diff --git a/backend/proteins/models/protein.py b/backend/proteins/models/protein.py index a0f687a91..8457c897d 100644 --- a/backend/proteins/models/protein.py +++ b/backend/proteins/models/protein.py @@ -130,8 +130,8 @@ class SwitchingChoices(models.TextChoices): PHOTOSWITCHABLE = ("ps", "Photoswitchable") PHOTOCONVERTIBLE = ("pc", "Photoconvertible") MULTIPHOTOCHROMIC = ("mp", "Multi-photochromic") - TIMER = ("t", "Multistate") - OTHER = ("o", "Timer") + TIMER = ("t", "Timer") + OTHER = ("o", "Multistate") class CofactorChoices(models.TextChoices): BILIRUBIN = ("br", "Bilirubin") @@ -589,7 +589,7 @@ class State(Fluorophore): # TODO: rename to ProteinState ) def save(self, *args, **kwargs) -> None: - self.entity_type = self.EntityTypes.PROTEIN + self.entity_type = Fluorophore.EntityTypes.PROTEIN # Cache parent protein info for efficient searching if self.protein_id: self.owner_name = self.protein.name From 87344bc1a267d7993ea27db8512aeb19a82741c2 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 25 Nov 2025 12:49:04 -0500 Subject: [PATCH 40/57] change hints --- backend/favit/models.py | 6 ++--- backend/fpbase/users/models.py | 2 +- backend/proteins/models/bleach.py | 6 ++--- backend/proteins/models/collection.py | 8 ++++--- backend/proteins/models/dye.py | 5 ++-- backend/proteins/models/efficiency.py | 8 ++++--- backend/proteins/models/excerpt.py | 2 +- .../models/fluorescence_measurement.py | 10 ++++---- backend/proteins/models/lineage.py | 8 ++++--- backend/proteins/models/microscope.py | 22 ++++++++--------- backend/proteins/models/mixins.py | 4 ++-- backend/proteins/models/oser.py | 6 ++--- backend/proteins/models/protein.py | 24 ++++++++++--------- backend/proteins/models/spectrum.py | 10 ++++---- backend/proteins/models/transition.py | 8 +++---- backend/references/models.py | 12 +++++----- 16 files changed, 75 insertions(+), 66 deletions(-) diff --git a/backend/favit/models.py b/backend/favit/models.py index 38ec2a619..e805df09b 100644 --- a/backend/favit/models.py +++ b/backend/favit/models.py @@ -11,18 +11,18 @@ from .managers import FavoriteManager if TYPE_CHECKING: - from fpbase.users.models import User # noqa F401 + from fpbase.users.models import User class Favorite(models.Model): user_id: int - user = models.ForeignKey["User"]( + user: models.ForeignKey[User] = models.ForeignKey( getattr(settings, "AUTH_USER_MODEL", "auth.User"), related_name="favorites", on_delete=models.CASCADE, ) target_content_type_id: int - target_content_type = models.ForeignKey[ContentType](ContentType, on_delete=models.CASCADE) + 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) diff --git a/backend/fpbase/users/models.py b/backend/fpbase/users/models.py index b09885ccb..0a708210f 100644 --- a/backend/fpbase/users/models.py +++ b/backend/fpbase/users/models.py @@ -48,7 +48,7 @@ class UserLogin(models.Model): """Represent users' logins, one per record""" user_id: int - user = models.ForeignKey[User](User, on_delete=models.CASCADE, related_name="logins") + 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: diff --git a/backend/proteins/models/bleach.py b/backend/proteins/models/bleach.py index cb6cb60f9..4e5fc1879 100644 --- a/backend/proteins/models/bleach.py +++ b/backend/proteins/models/bleach.py @@ -10,7 +10,7 @@ from references.models import Reference if TYPE_CHECKING: - from proteins.models import State # noqa F401 + from proteins.models import State class BleachMeasurement(Authorable, TimeStampedModel): @@ -95,7 +95,7 @@ class BleachMeasurement(Authorable, TimeStampedModel): ) cell_type = models.CharField(max_length=60, blank=True, verbose_name="Cell Type", help_text="e.g. HeLa") reference_id: int | None - reference = models.ForeignKey[Reference | None]( + reference: models.ForeignKey[Reference | None] = models.ForeignKey( Reference, related_name="bleach_measurements", verbose_name="Measurement Reference", @@ -105,7 +105,7 @@ class BleachMeasurement(Authorable, TimeStampedModel): help_text="Reference where the measurement was made", ) # usually, the original paper that published the protein state_id: int - state = models.ForeignKey["State"]( + state: models.ForeignKey[State] = models.ForeignKey( "State", related_name="bleach_measurements", verbose_name="Protein (state)", diff --git a/backend/proteins/models/collection.py b/backend/proteins/models/collection.py index 549a20aa6..69ce80ed5 100644 --- a/backend/proteins/models/collection.py +++ b/backend/proteins/models/collection.py @@ -9,7 +9,9 @@ from model_utils.models import TimeStampedModel if TYPE_CHECKING: - from proteins.models import Dye, Microscope, Protein # noqa: F401 + from django.contrib.auth.models import AbstractUser + + from proteins.models import Dye, Microscope, Protein User = get_user_model() @@ -18,7 +20,7 @@ class OwnedCollection(TimeStampedModel): name = models.CharField(max_length=100) description = models.CharField(max_length=512, blank=True) owner_id: int | None - owner = models.ForeignKey[User | None]( + owner: models.ForeignKey[AbstractUser | None] = models.ForeignKey( User, blank=True, null=True, @@ -65,7 +67,7 @@ class Meta: class FluorophoreCollection(ProteinCollection): if TYPE_CHECKING: - dyes = models.ManyToManyField["Dye", "FluorophoreCollection"]() + dyes: models.ManyToManyField[Dye, FluorophoreCollection] = models.ManyToManyField() fluor_on_scope: models.QuerySet[Microscope] else: dyes = models.ManyToManyField("Dye", blank=True, related_name="collection_memberships") diff --git a/backend/proteins/models/dye.py b/backend/proteins/models/dye.py index e46f194ed..fd119e7f3 100644 --- a/backend/proteins/models/dye.py +++ b/backend/proteins/models/dye.py @@ -9,7 +9,6 @@ if TYPE_CHECKING: from proteins.models import DyeState - from references.models import Reference # noqa: F401 class Dye(Authorable, TimeStampedModel, Product): # TODO: rename to SmallMolecule @@ -23,7 +22,7 @@ class Dye(Authorable, TimeStampedModel, Product): # TODO: rename to SmallMolecu slug = models.SlugField(unique=True) default_state_id: int | None - default_state = models.ForeignKey["DyeState | None"]( + default_state: models.ForeignKey["DyeState | None"] = models.ForeignKey( "DyeState", related_name="default_for", blank=True, @@ -60,7 +59,7 @@ class DyeState(Fluorophore): """ dye_id: int - dye = models.ForeignKey["Dye"](Dye, on_delete=models.CASCADE, related_name="states") + dye: models.ForeignKey["Dye"] = models.ForeignKey(Dye, on_delete=models.CASCADE, related_name="states") if TYPE_CHECKING: fluorophore_ptr: Fluorophore # added by Django MTI diff --git a/backend/proteins/models/efficiency.py b/backend/proteins/models/efficiency.py index a80ba13e4..d4a3e7701 100644 --- a/backend/proteins/models/efficiency.py +++ b/backend/proteins/models/efficiency.py @@ -9,7 +9,7 @@ from proteins.util.efficiency import oc_efficiency_report if TYPE_CHECKING: - from proteins.models import OpticalConfig # noqa F401 + from proteins.models import OpticalConfig class OcFluorEffQuerySet(models.QuerySet): @@ -28,9 +28,11 @@ def outdated(self): class OcFluorEff(TimeStampedModel): oc_id: int - oc = models.ForeignKey["OpticalConfig"]("OpticalConfig", on_delete=models.CASCADE) + oc: models.ForeignKey["OpticalConfig"] = models.ForeignKey("OpticalConfig", on_delete=models.CASCADE) fluor_id: int - fluor = models.ForeignKey[Fluorophore](Fluorophore, on_delete=models.CASCADE, related_name="oc_effs") + fluor: models.ForeignKey[Fluorophore] = models.ForeignKey( + Fluorophore, on_delete=models.CASCADE, related_name="oc_effs" + ) fluor_name = models.CharField(max_length=100, blank=True) ex_eff = models.FloatField( null=True, diff --git a/backend/proteins/models/excerpt.py b/backend/proteins/models/excerpt.py index 1adcc4ea1..621aeff2f 100644 --- a/backend/proteins/models/excerpt.py +++ b/backend/proteins/models/excerpt.py @@ -22,7 +22,7 @@ class Excerpt(Authorable, TimeStampedModel, StatusModel): else: proteins = models.ManyToManyField("Protein", blank=True, related_name="excerpts") reference_id: int | None - reference = models.ForeignKey[Reference | None]( + reference: models.ForeignKey[Reference | None] = models.ForeignKey( Reference, related_name="excerpts", null=True, diff --git a/backend/proteins/models/fluorescence_measurement.py b/backend/proteins/models/fluorescence_measurement.py index c002f4948..e11badc63 100644 --- a/backend/proteins/models/fluorescence_measurement.py +++ b/backend/proteins/models/fluorescence_measurement.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import TYPE_CHECKING from django.db import models @@ -5,8 +7,8 @@ from proteins.models.fluorescence_data import AbstractFluorescenceData if TYPE_CHECKING: - from proteins.models import Fluorophore # noqa: F401 - from references.models import Reference # noqa: F401 + from proteins.models import Fluorophore + from references.models import Reference # The "evidence" @@ -14,11 +16,11 @@ class FluorescenceMeasurement(AbstractFluorescenceData): """Raw data points from a specific reference.""" fluorophore_id: int - fluorophore = models.ForeignKey["Fluorophore"]( + fluorophore: models.ForeignKey[Fluorophore] = models.ForeignKey( "Fluorophore", related_name="measurements", on_delete=models.CASCADE ) reference_id: int | None - reference = models.ForeignKey["Reference | None"]( + reference: models.ForeignKey[Reference | None] = models.ForeignKey( "references.Reference", on_delete=models.CASCADE, null=True, diff --git a/backend/proteins/models/lineage.py b/backend/proteins/models/lineage.py index 886370593..26b3987ab 100644 --- a/backend/proteins/models/lineage.py +++ b/backend/proteins/models/lineage.py @@ -39,11 +39,13 @@ def get_prep_value(self, value): class Lineage(MPTTModel, TimeStampedModel, Authorable): protein_id: int - protein = models.OneToOneField["Protein"]("Protein", on_delete=models.CASCADE, related_name="lineage") + protein: models.OneToOneField[Protein] = models.OneToOneField( + "Protein", on_delete=models.CASCADE, related_name="lineage" + ) parent_id: int | None parent = TreeForeignKey("self", on_delete=models.CASCADE, null=True, blank=True, related_name="children") reference_id: int | None - reference = models.ForeignKey[Reference | None]( + reference: models.ForeignKey[Reference | None] = models.ForeignKey( Reference, on_delete=models.CASCADE, null=True, @@ -53,7 +55,7 @@ class Lineage(MPTTModel, TimeStampedModel, Authorable): mutation = MutationSetField(max_length=400, blank=True) rootmut = models.CharField(max_length=400, blank=True) root_node_id: int | None - root_node = models.ForeignKey["Lineage | None"]( + root_node: models.ForeignKey[Lineage | None] = models.ForeignKey( "self", null=True, on_delete=models.CASCADE, diff --git a/backend/proteins/models/microscope.py b/backend/proteins/models/microscope.py index 76e762936..171dcb6c2 100644 --- a/backend/proteins/models/microscope.py +++ b/backend/proteins/models/microscope.py @@ -21,7 +21,7 @@ if TYPE_CHECKING: from django.db.models.manager import RelatedManager - from proteins.models.collection import FluorophoreCollection, ProteinCollection # noqa: F401 + from proteins.models.collection import FluorophoreCollection, ProteinCollection from proteins.models.spectrum import D3Dict, Spectrum @@ -37,8 +37,8 @@ class Microscope(OwnedCollection): id = models.CharField(primary_key=True, max_length=22, default=shortuuid, editable=False) if TYPE_CHECKING: - extra_lights = models.ManyToManyField["Light", "Microscope"]() - extra_cameras = models.ManyToManyField["Camera", "Microscope"]() + extra_lights: models.ManyToManyField[Light, Microscope] = models.ManyToManyField() + extra_cameras: models.ManyToManyField[Camera, Microscope] = models.ManyToManyField() else: extra_lights = models.ManyToManyField("Light", blank=True, related_name="microscopes") extra_cameras = models.ManyToManyField("Camera", blank=True, related_name="microscopes") @@ -48,7 +48,7 @@ class Microscope(OwnedCollection): blank=True, ) collection_id: int | None - collection = models.ForeignKey["ProteinCollection | None"]( + collection: models.ForeignKey[ProteinCollection | None] = models.ForeignKey( "ProteinCollection", blank=True, null=True, @@ -56,7 +56,7 @@ class Microscope(OwnedCollection): on_delete=models.CASCADE, ) fluors_id: int | None - fluors = models.ForeignKey["FluorophoreCollection | None"]( + fluors: models.ForeignKey[FluorophoreCollection | None] = models.ForeignKey( "FluorophoreCollection", blank=True, null=True, @@ -230,7 +230,7 @@ class OpticalConfig(OwnedCollection): """A a single optical configuration comprising a set of filters""" microscope_id: int - microscope = models.ForeignKey["Microscope"]( + microscope: models.ForeignKey[Microscope] = models.ForeignKey( "Microscope", related_name="optical_configs", on_delete=models.CASCADE, @@ -239,7 +239,7 @@ class OpticalConfig(OwnedCollection): if TYPE_CHECKING: from proteins.models import OcFluorEff - filters = models.ManyToManyField["Filter", "OpticalConfig"]() + filters: models.ManyToManyField[Filter, OpticalConfig] = models.ManyToManyField() filterplacement_set: models.QuerySet[FilterPlacement] ocfluoreff_set: models.QuerySet[OcFluorEff] else: @@ -250,7 +250,7 @@ class OpticalConfig(OwnedCollection): through="FilterPlacement", ) light_id: int | None - light = models.ForeignKey["Light"]( + light: models.ForeignKey[Light] = models.ForeignKey( "Light", null=True, blank=True, @@ -258,7 +258,7 @@ class OpticalConfig(OwnedCollection): on_delete=models.SET_NULL, ) camera_id: int | None - camera = models.ForeignKey["Camera"]( + camera: models.ForeignKey[Camera] = models.ForeignKey( "Camera", null=True, blank=True, @@ -358,9 +358,9 @@ class FilterPlacement(models.Model): PATH_CHOICES = ((EX, "Excitation Path"), (EM, "Emission Path"), (BS, "Both Paths")) filter_id: int - filter = models.ForeignKey[Filter]("Filter", on_delete=models.CASCADE) + filter: models.ForeignKey[Filter] = models.ForeignKey("Filter", on_delete=models.CASCADE) config_id: int - config = models.ForeignKey["OpticalConfig"]("OpticalConfig", on_delete=models.CASCADE) + config: models.ForeignKey[OpticalConfig] = models.ForeignKey("OpticalConfig", on_delete=models.CASCADE) path = models.CharField(max_length=2, choices=PATH_CHOICES, verbose_name="Ex/Bs/Em Path") # when path == BS, reflects refers to the emission path reflects = models.BooleanField(default=False, help_text="Filter reflects emission (if BS or EM filter)") diff --git a/backend/proteins/models/mixins.py b/backend/proteins/models/mixins.py index ca5491258..f780eb005 100644 --- a/backend/proteins/models/mixins.py +++ b/backend/proteins/models/mixins.py @@ -17,7 +17,7 @@ def get_admin_url(self): class Authorable(models.Model): created_by_id: int | None - created_by = models.ForeignKey["User | None"]( + created_by: models.ForeignKey["User | None"] = models.ForeignKey( User, blank=True, null=True, @@ -25,7 +25,7 @@ class Authorable(models.Model): on_delete=models.SET_NULL, ) updated_by_id: int | None - updated_by = models.ForeignKey["User | None"]( + updated_by: models.ForeignKey["User | None"] = models.ForeignKey( User, blank=True, null=True, diff --git a/backend/proteins/models/oser.py b/backend/proteins/models/oser.py index cbfff823a..e93a43ce9 100644 --- a/backend/proteins/models/oser.py +++ b/backend/proteins/models/oser.py @@ -8,7 +8,7 @@ from references.models import Reference if TYPE_CHECKING: - from proteins.models import Protein # noqa F401 + from proteins.models import Protein class OSERMeasurement(Authorable, TimeStampedModel): @@ -61,7 +61,7 @@ class OSERMeasurement(Authorable, TimeStampedModel): temp = models.FloatField(null=True, blank=True, verbose_name="Temperature") reference_id: int | None - reference = models.ForeignKey["Reference | None"]( + reference: models.ForeignKey["Reference | None"] = models.ForeignKey( Reference, related_name="oser_measurements", verbose_name="Measurement Reference", @@ -71,7 +71,7 @@ class OSERMeasurement(Authorable, TimeStampedModel): help_text="Reference where the measurement was made", ) # usually, the original paper that published the protein protein_id: int - protein = models.ForeignKey["Protein"]( + protein: models.ForeignKey["Protein"] = models.ForeignKey( "Protein", related_name="oser_measurements", verbose_name="Protein", diff --git a/backend/proteins/models/protein.py b/backend/proteins/models/protein.py index 8457c897d..adda5a504 100644 --- a/backend/proteins/models/protein.py +++ b/backend/proteins/models/protein.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime import io import json @@ -39,7 +41,7 @@ from django.db.models.manager import RelatedManager from reversion.models import VersionQuerySet - from proteins.models import ( # noqa: F401 + from proteins.models import ( BleachMeasurement, Excerpt, Lineage, @@ -225,7 +227,7 @@ class CofactorChoices(models.TextChoices): # Relations parent_organism_id: int | None - parent_organism = models.ForeignKey["Organism | None"]( + parent_organism: models.ForeignKey[Organism | None] = models.ForeignKey( "Organism", related_name="proteins", verbose_name="Parental organism", @@ -236,7 +238,7 @@ class CofactorChoices(models.TextChoices): ) primary_reference_id: int | None - primary_reference = models.ForeignKey["Reference | None"]( + primary_reference: models.ForeignKey[Reference | None] = models.ForeignKey( Reference, related_name="primary_proteins", verbose_name="Primary Reference", @@ -246,12 +248,12 @@ class CofactorChoices(models.TextChoices): help_text="Preferably the publication that introduced the protein", ) if TYPE_CHECKING: - references = models.ManyToManyField["Reference", "Protein"]() + references: models.ManyToManyField[Reference, Protein] = models.ManyToManyField() else: references = models.ManyToManyField(Reference, related_name="proteins", blank=True) default_state_id: int | None - default_state = models.ForeignKey["State | None"]( + default_state: models.ForeignKey[State | None] = models.ForeignKey( "State", related_name="default_for", blank=True, @@ -262,12 +264,12 @@ class CofactorChoices(models.TextChoices): if TYPE_CHECKING: states = RelatedManager["State"]() lineage = RelatedManager["Lineage"]() - default_for: models.QuerySet["State"] + default_for: models.QuerySet[State] transitions: models.QuerySet[StateTransition] oser_measurements: models.QuerySet[OSERMeasurement] collection_memberships: models.QuerySet[ProteinCollection] excerpts: models.QuerySet[Excerpt] - snapgene_plasmids = models.ManyToManyField["SnapGenePlasmid", "Protein"]() + snapgene_plasmids: models.ManyToManyField[SnapGenePlasmid, Protein] = models.ManyToManyField() else: snapgene_plasmids = models.ManyToManyField( "SnapGenePlasmid", @@ -277,7 +279,7 @@ class CofactorChoices(models.TextChoices): ) # managers - objects: "_ProteinManager[Self]" = _ProteinManager() + objects: _ProteinManager[Self] = _ProteinManager() visible = QueryManager(~Q(status="hidden")) def mutations_from_root(self): @@ -559,7 +561,7 @@ def first_author(self): class State(Fluorophore): # TODO: rename to ProteinState protein_id: int - protein = models.ForeignKey["Protein"]( + protein: models.ForeignKey[Protein] = models.ForeignKey( Protein, related_name="states", help_text="The protein to which this state belongs", @@ -573,8 +575,8 @@ class State(Fluorophore): # TODO: rename to ProteinState ) if TYPE_CHECKING: - transitions = models.ManyToManyField["State", "State"]() - transition_state: models.QuerySet["State"] + transitions: models.ManyToManyField[State, State] = models.ManyToManyField() + transition_state: models.QuerySet[State] transitions_from: models.QuerySet[StateTransition] transitions_to: models.QuerySet[StateTransition] bleach_measurements: models.QuerySet[BleachMeasurement] diff --git a/backend/proteins/models/spectrum.py b/backend/proteins/models/spectrum.py index 3cd971bc2..5a144ca61 100644 --- a/backend/proteins/models/spectrum.py +++ b/backend/proteins/models/spectrum.py @@ -370,7 +370,7 @@ class Spectrum(Authorable, StatusModel, TimeStampedModel, AdminURLMixin): # https://lukeplant.me.uk/blog/posts/avoid-django-genericforeignkey/ # Fluorophore encompasses both State (ProteinState) and DyeState via MTI owner_fluor_id: int | None - owner_fluor = models.ForeignKey["Fluorophore | None"]( + owner_fluor: models.ForeignKey[Fluorophore | None] = models.ForeignKey( "Fluorophore", null=True, blank=True, @@ -378,7 +378,7 @@ class Spectrum(Authorable, StatusModel, TimeStampedModel, AdminURLMixin): related_name="spectra", ) owner_filter_id: int | None - owner_filter = models.OneToOneField["Filter | None"]( + owner_filter: models.OneToOneField[Filter | None] = models.OneToOneField( "Filter", null=True, blank=True, @@ -386,7 +386,7 @@ class Spectrum(Authorable, StatusModel, TimeStampedModel, AdminURLMixin): related_name="spectrum", ) owner_light_id: int | None - owner_light = models.OneToOneField["Light | None"]( + owner_light: models.OneToOneField[Light | None] = models.OneToOneField( "Light", null=True, blank=True, @@ -394,7 +394,7 @@ class Spectrum(Authorable, StatusModel, TimeStampedModel, AdminURLMixin): related_name="spectrum", ) owner_camera_id: int | None - owner_camera = models.OneToOneField["Camera | None"]( + owner_camera: models.OneToOneField[Camera | None] = models.OneToOneField( "Camera", null=True, blank=True, @@ -402,7 +402,7 @@ class Spectrum(Authorable, StatusModel, TimeStampedModel, AdminURLMixin): related_name="spectrum", ) reference_id: int | None - reference = models.ForeignKey["Reference | None"]( + reference: models.ForeignKey[Reference | None] = models.ForeignKey( Reference, null=True, blank=True, diff --git a/backend/proteins/models/transition.py b/backend/proteins/models/transition.py index 7c8028abd..cef8654ea 100644 --- a/backend/proteins/models/transition.py +++ b/backend/proteins/models/transition.py @@ -8,7 +8,7 @@ from proteins.models.mixins import Authorable if TYPE_CHECKING: - from proteins.models import Protein, State # noqa F401 + from proteins.models import Protein, State class StateTransition(Authorable, TimeStampedModel): @@ -20,7 +20,7 @@ class StateTransition(Authorable, TimeStampedModel): validators=[MinValueValidator(300), MaxValueValidator(1000)], ) protein_id: int - protein = models.ForeignKey["Protein"]( + protein: models.ForeignKey["Protein"] = models.ForeignKey( "Protein", related_name="transitions", verbose_name="Protein Transitioning", @@ -28,7 +28,7 @@ class StateTransition(Authorable, TimeStampedModel): on_delete=models.CASCADE, ) from_state_id: int - from_state = models.ForeignKey["State"]( + from_state: models.ForeignKey["State"] = models.ForeignKey( "State", related_name="transitions_from", verbose_name="From state", @@ -36,7 +36,7 @@ class StateTransition(Authorable, TimeStampedModel): on_delete=models.CASCADE, ) to_state_id: int - to_state = models.ForeignKey["State"]( + to_state: models.ForeignKey["State"] = models.ForeignKey( "State", related_name="transitions_to", verbose_name="To state", diff --git a/backend/references/models.py b/backend/references/models.py index 55533fdf8..7e38d76af 100644 --- a/backend/references/models.py +++ b/backend/references/models.py @@ -42,7 +42,7 @@ class Author(TimeStampedModel): given = models.CharField(max_length=80) initials = models.CharField(max_length=10) if TYPE_CHECKING: - publications = models.ManyToManyField["Reference", "Author"]() + publications: models.ManyToManyField[Reference, Author] = models.ManyToManyField() referenceauthor_set: models.QuerySet[ReferenceAuthor] else: publications = models.ManyToManyField("Reference", through="ReferenceAuthor") @@ -109,7 +109,7 @@ class Reference(TimeStampedModel): help_text="YYYY", ) if TYPE_CHECKING: - authors = models.ManyToManyField["Author", "Reference"]() + authors: models.ManyToManyField[Author, Reference] = models.ManyToManyField() referenceauthor_set: models.QuerySet[ReferenceAuthor] primary_proteins: models.QuerySet[Protein] proteins: models.QuerySet[Protein] @@ -124,7 +124,7 @@ class Reference(TimeStampedModel): summary = models.CharField(max_length=512, blank=True, help_text="Brief summary of findings") created_by_id: int | None - created_by = models.ForeignKey[User | None]( + created_by: models.ForeignKey[User | None] = models.ForeignKey( User, related_name="reference_author", blank=True, @@ -132,7 +132,7 @@ class Reference(TimeStampedModel): on_delete=models.CASCADE, ) updated_by_id: int | None - updated_by = models.ForeignKey[User | None]( + updated_by: models.ForeignKey[User | None] = models.ForeignKey( User, related_name="reference_modifier", blank=True, @@ -232,9 +232,9 @@ def url(self): class ReferenceAuthor(models.Model): reference_id: int - reference = models.ForeignKey[Reference](Reference, on_delete=models.CASCADE) + reference: models.ForeignKey[Reference] = models.ForeignKey(Reference, on_delete=models.CASCADE) author_id: int - author = models.ForeignKey[Author](Author, on_delete=models.CASCADE) + author: models.ForeignKey[Author] = models.ForeignKey(Author, on_delete=models.CASCADE) author_idx = models.PositiveSmallIntegerField() class Meta: From 6bd7a88cbfe4763bd81c04af199098016296a6ad Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 25 Nov 2025 12:53:03 -0500 Subject: [PATCH 41/57] remove comments about measurable fields in migration functions --- .../migrations/0059_add_fluorophore_and_new_models.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py index 8c5170e36..0d6905b1d 100644 --- a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py +++ b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py @@ -78,8 +78,6 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N protein = Protein.objects.get(id=row["protein_id"]) # Create State (MTI child of Fluorophore) - # Note: Measurable fields (ex_max, em_max, qy, etc.) are left empty here - # and will be populated by rebuild_attributes() after creating FluorescenceMeasurement state = State.objects.create( id=old_id, # Preserve old ID for easier FK updates later created=row["created"], @@ -169,8 +167,6 @@ def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> dic ) # Create DyeState (MTI child of Fluorophore) - # Note: Measurable fields (ex_max, em_max, qy, etc.) are left empty here - # and will be populated by rebuild_attributes() after creating FluorescenceMeasurement dyestate = DyeState.objects.create( created=row["created"], modified=row["modified"], From df635b898e012916752a07f162cf0cb042759b93 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 25 Nov 2025 15:40:13 -0500 Subject: [PATCH 42/57] refactor rebuild_attributes --- backend/proteins/models/dye.py | 12 + backend/proteins/models/fluorophore.py | 112 +++-- backend/proteins/models/protein.py | 3 +- backend/proteins/util/_scipy.py | 6 +- .../tests/test_proteins/test_fluorophore.py | 423 ++++++++++++++++++ pyproject.toml | 1 + 6 files changed, 526 insertions(+), 31 deletions(-) create mode 100644 backend/tests/test_proteins/test_fluorophore.py diff --git a/backend/proteins/models/dye.py b/backend/proteins/models/dye.py index fd119e7f3..0ba481011 100644 --- a/backend/proteins/models/dye.py +++ b/backend/proteins/models/dye.py @@ -9,6 +9,7 @@ if TYPE_CHECKING: from proteins.models import DyeState + from references.models import Reference class Dye(Authorable, TimeStampedModel, Product): # TODO: rename to SmallMolecule @@ -30,6 +31,17 @@ class Dye(Authorable, TimeStampedModel, Product): # TODO: rename to SmallMolecu on_delete=models.SET_NULL, ) + primary_reference_id: int | None + primary_reference: models.ForeignKey["Reference | None"] = models.ForeignKey( + "references.Reference", + related_name="primary_dyes", + verbose_name="Primary Reference", + blank=True, + null=True, + on_delete=models.SET_NULL, + help_text="The publication that introduced the dye", + ) + if TYPE_CHECKING: states = models.manager.RelatedManager["DyeState"]() diff --git a/backend/proteins/models/fluorophore.py b/backend/proteins/models/fluorophore.py index 859daf68a..c2a028abc 100644 --- a/backend/proteins/models/fluorophore.py +++ b/backend/proteins/models/fluorophore.py @@ -13,7 +13,7 @@ from django.db.models import QuerySet from django.db.models.manager import RelatedManager - from proteins.models import Dye, DyeState, FluorescenceMeasurement, OcFluorEff, Protein, State # noqa: F401 + from proteins.models import Dye, DyeState, FluorescenceMeasurement, OcFluorEff, Protein, State from proteins.models.spectrum import D3Dict, Spectrum @@ -57,11 +57,13 @@ class EntityTypes(models.TextChoices): max_length=255, db_index=True, blank=True, + default="", help_text="Protein/Dye name (cached for searching)", ) owner_slug = models.SlugField( max_length=200, blank=True, + default="", help_text="Protein/Dye slug (cached for URLs)", ) @@ -72,14 +74,17 @@ class EntityTypes(models.TextChoices): # Lineage Tracking # Maps field names to Measurement IDs. e.g., {'ex_max': 102, 'qy': 105} source_map = models.JSONField(default=dict, blank=True) + # Admin override: Per-field pinned measurement IDs that won't be auto-updated. + # e.g., {'qy': 105} means qy always comes from measurement 105, ignoring priority rules. + pinned_source_map = models.JSONField(default=dict, blank=True) # Managers objects: FluorophoreManager[Self] = FluorophoreManager() if TYPE_CHECKING: - spectra = RelatedManager["Spectrum"]() - measurements = RelatedManager["FluorescenceMeasurement"]() - oc_effs = RelatedManager["OcFluorEff"]() + spectra: RelatedManager[Spectrum] + measurements: RelatedManager[FluorescenceMeasurement] + oc_effs: RelatedManager[OcFluorEff] # these are not *guaranteed* to exist, they come from Django MTI dyestate: DyeState @@ -118,42 +123,89 @@ def as_subclass(self) -> Self: return getattr(self, subclass_name) return self # Fallback to parent if no child found - def rebuild_attributes(self): + def rebuild_attributes(self) -> None: """The Compositing Engine. Aggregates all measurements to determine the current canonical values. + + Priority order (highest to lowest): + 1. Pinned overrides - Admin has explicitly pinned a measurement for a field + 2. Trusted measurements - is_trusted=True on FluorescenceMeasurement + 3. Primary reference - Measurement from the owner's primary_reference + 4. Most recent - Fallback by date_measured (most recent first) """ - # 1. Fetch all measurements, sorted by priority: - # Curator Trusted > Primary Reference > Most Recent Date - measurements = self.measurements.select_related("reference").order_by( - "-is_trusted", - # Assuming you have a helper to check if ref is primary for the owner - # This part requires custom logic depending on if self is Protein or Dye - # '-is_primary_ref', - "-date_measured", - ) + from proteins.models import FluorescenceMeasurement measurable_fields = AbstractFluorescenceData.get_measurable_fields() - new_values = {} - new_source_map = {} + new_values: dict[str, object] = {} + new_source_map: dict[str, int] = {} + + # Get primary reference ID for priority sorting + primary_ref_id = self._get_primary_reference_id() + + # 1. Handle pinned fields first (admin overrides) + pinned_fields: set[str] = set() + for field, measurement_id in self.pinned_source_map.items(): + if field not in measurable_fields: + continue + try: + measurement = FluorescenceMeasurement.objects.get(id=measurement_id, fluorophore=self) + val = getattr(measurement, field) + if val is not None: + new_values[field] = val + new_source_map[field] = measurement.id + pinned_fields.add(field) + except FluorescenceMeasurement.DoesNotExist: + # Pinned measurement no longer exists, skip it + pass + + # 2. Fetch all measurements for non-pinned fields + measurements = list(self.measurements.select_related("reference").all()) + + # Sort by priority: trusted > primary_reference > most recent date + def measurement_priority(m: FluorescenceMeasurement) -> tuple[int, int, str]: + # Lower tuple = higher priority (sorted ascending) + trusted_score = 0 if m.is_trusted else 1 + primary_score = 0 if primary_ref_id and m.reference_id == primary_ref_id else 1 + # Use date_measured for sorting, fallback to empty string (sorts last) + date_str = m.date_measured.isoformat() if m.date_measured else "" + # Negate by reversing string comparison for descending date order + return (trusted_score, primary_score, date_str) + + # Sort: trusted first, then primary ref, then most recent date + measurements.sort(key=measurement_priority) + # Reverse date sorting within groups (we want most recent first) + # Re-sort with proper date handling + measurements.sort( + key=lambda m: ( + 0 if m.is_trusted else 1, + 0 if primary_ref_id and m.reference_id == primary_ref_id else 1, + # Invert date for descending order (most recent first) + -(m.date_measured.toordinal() if m.date_measured else 0), + ) + ) - # 2. Waterfall Logic: Find the first non-null value for each field + # 3. Waterfall Logic: Find the first non-null value for each non-pinned field for field in measurable_fields: - found_val = None - found_source_id = None + if field in pinned_fields: + continue # Already handled above for m in measurements: val = getattr(m, field) if val is not None: - found_val = val - found_source_id = m.id + new_values[field] = val + new_source_map[field] = m.id break - - new_values[field] = found_val - if found_source_id: - new_source_map[field] = found_source_id - - # 3. Update Cache + else: + # No measurement had a value for this field + # Use field default for non-nullable fields (e.g., is_dark) + field_obj = self._meta.get_field(field) + if field_obj.has_default(): + new_values[field] = field_obj.get_default() + else: + new_values[field] = None + + # 4. Update cached values for key, val in new_values.items(): setattr(self, key, val) @@ -259,3 +311,9 @@ def _owner(self) -> Dye | Protein | None: if hasattr(self, "state"): return self.state.protein return None + + def _get_primary_reference_id(self) -> int | None: + """Get the primary reference ID for the owner entity (Protein or Dye).""" + if owner := self._owner(): + return owner.primary_reference_id + return None diff --git a/backend/proteins/models/protein.py b/backend/proteins/models/protein.py index adda5a504..ea68be67b 100644 --- a/backend/proteins/models/protein.py +++ b/backend/proteins/models/protein.py @@ -6,7 +6,6 @@ import os import sys from collections import Counter -from collections.abc import Sequence from random import choices from subprocess import PIPE, run from typing import TYPE_CHECKING, cast @@ -36,6 +35,7 @@ from references.models import Reference if TYPE_CHECKING: + from collections.abc import Sequence from typing import Self from django.db.models.manager import RelatedManager @@ -47,7 +47,6 @@ Lineage, Organism, OSERMeasurement, - ProteinCollection, SnapGenePlasmid, StateTransition, ) diff --git a/backend/proteins/util/_scipy.py b/backend/proteins/util/_scipy.py index db119153c..0405e89f0 100644 --- a/backend/proteins/util/_scipy.py +++ b/backend/proteins/util/_scipy.py @@ -12,11 +12,13 @@ from __future__ import annotations -from collections.abc import Callable -from typing import Any +from typing import TYPE_CHECKING, Any import numpy as np +if TYPE_CHECKING: + from collections.abc import Callable + # ============================================================================ # scipy.signal replacements # ============================================================================ diff --git a/backend/tests/test_proteins/test_fluorophore.py b/backend/tests/test_proteins/test_fluorophore.py new file mode 100644 index 000000000..e79d1ef43 --- /dev/null +++ b/backend/tests/test_proteins/test_fluorophore.py @@ -0,0 +1,423 @@ +"""Tests for Fluorophore.rebuild_attributes() compositing logic.""" + +from datetime import date + +import pytest + +from proteins.models import Dye, DyeState, Protein, State +from proteins.models import FluorescenceMeasurement as FM +from references.models import Reference + + +@pytest.fixture +def protein(db) -> Protein: + """Create a simple protein.""" + return Protein.objects.create(name="TestProtein") + + +@pytest.fixture +def protein_with_primary_ref(db) -> Protein: + """Create a protein with a primary reference.""" + ref = Reference(doi="10.1234/primary", year=2020) + ref.save(skipdoi=True) + return Protein.objects.create(name="TestProteinWithRef", primary_reference=ref) + + +@pytest.fixture +def dye(db) -> Dye: + """Create a simple dye.""" + return Dye.objects.create(name="TestDye") + + +@pytest.fixture +def dye_with_primary_ref(db) -> Dye: + """Create a dye with a primary reference.""" + ref = Reference(doi="10.1234/dye-primary", year=2020) + ref.save(skipdoi=True) + return Dye.objects.create(name="TestDyeWithRef", primary_reference=ref) + + +@pytest.fixture +def state(protein: Protein) -> State: + """Create a state for the protein.""" + return State.objects.create(protein=protein, name="default") + + +@pytest.fixture +def state_with_ref(protein_with_primary_ref: Protein) -> State: + """Create a state for a protein with primary reference.""" + return State.objects.create(protein=protein_with_primary_ref, name="default") + + +@pytest.fixture +def dyestate(dye: Dye) -> DyeState: + """Create a dyestate for the dye.""" + return DyeState.objects.create(dye=dye, name="default") + + +@pytest.fixture +def dyestate_with_ref(dye_with_primary_ref: Dye) -> DyeState: + """Create a dyestate for a dye with primary reference.""" + return DyeState.objects.create(dye=dye_with_primary_ref, name="default") + + +class TestRebuildAttributesBasicWaterfall: + """Test basic waterfall logic - first non-null value wins.""" + + def test_single_measurement_sets_values(self, state: State): + """A single measurement should populate the fluorophore.""" + m = FM(fluorophore=state, ex_max=488, em_max=509, qy=0.67) + m.save(rebuild_cache=False) + state.rebuild_attributes() + state.refresh_from_db() + + assert state.ex_max == 488 + assert state.em_max == 509 + assert state.qy == 0.67 + + def test_multiple_measurements_first_value_wins(self, state: State): + """When multiple measurements exist, first non-null value wins.""" + # Measurement with partial data (no qy) + m1 = FM(fluorophore=state, ex_max=488, em_max=509, qy=None, date_measured=date(2020, 1, 1)) + m1.save(rebuild_cache=False) + # Measurement with different values + m2 = FM(fluorophore=state, ex_max=500, em_max=520, qy=0.5, date_measured=date(2019, 1, 1)) + m2.save(rebuild_cache=False) + + state.rebuild_attributes() + state.refresh_from_db() + + # More recent measurement wins for ex_max and em_max + assert state.ex_max == 488 + assert state.em_max == 509 + # qy comes from second measurement (first had null) + assert state.qy == 0.5 + + def test_source_map_tracks_measurement_ids(self, state: State): + """source_map should track which measurement each field came from.""" + m1 = FM(fluorophore=state, ex_max=488, qy=None, date_measured=date(2020, 1, 1)) + m1.save(rebuild_cache=False) + m2 = FM(fluorophore=state, ex_max=None, qy=0.5, date_measured=date(2019, 1, 1)) + m2.save(rebuild_cache=False) + + state.rebuild_attributes() + state.refresh_from_db() + + assert state.source_map["ex_max"] == m1.id + assert state.source_map["qy"] == m2.id + + +class TestRebuildAttributesTrustedPriority: + """Test that is_trusted=True measurements take highest priority.""" + + def test_trusted_measurement_overrides_recent(self, state: State): + """A trusted measurement should override a more recent one.""" + # Recent but not trusted + m1 = FM(fluorophore=state, ex_max=500, em_max=520, is_trusted=False, date_measured=date(2023, 1, 1)) + m1.save(rebuild_cache=False) + # Older but trusted + m2 = FM(fluorophore=state, ex_max=488, em_max=509, is_trusted=True, date_measured=date(2010, 1, 1)) + m2.save(rebuild_cache=False) + + state.rebuild_attributes() + state.refresh_from_db() + + # Trusted measurement wins + assert state.ex_max == 488 + assert state.em_max == 509 + + +class TestRebuildAttributesPrimaryReferencePriority: + """Test that primary reference measurements take priority over non-primary.""" + + def test_primary_ref_measurement_overrides_recent(self, state_with_ref: State): + """Measurement from primary reference should override more recent non-primary.""" + primary_ref = state_with_ref.protein.primary_reference + other_ref = Reference(doi="10.1234/other", year=2022) + other_ref.save(skipdoi=True) + + # Recent but not from primary reference + m1 = FM( + fluorophore=state_with_ref, + reference=other_ref, + ex_max=500, + em_max=520, + date_measured=date(2023, 1, 1), + ) + m1.save(rebuild_cache=False) + # Older but from primary reference + m2 = FM( + fluorophore=state_with_ref, + reference=primary_ref, + ex_max=488, + em_max=509, + date_measured=date(2010, 1, 1), + ) + m2.save(rebuild_cache=False) + + state_with_ref.rebuild_attributes() + state_with_ref.refresh_from_db() + + # Primary reference measurement wins + assert state_with_ref.ex_max == 488 + assert state_with_ref.em_max == 509 + + def test_primary_ref_on_dye(self, dyestate_with_ref: DyeState): + """Test that primary reference works for dyes too.""" + primary_ref = dyestate_with_ref.dye.primary_reference + other_ref = Reference(doi="10.1234/dye-other", year=2022) + other_ref.save(skipdoi=True) + + # Not from primary reference + m1 = FM( + fluorophore=dyestate_with_ref, + reference=other_ref, + ex_max=600, + em_max=650, + date_measured=date(2023, 1, 1), + ) + m1.save(rebuild_cache=False) + # From primary reference + m2 = FM( + fluorophore=dyestate_with_ref, + reference=primary_ref, + ex_max=550, + em_max=580, + date_measured=date(2010, 1, 1), + ) + m2.save(rebuild_cache=False) + + dyestate_with_ref.rebuild_attributes() + dyestate_with_ref.refresh_from_db() + + # Primary reference measurement wins + assert dyestate_with_ref.ex_max == 550 + assert dyestate_with_ref.em_max == 580 + + +class TestRebuildAttributesPriorityOrder: + """Test the complete priority order: trusted > primary_ref > date.""" + + def test_trusted_beats_primary_ref(self, state_with_ref: State): + """is_trusted should override even primary reference.""" + primary_ref = state_with_ref.protein.primary_reference + + # From primary reference but not trusted + m1 = FM( + fluorophore=state_with_ref, + reference=primary_ref, + ex_max=488, + em_max=509, + is_trusted=False, + date_measured=date(2020, 1, 1), + ) + m1.save(rebuild_cache=False) + # Trusted but not from primary reference + m2 = FM( + fluorophore=state_with_ref, + ex_max=500, + em_max=520, + is_trusted=True, + date_measured=date(2010, 1, 1), + ) + m2.save(rebuild_cache=False) + + state_with_ref.rebuild_attributes() + state_with_ref.refresh_from_db() + + # Trusted measurement wins over primary reference + assert state_with_ref.ex_max == 500 + assert state_with_ref.em_max == 520 + + def test_date_priority_when_equal_trust_and_ref(self, state: State): + """When trust and reference status are equal, more recent date wins.""" + m1 = FM(fluorophore=state, ex_max=488, em_max=509, date_measured=date(2020, 6, 1)) + m1.save(rebuild_cache=False) + m2 = FM(fluorophore=state, ex_max=500, em_max=520, date_measured=date(2023, 6, 1)) + m2.save(rebuild_cache=False) + + state.rebuild_attributes() + state.refresh_from_db() + + # More recent date wins + assert state.ex_max == 500 + assert state.em_max == 520 + + +class TestRebuildAttributesPinnedFields: + """Test that pinned_source_map provides per-field override.""" + + def test_pinned_field_overrides_all_priorities(self, state_with_ref: State): + """A pinned field should override even trusted measurements.""" + primary_ref = state_with_ref.protein.primary_reference + + # Trusted measurement from primary reference - highest normal priority + m_trusted = FM( + fluorophore=state_with_ref, + reference=primary_ref, + ex_max=488, + em_max=509, + qy=0.67, + is_trusted=True, + date_measured=date(2020, 1, 1), + ) + m_trusted.save(rebuild_cache=False) + # Older, not trusted, not primary - lowest normal priority + m_pinned = FM( + fluorophore=state_with_ref, + ex_max=500, + em_max=520, + qy=0.50, + is_trusted=False, + date_measured=date(2010, 1, 1), + ) + m_pinned.save(rebuild_cache=False) + + # Pin qy to the low-priority measurement + state_with_ref.pinned_source_map = {"qy": m_pinned.id} + state_with_ref.save() + + state_with_ref.rebuild_attributes() + state_with_ref.refresh_from_db() + + # ex_max and em_max should come from trusted measurement + assert state_with_ref.ex_max == 488 + assert state_with_ref.em_max == 509 + # qy should come from the pinned measurement + assert state_with_ref.qy == 0.50 + # source_map should reflect the pinned override + assert state_with_ref.source_map["qy"] == m_pinned.id + assert state_with_ref.source_map["ex_max"] == m_trusted.id + + def test_pinned_field_with_null_value_is_skipped(self, state: State): + """If a pinned measurement has null for the pinned field, skip it.""" + m1 = FM(fluorophore=state, ex_max=488, qy=0.67, date_measured=date(2020, 1, 1)) + m1.save(rebuild_cache=False) + m2 = FM(fluorophore=state, ex_max=500, qy=None, date_measured=date(2010, 1, 1)) + m2.save(rebuild_cache=False) + + # Pin qy to m2, but m2 has null qy + state.pinned_source_map = {"qy": m2.id} + state.save() + + state.rebuild_attributes() + state.refresh_from_db() + + # qy should fall back to waterfall logic since pinned was null + assert state.qy == 0.67 + assert state.source_map["qy"] == m1.id + + def test_pinned_nonexistent_measurement_is_ignored(self, state: State): + """If pinned measurement doesn't exist, ignore and use waterfall.""" + m = FM(fluorophore=state, qy=0.67, date_measured=date(2020, 1, 1)) + m.save(rebuild_cache=False) + + # Pin to a non-existent measurement ID + state.pinned_source_map = {"qy": 999999} + state.save() + + state.rebuild_attributes() + state.refresh_from_db() + + # Should fall back to waterfall logic + assert state.qy == 0.67 + + def test_pinned_measurement_from_other_fluorophore_is_ignored(self, protein: Protein): + """A pinned measurement that belongs to a different fluorophore is ignored.""" + state1 = State.objects.create(protein=protein, name="state1") + state2 = State.objects.create(protein=protein, name="state2") + + m1 = FM(fluorophore=state1, qy=0.67) + m1.save(rebuild_cache=False) + m_other = FM(fluorophore=state2, qy=0.50) + m_other.save(rebuild_cache=False) + + # Try to pin state1's qy to state2's measurement + state1.pinned_source_map = {"qy": m_other.id} + state1.save() + + state1.rebuild_attributes() + state1.refresh_from_db() + + # Should use state1's own measurement + assert state1.qy == 0.67 + + +class TestRebuildAttributesNoMeasurements: + """Test behavior when there are no measurements.""" + + def test_no_measurements_sets_null_values(self, state: State): + """With no measurements, all values should be null.""" + state.rebuild_attributes() + state.refresh_from_db() + + assert state.ex_max is None + assert state.em_max is None + assert state.qy is None + assert state.source_map == {} + + +class TestRebuildAttributesAutoTrigger: + """Test that save/delete on measurements auto-triggers rebuild.""" + + def test_measurement_save_triggers_rebuild(self, state: State): + """Creating a measurement should trigger rebuild_attributes.""" + # Create measurement - should auto-trigger rebuild (rebuild_cache=True by default) + m = FM(fluorophore=state, ex_max=488, em_max=509) + m.save() + state.refresh_from_db() + + assert state.ex_max == 488 + assert state.em_max == 509 + + def test_measurement_delete_triggers_rebuild(self, state: State): + """Deleting a measurement should trigger rebuild_attributes.""" + m1 = FM(fluorophore=state, ex_max=488, em_max=509, date_measured=date(2023, 1, 1)) + m1.save() + state.refresh_from_db() + assert state.ex_max == 488 + + m2 = FM(fluorophore=state, ex_max=500, em_max=520, date_measured=date(2020, 1, 1)) + m2.save() + state.refresh_from_db() + # Still 488 because more recent + assert state.ex_max == 488 + + # Delete the more recent measurement + m1.delete() + state.refresh_from_db() + + # Should now use the older measurement + assert state.ex_max == 500 + assert state.em_max == 520 + + def test_rebuild_cache_false_skips_rebuild(self, state: State): + """rebuild_cache=False should skip the rebuild.""" + m = FM(fluorophore=state, ex_max=488) + m.save(rebuild_cache=False) + state.refresh_from_db() + + # Should not have been updated + assert state.ex_max is None + + +class TestGetPrimaryReferenceId: + """Test _get_primary_reference_id helper method.""" + + def test_returns_none_for_no_primary_ref(self, state: State): + """Should return None when protein has no primary reference.""" + assert state._get_primary_reference_id() is None + + def test_returns_primary_ref_id_for_protein(self, state_with_ref: State): + """Should return primary_reference_id for protein state.""" + expected = state_with_ref.protein.primary_reference_id + assert state_with_ref._get_primary_reference_id() == expected + + def test_returns_none_for_dye_without_ref(self, dyestate: DyeState): + """Should return None when dye has no primary reference.""" + assert dyestate._get_primary_reference_id() is None + + def test_returns_primary_ref_id_for_dye(self, dyestate_with_ref: DyeState): + """Should return primary_reference_id for dye state.""" + expected = dyestate_with_ref.dye.primary_reference_id + assert dyestate_with_ref._get_primary_reference_id() == expected diff --git a/pyproject.toml b/pyproject.toml index 3048b0950..149aa200d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -131,6 +131,7 @@ select = [ "RUF", # Ruff-specific rules "LOG", # flake8-logging "G", # flake8-logging-format + "TC", # flake8-type-checking ] ignore = [ "B905", # `zip()` without an explicit `strict=` parameter From 8006dc6d813732ea6578b5264b5524b0961df2dd Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 25 Nov 2025 16:12:20 -0500 Subject: [PATCH 43/57] update migrations --- .../0059_add_fluorophore_and_new_models.py | 94 +++++++++++++------ backend/proteins/models/protein.py | 2 +- 2 files changed, 64 insertions(+), 32 deletions(-) diff --git a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py index 0d6905b1d..e2c39980f 100644 --- a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py +++ b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py @@ -9,23 +9,28 @@ 5. proteins.OcFluorEff updated to point to new Fluorophore records """ + from __future__ import annotations import json import logging -from typing import Any +from typing import TYPE_CHECKING, Any import django.db.models.deletion import django.utils.timezone import model_utils.fields -from django.apps.registry import Apps +from django.conf import settings from django.core.validators import MaxValueValidator, MinValueValidator from django.db import migrations, models -from django.db.backends.base.schema import BaseDatabaseSchemaEditor -from django.db.backends.utils import CursorWrapper + +if TYPE_CHECKING: + from django.apps.registry import Apps + from django.db.backends.base.schema import BaseDatabaseSchemaEditor + from django.db.backends.utils import CursorWrapper logger = logging.getLogger(__name__) + def _dictfetchall(cursor: CursorWrapper) -> list[dict[str, Any]]: """Return all rows from a cursor as a dict. Assume the column names are unique.""" if not cursor.description: @@ -356,8 +361,7 @@ def populate_emhex_exhex(apps, _schema_editor): """ def wave_to_hex(wavelength, gamma=1): - """This converts a given wavelength into an approximate RGB value. - """ + """This converts a given wavelength into an approximate RGB value.""" if not wavelength: return "#000" @@ -404,7 +408,6 @@ def wave_to_hex(wavelength, gamma=1): b *= 255 return f"#{int(r):02x}{int(g):02x}{int(b):02x}" - Fluorophore = apps.get_model("proteins", "Fluorophore") fluorophores_to_update = [] @@ -582,9 +585,7 @@ def abstract_fluorescence_data_fields(): return [ ( "is_dark", - models.BooleanField( - default=False, verbose_name="Dark State", help_text="This state does not fluoresce" - ), + models.BooleanField(default=False, verbose_name="Dark State", help_text="This state does not fluoresce"), ), ( "ex_max", @@ -676,8 +677,26 @@ def abstract_fluorescence_data_fields(): def authorable_mixin_fields(): """Return fresh field instances for Authorable mixin.""" return [ - ("created_by_id", models.IntegerField(null=True, blank=True)), - ("updated_by_id", models.IntegerField(null=True, blank=True)), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_author", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "updated_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_modifier", + to=settings.AUTH_USER_MODEL, + ), + ), ] @@ -759,21 +778,22 @@ class Migration(migrations.Migration): ( "owner_name", models.CharField( - max_length=255, - db_index=True, blank=True, - null=True, + db_index=True, + default="", help_text="Protein/Dye name (cached for searching)", + max_length=255, ), ), ( "owner_slug", models.SlugField( - max_length=200, blank=True, null=True, help_text="Protein/Dye slug (cached for URLs)" + max_length=200, blank=True, default="", help_text="Protein/Dye slug (cached for URLs)" ), ), *abstract_fluorescence_data_fields(), ("source_map", models.JSONField(default=dict, blank=True)), + ("pinned_source_map", models.JSONField(default=dict, blank=True)), *authorable_mixin_fields(), ], options={ @@ -799,7 +819,9 @@ class Migration(migrations.Migration): ( "fluorophore", models.ForeignKey( - "Fluorophore", related_name="measurements", on_delete=django.db.models.deletion.CASCADE + on_delete=django.db.models.deletion.CASCADE, + related_name="measurements", + to="proteins.fluorophore", ), ), ( @@ -820,10 +842,19 @@ class Migration(migrations.Migration): fields=[ ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ("name", models.CharField(max_length=255, db_index=True)), + ("slug", models.SlugField(unique=True)), ( - "slug", - models.SlugField(max_length=100, unique=True), - ), # Increased from default 50 to accommodate long names + "primary_reference", + models.ForeignKey( + blank=True, + help_text="The publication that introduced the dye", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="primary_dyes", + to="references.reference", + verbose_name="Primary Reference", + ), + ), *product_mixin_fields(), *authorable_mixin_fields(), *timestamped_mixin_fields(), @@ -843,7 +874,12 @@ class Migration(migrations.Migration): to="proteins.fluorophore", ), ), - ("dye", models.ForeignKey("Dye", on_delete=django.db.models.deletion.CASCADE, related_name="states")), + ( + "dye", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, related_name="states", to="proteins.dye" + ), + ), ], options={ "abstract": False, @@ -879,10 +915,10 @@ class Migration(migrations.Migration): ( "protein", models.ForeignKey( - "Protein", - related_name="states", help_text="The protein to which this state belongs", on_delete=django.db.models.deletion.CASCADE, + related_name="states", + to="proteins.protein", ), ), ( @@ -953,14 +989,9 @@ class Migration(migrations.Migration): reverse_sql=migrations.RunSQL.noop, ), # Step 2: Remove old foreign keys from Spectrum - migrations.RemoveField( - model_name="spectrum", - name="owner_state", - ), - migrations.RemoveField( - model_name="spectrum", - name="owner_dye", - ), + migrations.RemoveField(model_name="spectrum", name="owner_state"), + migrations.RemoveField(model_name="spectrum", name="owner_dye"), + migrations.RemoveIndex(model_name="spectrum", name="spectrum_state_status_idx"), # Step 3: Make owner_fluor non-nullable now that all data is migrated # First, verify no nulls exist (will fail if there are any) migrations.RunSQL( @@ -999,4 +1030,5 @@ class Migration(migrations.Migration): to="proteins.fluorophore", ), ), + migrations.AlterUniqueTogether(name="ocfluoreff", unique_together={("oc", "fluor")}), ] diff --git a/backend/proteins/models/protein.py b/backend/proteins/models/protein.py index ea68be67b..e1e5a5cb3 100644 --- a/backend/proteins/models/protein.py +++ b/backend/proteins/models/protein.py @@ -131,8 +131,8 @@ class SwitchingChoices(models.TextChoices): PHOTOSWITCHABLE = ("ps", "Photoswitchable") PHOTOCONVERTIBLE = ("pc", "Photoconvertible") MULTIPHOTOCHROMIC = ("mp", "Multi-photochromic") - TIMER = ("t", "Timer") OTHER = ("o", "Multistate") + TIMER = ("t", "Timer") class CofactorChoices(models.TextChoices): BILIRUBIN = ("br", "Bilirubin") From d83fea686b4037a9d3e61f2d3aca6a35e8a4ffde Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 25 Nov 2025 16:15:00 -0500 Subject: [PATCH 44/57] add check to ci --- .github/workflows/ci.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a584a364..3885cc51c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 + test: runs-on: ubuntu-latest services: From 4edb27bf1674fa1163d219a901e7994f39a4b6a8 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 25 Nov 2025 17:03:00 -0500 Subject: [PATCH 45/57] fix test --- .../migrations/0059_add_fluorophore_and_new_models.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py index e2c39980f..2cc237e28 100644 --- a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py +++ b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py @@ -1011,7 +1011,9 @@ class Migration(migrations.Migration): """, reverse_sql=migrations.RunSQL.noop, ), - # Step 4: Remove GenericForeignKey fields from OcFluorEff + # Step 4: Update unique constraint on OcFluorEff + migrations.AlterUniqueTogether(name="ocfluoreff", unique_together={("oc", "fluor")}), + # Step 5: Remove GenericForeignKey fields from OcFluorEff migrations.RemoveField( model_name="ocfluoreff", name="content_type", @@ -1020,7 +1022,7 @@ class Migration(migrations.Migration): model_name="ocfluoreff", name="object_id", ), - # Step 5: Make fluor FK on OcFluorEff non-nullable + # Step 6: Make fluor FK on OcFluorEff non-nullable migrations.AlterField( model_name="ocfluoreff", name="fluor", @@ -1030,5 +1032,4 @@ class Migration(migrations.Migration): to="proteins.fluorophore", ), ), - migrations.AlterUniqueTogether(name="ocfluoreff", unique_together={("oc", "fluor")}), ] From e47d6c74fd3e8492b1fb9b212024f9da57e3586f Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 25 Nov 2025 17:08:04 -0500 Subject: [PATCH 46/57] fix typing --- backend/proteins/models/microscope.py | 4 ++-- backend/proteins/models/mixins.py | 10 ++++++++-- backend/references/models.py | 6 ++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/backend/proteins/models/microscope.py b/backend/proteins/models/microscope.py index 171dcb6c2..93dc6d09e 100644 --- a/backend/proteins/models/microscope.py +++ b/backend/proteins/models/microscope.py @@ -250,7 +250,7 @@ class OpticalConfig(OwnedCollection): through="FilterPlacement", ) light_id: int | None - light: models.ForeignKey[Light] = models.ForeignKey( + light: models.ForeignKey[Light | None] = models.ForeignKey( "Light", null=True, blank=True, @@ -258,7 +258,7 @@ class OpticalConfig(OwnedCollection): on_delete=models.SET_NULL, ) camera_id: int | None - camera: models.ForeignKey[Camera] = models.ForeignKey( + camera: models.ForeignKey[Camera | None] = models.ForeignKey( "Camera", null=True, blank=True, diff --git a/backend/proteins/models/mixins.py b/backend/proteins/models/mixins.py index f780eb005..7169d6cda 100644 --- a/backend/proteins/models/mixins.py +++ b/backend/proteins/models/mixins.py @@ -1,8 +1,14 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.db import models from django.urls import reverse +if TYPE_CHECKING: + from django.contrib.auth.models import AbstractUser User = get_user_model() @@ -17,7 +23,7 @@ def get_admin_url(self): class Authorable(models.Model): created_by_id: int | None - created_by: models.ForeignKey["User | None"] = models.ForeignKey( + created_by: models.ForeignKey[AbstractUser | None] = models.ForeignKey( User, blank=True, null=True, @@ -25,7 +31,7 @@ class Authorable(models.Model): on_delete=models.SET_NULL, ) updated_by_id: int | None - updated_by: models.ForeignKey["User | None"] = models.ForeignKey( + updated_by: models.ForeignKey[AbstractUser | None] = models.ForeignKey( User, blank=True, null=True, diff --git a/backend/references/models.py b/backend/references/models.py index 7e38d76af..25d103b77 100644 --- a/backend/references/models.py +++ b/backend/references/models.py @@ -20,6 +20,8 @@ from proteins.validators import validate_doi if TYPE_CHECKING: + from django.contrib.auth.models import AbstractUser + from proteins.models import ( BleachMeasurement, Excerpt, @@ -124,7 +126,7 @@ class Reference(TimeStampedModel): summary = models.CharField(max_length=512, blank=True, help_text="Brief summary of findings") created_by_id: int | None - created_by: models.ForeignKey[User | None] = models.ForeignKey( + created_by: models.ForeignKey[AbstractUser | None] = models.ForeignKey( User, related_name="reference_author", blank=True, @@ -132,7 +134,7 @@ class Reference(TimeStampedModel): on_delete=models.CASCADE, ) updated_by_id: int | None - updated_by: models.ForeignKey[User | None] = models.ForeignKey( + updated_by: models.ForeignKey[AbstractUser | None] = models.ForeignKey( User, related_name="reference_modifier", blank=True, From 9c3cc575090e22692f8a9468dfc6c14f7b3c8d75 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 25 Nov 2025 17:53:40 -0500 Subject: [PATCH 47/57] cleanup --- backend/proteins/models/fluorophore.py | 70 +++++++------------------- 1 file changed, 19 insertions(+), 51 deletions(-) diff --git a/backend/proteins/models/fluorophore.py b/backend/proteins/models/fluorophore.py index c2a028abc..d85286121 100644 --- a/backend/proteins/models/fluorophore.py +++ b/backend/proteins/models/fluorophore.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, Final, cast from django.db import models from django.utils.text import slugify @@ -134,81 +134,49 @@ def rebuild_attributes(self) -> None: 3. Primary reference - Measurement from the owner's primary_reference 4. Most recent - Fallback by date_measured (most recent first) """ - from proteins.models import FluorescenceMeasurement - measurable_fields = AbstractFluorescenceData.get_measurable_fields() new_values: dict[str, object] = {} new_source_map: dict[str, int] = {} - - # Get primary reference ID for priority sorting primary_ref_id = self._get_primary_reference_id() - # 1. Handle pinned fields first (admin overrides) + # Fetch pinned measurements in one query + pinned_source_map = cast("dict[str, int]", self.pinned_source_map) + pinned_by_id = {m.id: m for m in self.measurements.filter(id__in=pinned_source_map.values())} + + # Handle pinned fields first (admin overrides) pinned_fields: set[str] = set() - for field, measurement_id in self.pinned_source_map.items(): - if field not in measurable_fields: - continue - try: - measurement = FluorescenceMeasurement.objects.get(id=measurement_id, fluorophore=self) - val = getattr(measurement, field) - if val is not None: + for field, mid in pinned_source_map.items(): + if field in measurable_fields and (m := pinned_by_id.get(mid)): + if (val := getattr(m, field)) is not None: new_values[field] = val - new_source_map[field] = measurement.id + new_source_map[field] = m.id pinned_fields.add(field) - except FluorescenceMeasurement.DoesNotExist: - # Pinned measurement no longer exists, skip it - pass - - # 2. Fetch all measurements for non-pinned fields - measurements = list(self.measurements.select_related("reference").all()) # Sort by priority: trusted > primary_reference > most recent date - def measurement_priority(m: FluorescenceMeasurement) -> tuple[int, int, str]: - # Lower tuple = higher priority (sorted ascending) - trusted_score = 0 if m.is_trusted else 1 - primary_score = 0 if primary_ref_id and m.reference_id == primary_ref_id else 1 - # Use date_measured for sorting, fallback to empty string (sorts last) - date_str = m.date_measured.isoformat() if m.date_measured else "" - # Negate by reversing string comparison for descending date order - return (trusted_score, primary_score, date_str) - - # Sort: trusted first, then primary ref, then most recent date - measurements.sort(key=measurement_priority) - # Reverse date sorting within groups (we want most recent first) - # Re-sort with proper date handling - measurements.sort( + measurements = sorted( + self.measurements.all(), key=lambda m: ( - 0 if m.is_trusted else 1, - 0 if primary_ref_id and m.reference_id == primary_ref_id else 1, - # Invert date for descending order (most recent first) + not m.is_trusted, + not (primary_ref_id and m.reference_id == primary_ref_id), -(m.date_measured.toordinal() if m.date_measured else 0), - ) + ), ) - # 3. Waterfall Logic: Find the first non-null value for each non-pinned field + # Waterfall: first non-null value for each non-pinned field for field in measurable_fields: if field in pinned_fields: - continue # Already handled above - + continue for m in measurements: - val = getattr(m, field) - if val is not None: + if (val := getattr(m, field)) is not None: new_values[field] = val new_source_map[field] = m.id break else: - # No measurement had a value for this field - # Use field default for non-nullable fields (e.g., is_dark) field_obj = self._meta.get_field(field) - if field_obj.has_default(): - new_values[field] = field_obj.get_default() - else: - new_values[field] = None + new_values[field] = field_obj.get_default() if field_obj.has_default() else None - # 4. Update cached values for key, val in new_values.items(): setattr(self, key, val) - self.source_map = new_source_map self.save() From 97fcada259e3f6ee9b23291c364519b68de7773f Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 25 Nov 2025 17:57:57 -0500 Subject: [PATCH 48/57] remove files --- migration_notes.md | 10 --------- test_migration.sh | 53 ---------------------------------------------- 2 files changed, 63 deletions(-) delete mode 100644 migration_notes.md delete mode 100755 test_migration.sh diff --git a/migration_notes.md b/migration_notes.md deleted file mode 100644 index 675d4d1e7..000000000 --- a/migration_notes.md +++ /dev/null @@ -1,10 +0,0 @@ -# Migration notes - - - `common name` (VARCHAR 255) - - `molecular formula` in Hill notation (VARCHAR) - - `molecular weight` (DECIMAL 10,4) - - `canonical SMILES` (TEXT) as the primary structure representation - - `InChI` (TEXT) for standardized structure encoding - - `InChIKey` (CHAR 27) provide uniqueness guarantees and cross-database compatibility - - `CAS numbers` (VARCHAR 50) enable literature searches - - `PubChem CIDs` (INTEGER) offer free cross-referencing to the world's largest chemical database. diff --git a/test_migration.sh b/test_migration.sh deleted file mode 100755 index 96db56232..000000000 --- a/test_migration.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/bash -set -e # Exit on error - -# Colors for output -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -SOURCE_DB="fpbase_pre" -TARGET_DB="fpbase_migrated" - -echo -e "${YELLOW}=== FPbase Migration Testing Script ===${NC}" -echo "" -echo "This script will:" -echo " 1. Drop '${TARGET_DB}' database if it exists" -echo " 2. Create a fresh copy from '${SOURCE_DB}'" -echo " 3. Run migration 0059 on the new database" -echo " 4. Leave '${SOURCE_DB}' completely untouched" -echo "" - -# Check if source database exists -if ! psql -lqt | cut -d \| -f 1 | grep -qw "$SOURCE_DB"; then - echo "ERROR: Source database '$SOURCE_DB' does not exist!" - exit 1 -fi - -echo -e "${GREEN}Step 1: Dropping existing '${TARGET_DB}' database (if it exists)${NC}" -psql postgres -c "DROP DATABASE IF EXISTS ${TARGET_DB};" 2>/dev/null || true - -echo -e "${GREEN}Step 2: Creating '${TARGET_DB}' as a copy of '${SOURCE_DB}'${NC}" -psql postgres -c "CREATE DATABASE ${TARGET_DB} WITH TEMPLATE ${SOURCE_DB};" - -echo -e "${GREEN}Step 3: Running migration 0059 on '${TARGET_DB}'${NC}" - -# Set DATABASE_URL to point to the new database -# This assumes PostgreSQL is running locally on default port -export DATABASE_URL="postgresql://localhost/${TARGET_DB}" - -# Run the specific migration -uv run backend/manage.py migrate proteins 0059 - -echo "" -echo -e "${GREEN}=== Migration complete! ===${NC}" -echo "" -echo "Databases:" -echo " - ${SOURCE_DB}: untouched (original test data)" -echo " - ${TARGET_DB}: migrated (contains new schema)" -echo "" -echo "To inspect the migrated database:" -echo " psql ${TARGET_DB}" -echo "" -echo "To run this script again:" -echo " ./test_migration.sh" From 24a767588722b8c1772dee9db3e5dab690be769e Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 25 Nov 2025 19:20:20 -0500 Subject: [PATCH 49/57] fix fret stuff --- backend/proteins/factories.py | 54 +++++++++++++------ .../0059_add_fluorophore_and_new_models.py | 2 +- backend/proteins/models/dye.py | 2 +- backend/proteins/views/fret.py | 39 ++++++-------- .../tests/test_proteins/test_ajax_views.py | 26 ++++----- backend/tests_e2e/test_e2e.py | 4 +- 6 files changed, 72 insertions(+), 55 deletions(-) diff --git a/backend/proteins/factories.py b/backend/proteins/factories.py index 15277c406..155dbad9e 100644 --- a/backend/proteins/factories.py +++ b/backend/proteins/factories.py @@ -9,7 +9,19 @@ 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.models import ( + Camera, + Dye, + DyeState, + Filter, + FilterPlacement, + Light, + Microscope, + OpticalConfig, + Protein, + Spectrum, + State, +) from proteins.util.helpers import wave_to_hex from references.factories import ReferenceFactory @@ -181,17 +193,6 @@ class Meta: exhex = factory.LazyAttribute(lambda o: wave_to_hex(o.ex_max)) is_dark = False - -class StateFactory(FluorophoreFactory): - class Meta: - model = State - django_get_or_create = ("name", "slug") - - name = "default" - slug = factory.LazyAttribute(lambda o: f"{o.protein.slug}_{slugify(o.name)}") - maturation = factory.Faker("pyfloat", min_value=0, max_value=1600) - protein = factory.SubFactory("proteins.factories.ProteinFactory", default_state=None) - ex_spectrum = factory.RelatedFactory( "proteins.factories.SpectrumFactory", factory_related_name="owner_fluor", @@ -212,13 +213,14 @@ class Meta: ) -class DyeFactory(factory.django.DjangoModelFactory): +class StateFactory(FluorophoreFactory): class Meta: - model = "proteins.Dye" + model = State django_get_or_create = ("name", "slug") - name = factory.Sequence(lambda n: f"TestDye{n}") - slug = factory.LazyAttribute(lambda o: slugify(o.name)) + slug = factory.LazyAttribute(lambda o: f"{o.protein.slug}_{slugify(o.name)}") + maturation = factory.Faker("pyfloat", min_value=0, max_value=1600) + protein = factory.SubFactory("proteins.factories.ProteinFactory", default_state=None) class ProteinFactory(factory.django.DjangoModelFactory[Protein]): @@ -238,6 +240,26 @@ class Meta: default_state = factory.RelatedFactory(StateFactory, factory_related_name="protein") +class DyeStateFactory(FluorophoreFactory): + class Meta: + model = DyeState + django_get_or_create = ("name", "slug") + + slug = factory.LazyAttribute(lambda o: f"{o.dye.slug}_{slugify(o.name)}") + dye = factory.SubFactory("proteins.factories.DyeFactory", default_state=None) + + +class DyeFactory(factory.django.DjangoModelFactory[Dye]): + class Meta: + model = Dye + django_get_or_create = ("name", "slug") + + name = factory.Sequence(lambda n: f"TestDye{n}") + slug = factory.LazyAttribute(lambda o: slugify(o.name)) + primary_reference = factory.SubFactory("references.factories.ReferenceFactory") + default_state = factory.RelatedFactory(DyeStateFactory, factory_related_name="dye") + + class SpectrumFactory(factory.django.DjangoModelFactory): class Meta: model = Spectrum diff --git a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py index 2cc237e28..c18b17699 100644 --- a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py +++ b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py @@ -842,7 +842,7 @@ class Migration(migrations.Migration): fields=[ ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), ("name", models.CharField(max_length=255, db_index=True)), - ("slug", models.SlugField(unique=True)), + ("slug", models.SlugField(max_length=200, unique=True)), ( "primary_reference", models.ForeignKey( diff --git a/backend/proteins/models/dye.py b/backend/proteins/models/dye.py index 0ba481011..9bed4d66c 100644 --- a/backend/proteins/models/dye.py +++ b/backend/proteins/models/dye.py @@ -20,7 +20,7 @@ class Dye(Authorable, TimeStampedModel, Product): # TODO: rename to SmallMolecu # --- Identification --- name = models.CharField(max_length=255, db_index=True) - slug = models.SlugField(unique=True) + slug = models.SlugField(max_length=200, unique=True) default_state_id: int | None default_state: models.ForeignKey["DyeState | None"] = models.ForeignKey( diff --git a/backend/proteins/views/fret.py b/backend/proteins/views/fret.py index 5580fb08a..dafb2d5fc 100644 --- a/backend/proteins/views/fret.py +++ b/backend/proteins/views/fret.py @@ -1,12 +1,13 @@ from django.core.cache import cache +from django.db.models import Case, F, Value, When +from django.db.models.functions import Concat from django.http import JsonResponse from django.shortcuts import render from fpbase.celery import app from fpbase.util import is_ajax -from proteins.models.dye import DyeState -from ..models import State +from ..models import Fluorophore from ..tasks import calc_fret @@ -30,11 +31,20 @@ def fret_chart(request): cache.set("calc_fret_job", job.id) return JsonResponse({"data": forster_list}) - slugs = ( - State.objects.exclude(ext_coeff=None) + # Query all fluorophores (States + DyeStates) with required properties + # Build display name and sort in the database + fluorophores = ( + Fluorophore.objects.exclude(ext_coeff=None) .exclude(qy=None) .filter(spectra__subtype__in=("ex", "ab")) - .values("slug", "name", "protein__name", "spectra__category", "spectra__subtype") + .annotate( + display_name=Case( + When(name="default", then=F("owner_name")), + default=Concat(F("owner_name"), Value(" ("), F("name"), Value(")")), + ) + ) + .order_by("display_name") + .values("slug", "display_name", "spectra__category", "spectra__subtype") ) slugs = [ @@ -42,24 +52,9 @@ def fret_chart(request): "slug": x["slug"], "category": x["spectra__category"], "subtype": x["spectra__subtype"], - "name": x["protein__name"] + (f" ({x['name']})" if x["name"] != "default" else ""), + "name": x["display_name"], } - for x in slugs + for x in fluorophores ] - good_dyes = ( - DyeState.objects.exclude(ext_coeff=None).exclude(qy=None).filter(spectra__subtype__in=("ex", "ab")) - ).values("slug", "name", "spectra__category", "spectra__subtype") - - slugs += [ - { - "slug": x["slug"], - "category": x["spectra__category"], - "subtype": x["spectra__subtype"], - "name": x["name"], - } - for x in good_dyes - ] - - slugs.sort(key=lambda x: x["name"]) return render(request, template, {"probeslugs": slugs}) diff --git a/backend/tests/test_proteins/test_ajax_views.py b/backend/tests/test_proteins/test_ajax_views.py index 8eb07e248..59ba3ecef 100644 --- a/backend/tests/test_proteins/test_ajax_views.py +++ b/backend/tests/test_proteins/test_ajax_views.py @@ -33,28 +33,28 @@ class SimilarSpectrumOwnersViewTests(TestCase): def setUpTestData(cls): """Create test data once for all tests in this class.""" # Create proteins with states - all with similar names so find_similar_owners finds them + # Use default_state=None to prevent auto-creation, then create states with name="default" + # so that Fluorophore.label returns just the protein name cls.proteins = [] cls.states = [] for i in range(5): - protein = ProteinFactory(name=f"SimilarProtein{i}") - state = StateFactory(protein=protein, name=f"state{i}") + protein = ProteinFactory(name=f"SimilarProtein{i}", default_state=None) + state = StateFactory(protein=protein, name="default") cls.proteins.append(protein) cls.states.append(state) - # Add spectra to states - SpectrumFactory(owner_fluor=state, category=Spectrum.PROTEIN, subtype=Spectrum.EX) - SpectrumFactory(owner_fluor=state, category=Spectrum.PROTEIN, subtype=Spectrum.EM) # Create dyes - note: DyeState is the fluorophore that owns spectra, not Dye itself + # Use default_state=None to prevent auto-creation cls.dyes = [] cls.dye_states = [] for i in range(3): - dye = DyeFactory(name=f"SimilarDye{i}") + dye = DyeFactory(name=f"SimilarDye{i}", default_state=None) cls.dyes.append(dye) - # Create DyeState as the actual fluorophore owner of spectra + # Create DyeState with name="default" so label returns just the dye name dye_state = DyeState.objects.create( dye=dye, - name=f"SimilarDye{i} state", - slug=f"similardye{i}-state", + name="default", + slug=f"similardye{i}-default", ex_max=488, em_max=520, ) @@ -276,18 +276,18 @@ def test_similar_spectrum_owners_state_includes_protein_name(self): self.assertIn(self.proteins[0].name, names) def test_similar_spectrum_owners_dye_uses_own_name(self): - """Test that DyeStates (Fluorophores without protein) use their label.""" + """Test that DyeStates (Fluorophores without protein) use their dye's name.""" response = self.client.post( "/ajax/validate_spectrumownername/", - {"owner": self.dye_states[0].label}, + {"owner": self.dyes[0].name}, headers={"X-Requested-With": "XMLHttpRequest"}, ) data = response.json() self.assertGreater(len(data["similars"]), 0) - # At least one should be the dye state we searched for + # At least one should be the dye we searched for (label returns dye name when state name is "default") names = [s["name"] for s in data["similars"]] - self.assertIn(self.dye_states[0].label, names) + self.assertIn(self.dyes[0].name, names) def test_similar_spectrum_owners_works_without_ajax_header(self): """Test that the endpoint works without the X-Requested-With header.""" diff --git a/backend/tests_e2e/test_e2e.py b/backend/tests_e2e/test_e2e.py index 43c3bfea5..cfd7801c1 100644 --- a/backend/tests_e2e/test_e2e.py +++ b/backend/tests_e2e/test_e2e.py @@ -18,6 +18,7 @@ from favit.models import Favorite from proteins.factories import ( + DyeFactory, FilterFactory, MicroscopeFactory, OpticalConfigWithFiltersFactory, @@ -332,9 +333,8 @@ def test_fret_page_loads(live_server: LiveServer, page: Page, assert_snapshot: C default_state__em_max=525, default_state__qy=0.8, ) - ProteinFactory( + DyeFactory( name="acceptor", - agg="m", default_state__ex_max=525, default_state__em_max=550, default_state__ext_coeff=55000, From 17c78f0b259f991b76f1c430fa623b78e342a0bd Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 25 Nov 2025 20:05:54 -0500 Subject: [PATCH 50/57] test scope report --- backend/proteins/views/microscope.py | 49 ++---- backend/tests/test_proteins/test_tasks.py | 53 +++++- backend/tests/test_proteins/test_views.py | 195 +++++++++++++++++++++- 3 files changed, 264 insertions(+), 33 deletions(-) diff --git a/backend/proteins/views/microscope.py b/backend/proteins/views/microscope.py index 4fa6eb552..1521a8c6a 100644 --- a/backend/proteins/views/microscope.py +++ b/backend/proteins/views/microscope.py @@ -11,42 +11,29 @@ from django.db import transaction from django.db.models import Case, CharField, Count, F, Q, Value, When from django.db.models.functions import Cast, Lower -from django.http import ( - Http404, - HttpResponseNotAllowed, - HttpResponseRedirect, - JsonResponse, -) +from django.http import Http404, HttpResponseNotAllowed, HttpResponseRedirect, JsonResponse from django.urls import resolve, reverse_lazy from django.utils.decorators import method_decorator from django.views.decorators.clickjacking import xframe_options_exempt -from django.views.generic import ( - CreateView, - DeleteView, - DetailView, - ListView, - UpdateView, -) +from django.views.generic import CreateView, DeleteView, DetailView, ListView, UpdateView from fpbase.celery import app from fpbase.util import is_ajax -from proteins.models.dye import DyeState - -from ..forms import MicroscopeForm, OpticalConfigFormSet -from ..models import ( +from proteins.forms import MicroscopeForm, OpticalConfigFormSet +from proteins.models import ( Camera, + DyeState, + Fluorophore, Light, Microscope, + OcFluorEff, OpticalConfig, ProteinCollection, Spectrum, State, ) - -# from ..util.efficiency import microscope_efficiency_report -from ..models.efficiency import OcFluorEff -from ..tasks import calculate_scope_report -from .mixins import OwnableObject +from proteins.tasks import calculate_scope_report +from proteins.views.mixins import OwnableObject def update_scope_report(request): @@ -105,19 +92,19 @@ def scope_report_json(request, pk): .annotate( # For proteins, use the protein's UUID; for dyes, use the dye's ID (cast to string) owner_id=Case( - When(fluor__entity_type="protein", then=F("fluor__state__protein__uuid")), - When(fluor__entity_type="dye", then=Cast(F("fluor__dyestate__dye__id"), CharField())), + When( + fluor__entity_type=Fluorophore.EntityTypes.PROTEIN, + then=F("fluor__state__protein__uuid"), + ), + When( + fluor__entity_type=Fluorophore.EntityTypes.DYE, + then=Cast(F("fluor__dyestate__dye__id"), CharField()), + ), default=Value(None), output_field=CharField(), ), owner_slug=F("fluor__slug"), - # Map entity_type to the old 'p'/'d' format - type=Case( - When(fluor__entity_type="protein", then=Value("p")), - When(fluor__entity_type="dye", then=Value("d")), - default=Value("p"), - output_field=CharField(), - ), + type=F("fluor__entity_type"), ) .values( "owner_id", diff --git a/backend/tests/test_proteins/test_tasks.py b/backend/tests/test_proteins/test_tasks.py index 5a4c9fd36..eae3dc8d6 100644 --- a/backend/tests/test_proteins/test_tasks.py +++ b/backend/tests/test_proteins/test_tasks.py @@ -4,7 +4,7 @@ import pytest -from proteins.factories import MicroscopeFactory, OpticalConfigWithFiltersFactory, StateFactory +from proteins.factories import DyeStateFactory, MicroscopeFactory, OpticalConfigWithFiltersFactory, StateFactory from proteins.models import OcFluorEff from proteins.tasks import calc_fret, calculate_scope_report @@ -76,6 +76,57 @@ def test_calculate_scope_report_batch_processing(self): eff_count = OcFluorEff.objects.filter(oc=optical_config).count() assert eff_count > 0 + def test_calculate_scope_report_creates_efficiency_records_for_dyes(self): + """Test that calculate_scope_report creates OcFluorEff records for DyeStates. + + This exercises the DyeState code path which uses a different ID mapping + than State (protein fluorophores). + """ + microscope = MicroscopeFactory() + optical_config = OpticalConfigWithFiltersFactory(microscope=microscope) + dye_state = DyeStateFactory() + + # Ensure the dye state has spectra (required for with_spectra() filter) + assert dye_state.has_spectra() + + # No OcFluorEff records should exist yet + assert OcFluorEff.objects.count() == 0 + + # Mock the task's update_state to avoid issues + with patch.object(calculate_scope_report, "update_state"): + calculate_scope_report.run(scope_id=microscope.id) + + # OcFluorEff records should now exist for the dye state + assert OcFluorEff.objects.count() > 0 + eff = OcFluorEff.objects.filter(oc=optical_config, fluor=dye_state).first() + assert eff is not None, "OcFluorEff record should exist for dye state" + + def test_calculate_scope_report_with_both_states_and_dyes(self): + """Test that calculate_scope_report handles both State and DyeState. + + This validates the migration's handling of mixed fluorophore types. + """ + microscope = MicroscopeFactory() + optical_config = OpticalConfigWithFiltersFactory(microscope=microscope) + + # Create both a protein state and a dye state + state = StateFactory() + dye_state = DyeStateFactory() + + assert state.has_spectra() + assert dye_state.has_spectra() + + # Run the task + with patch.object(calculate_scope_report, "update_state"): + calculate_scope_report.run(scope_id=microscope.id) + + # Should have created OcFluorEff records for both + state_eff = OcFluorEff.objects.filter(oc=optical_config, fluor=state).first() + dye_eff = OcFluorEff.objects.filter(oc=optical_config, fluor=dye_state).first() + + assert state_eff is not None, "OcFluorEff should exist for protein state" + assert dye_eff is not None, "OcFluorEff should exist for dye state" + @pytest.mark.django_db class TestCalculateScopeReport: diff --git a/backend/tests/test_proteins/test_views.py b/backend/tests/test_proteins/test_views.py index 866c8ec9b..22e925978 100644 --- a/backend/tests/test_proteins/test_views.py +++ b/backend/tests/test_proteins/test_views.py @@ -1,12 +1,16 @@ import json from typing import cast +from unittest.mock import patch +import pytest from django.contrib.auth import get_user_model from django.core.files.uploadedfile import SimpleUploadedFile from django.test import TestCase from django.urls import reverse -from proteins.models import Protein, Spectrum, State +from proteins.factories import DyeStateFactory, MicroscopeFactory, OpticalConfigWithFiltersFactory, StateFactory +from proteins.models import OcFluorEff, Protein, Spectrum, State +from proteins.tasks import calculate_scope_report User = get_user_model() @@ -262,3 +266,192 @@ def test_spectrum_preview_data_source_defaults_to_file(self): self.assertIn("form_errors", data) # Should show validation error since it defaults to file validation self.assertTrue("__all__" in data["form_errors"] or "file" in data["form_errors"]) + + +@pytest.mark.django_db +class TestScopeReportJson: + """Test scope_report_json view that serves microscope efficiency report data. + + These tests exercise the query paths through the Fluorophore MTI structure, + ensuring both State (protein) and DyeState entries are handled correctly. + """ + + def test_scope_report_json_returns_valid_response(self, client): + """Test that scope_report_json returns valid JSON structure.""" + microscope = MicroscopeFactory() + OpticalConfigWithFiltersFactory(microscope=microscope) + + url = reverse("proteins:scope_report_json", args=[microscope.id]) + response = client.get(url) + + assert response.status_code == 200 + data = response.json() + + # Check expected top-level structure + assert "microscope" in data + assert "report" in data + assert "fluors" in data + assert data["microscope"] == microscope.id + + def test_scope_report_json_includes_protein_states(self, client): + """Test that scope_report_json correctly includes protein States.""" + microscope = MicroscopeFactory() + oc = OpticalConfigWithFiltersFactory(microscope=microscope) + state = StateFactory() + + # Create OcFluorEff record using bulk_create to bypass save() which + # would call update_effs() and overwrite our test values + OcFluorEff.objects.bulk_create( + [ + OcFluorEff( + oc=oc, + fluor=state, + fluor_name=str(state), + ex_eff=0.8, + em_eff=0.7, + brightness=50.0, + ) + ] + ) + + url = reverse("proteins:scope_report_json", args=[microscope.id]) + response = client.get(url) + + assert response.status_code == 200 + data = response.json() + + # State should appear in report + assert len(data["report"]) > 0 + config_data = data["report"][0] + assert "values" in config_data + + # Find the state in the values + state_entries = [v for v in config_data["values"] if v["fluor_slug"] == state.slug] + assert len(state_entries) == 1, f"State {state.slug} should appear in report" + + entry = state_entries[0] + assert entry["shape"] == "circle", "Protein states should have circle shape" + assert entry["ex_eff"] == 0.8 + assert entry["em_eff"] == 0.7 + + # State should appear in fluors dict + assert state.slug in data["fluors"] + fluor_data = data["fluors"][state.slug] + assert fluor_data["type"] == "p", "Protein fluorophores should have type 'p'" + + def test_scope_report_json_includes_dye_states(self, client): + """Test that scope_report_json correctly includes DyeStates.""" + microscope = MicroscopeFactory() + oc = OpticalConfigWithFiltersFactory(microscope=microscope) + dye_state = DyeStateFactory() + + # Create OcFluorEff record using bulk_create to bypass save() + OcFluorEff.objects.bulk_create( + [ + OcFluorEff( + oc=oc, + fluor=dye_state, + fluor_name=str(dye_state), + ex_eff=0.9, + em_eff=0.6, + brightness=40.0, + ) + ] + ) + + url = reverse("proteins:scope_report_json", args=[microscope.id]) + response = client.get(url) + + assert response.status_code == 200 + data = response.json() + + # DyeState should appear in report + assert len(data["report"]) > 0 + config_data = data["report"][0] + + # Find the dye state in the values + dye_entries = [v for v in config_data["values"] if v["fluor_slug"] == dye_state.slug] + assert len(dye_entries) == 1, f"DyeState {dye_state.slug} should appear in report" + + entry = dye_entries[0] + assert entry["shape"] == "square", "Dye states should have square shape" + assert entry["ex_eff"] == 0.9 + assert entry["em_eff"] == 0.6 + + # DyeState should appear in fluors dict + assert dye_state.slug in data["fluors"] + fluor_data = data["fluors"][dye_state.slug] + assert fluor_data["type"] == "d", "Dye fluorophores should have type 'd'" + + def test_scope_report_json_with_both_states_and_dyes(self, client): + """Test that scope_report_json handles mixed State and DyeState data. + + This is the key test for validating the migration's handling of + the new Fluorophore MTI structure with both entity types. + """ + microscope = MicroscopeFactory() + oc = OpticalConfigWithFiltersFactory(microscope=microscope) + state = StateFactory() + dye_state = DyeStateFactory() + + # Create OcFluorEff records for both using bulk_create + OcFluorEff.objects.bulk_create( + [ + OcFluorEff(oc=oc, fluor=state, fluor_name=str(state), ex_eff=0.8, em_eff=0.7), + OcFluorEff(oc=oc, fluor=dye_state, fluor_name=str(dye_state), ex_eff=0.9, em_eff=0.6), + ] + ) + + url = reverse("proteins:scope_report_json", args=[microscope.id]) + response = client.get(url) + + assert response.status_code == 200 + data = response.json() + + # Both should appear in report + config_data = data["report"][0] + slugs_in_report = [v["fluor_slug"] for v in config_data["values"]] + + assert state.slug in slugs_in_report, "State should appear in report" + assert dye_state.slug in slugs_in_report, "DyeState should appear in report" + + # Both should be in fluors dict with correct types + assert data["fluors"][state.slug]["type"] == "p" + assert data["fluors"][dye_state.slug]["type"] == "d" + + def test_scope_report_json_full_workflow_integration(self, client): + """Integration test: run calculate_scope_report then verify JSON output. + + This tests the complete workflow from task execution to JSON response, + validating that the migration preserves the end-to-end behavior. + """ + microscope = MicroscopeFactory() + OpticalConfigWithFiltersFactory(microscope=microscope) + state = StateFactory() + dye_state = DyeStateFactory() + + # Verify both have spectra + assert state.has_spectra() + assert dye_state.has_spectra() + + # Run the task to create OcFluorEff records + with patch.object(calculate_scope_report, "update_state"): + calculate_scope_report.run(scope_id=microscope.id) + + # Verify OcFluorEff records were created + assert OcFluorEff.objects.count() >= 2 + + # Now test the JSON endpoint + url = reverse("proteins:scope_report_json", args=[microscope.id]) + response = client.get(url) + + assert response.status_code == 200 + data = response.json() + + # Verify structure + assert "report" in data + assert "fluors" in data + + # Verify fluors dict contains our fluorophores + assert state.slug in data["fluors"], f"State {state.slug} missing from fluors" + assert dye_state.slug in data["fluors"], f"DyeState {dye_state.slug} missing from fluors" From 618c7f6f54792c8ff5570d6d47e5c5a38dabbe39 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 25 Nov 2025 20:49:27 -0500 Subject: [PATCH 51/57] chang m2m typing --- backend/proteins/models/collection.py | 14 +++++----- backend/proteins/models/excerpt.py | 9 ++++--- backend/proteins/models/microscope.py | 30 ++++++++++------------ backend/proteins/models/protein.py | 37 ++++++++++++--------------- backend/references/models.py | 33 ++++++++++++------------ 5 files changed, 60 insertions(+), 63 deletions(-) diff --git a/backend/proteins/models/collection.py b/backend/proteins/models/collection.py index 69ce80ed5..fd914e9c6 100644 --- a/backend/proteins/models/collection.py +++ b/backend/proteins/models/collection.py @@ -48,10 +48,11 @@ class Meta: class ProteinCollection(OwnedCollection): if TYPE_CHECKING: - proteins: models.ManyToManyField[Protein, ProteinCollection] on_scope: models.QuerySet[Microscope] - else: - proteins = models.ManyToManyField("Protein", related_name="collection_memberships") + + proteins: models.ManyToManyField[Protein, ProteinCollection] = models.ManyToManyField( + "Protein", related_name="collection_memberships" + ) private = models.BooleanField( default=False, verbose_name="Private Collection", @@ -66,11 +67,12 @@ class Meta: class FluorophoreCollection(ProteinCollection): + dyes: models.ManyToManyField[Dye, FluorophoreCollection] = models.ManyToManyField( + "Dye", blank=True, related_name="collection_memberships" + ) + if TYPE_CHECKING: - dyes: models.ManyToManyField[Dye, FluorophoreCollection] = models.ManyToManyField() fluor_on_scope: models.QuerySet[Microscope] - else: - dyes = models.ManyToManyField("Dye", blank=True, related_name="collection_memberships") def get_absolute_url(self): return reverse("proteins:fluor-collection-detail", args=[self.id]) diff --git a/backend/proteins/models/excerpt.py b/backend/proteins/models/excerpt.py index 621aeff2f..abf1bb9c3 100644 --- a/backend/proteins/models/excerpt.py +++ b/backend/proteins/models/excerpt.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import TYPE_CHECKING from django.db import models @@ -17,10 +19,9 @@ class Excerpt(Authorable, TimeStampedModel, StatusModel): STATUS = Choices("approved", "flagged", "rejected") content = models.TextField(max_length=1200, help_text="Brief excerpt describing this protein") - if TYPE_CHECKING: - proteins: models.ManyToManyField["Protein", "Excerpt"] - else: - proteins = models.ManyToManyField("Protein", blank=True, related_name="excerpts") + proteins: models.ManyToManyField[Protein, Excerpt] = models.ManyToManyField( + "Protein", blank=True, related_name="excerpts" + ) reference_id: int | None reference: models.ForeignKey[Reference | None] = models.ForeignKey( Reference, diff --git a/backend/proteins/models/microscope.py b/backend/proteins/models/microscope.py index 93dc6d09e..66692cb7d 100644 --- a/backend/proteins/models/microscope.py +++ b/backend/proteins/models/microscope.py @@ -21,6 +21,7 @@ if TYPE_CHECKING: from django.db.models.manager import RelatedManager + from proteins.models import OcFluorEff from proteins.models.collection import FluorophoreCollection, ProteinCollection from proteins.models.spectrum import D3Dict, Spectrum @@ -36,12 +37,12 @@ class Microscope(OwnedCollection): """ id = models.CharField(primary_key=True, max_length=22, default=shortuuid, editable=False) - if TYPE_CHECKING: - extra_lights: models.ManyToManyField[Light, Microscope] = models.ManyToManyField() - extra_cameras: models.ManyToManyField[Camera, Microscope] = models.ManyToManyField() - else: - extra_lights = models.ManyToManyField("Light", blank=True, related_name="microscopes") - extra_cameras = models.ManyToManyField("Camera", blank=True, related_name="microscopes") + extra_lights: models.ManyToManyField[Light, Microscope] = models.ManyToManyField( + "Light", blank=True, related_name="microscopes" + ) + extra_cameras: models.ManyToManyField[Camera, Microscope] = models.ManyToManyField( + "Camera", blank=True, related_name="microscopes" + ) extra_lasers = ArrayField( models.PositiveSmallIntegerField(validators=[MinValueValidator(300), MaxValueValidator(1600)]), default=list, @@ -237,18 +238,15 @@ class OpticalConfig(OwnedCollection): ) comments = models.CharField(max_length=256, blank=True) if TYPE_CHECKING: - from proteins.models import OcFluorEff - - filters: models.ManyToManyField[Filter, OpticalConfig] = models.ManyToManyField() filterplacement_set: models.QuerySet[FilterPlacement] ocfluoreff_set: models.QuerySet[OcFluorEff] - else: - filters = models.ManyToManyField( - "Filter", - related_name="optical_configs", - blank=True, - through="FilterPlacement", - ) + + filters: models.ManyToManyField[Filter, OpticalConfig] = models.ManyToManyField( + "Filter", + related_name="optical_configs", + blank=True, + through="FilterPlacement", + ) light_id: int | None light: models.ForeignKey[Light | None] = models.ForeignKey( "Light", diff --git a/backend/proteins/models/protein.py b/backend/proteins/models/protein.py index e1e5a5cb3..76dfbde74 100644 --- a/backend/proteins/models/protein.py +++ b/backend/proteins/models/protein.py @@ -246,10 +246,9 @@ class CofactorChoices(models.TextChoices): on_delete=models.SET_NULL, help_text="Preferably the publication that introduced the protein", ) - if TYPE_CHECKING: - references: models.ManyToManyField[Reference, Protein] = models.ManyToManyField() - else: - references = models.ManyToManyField(Reference, related_name="proteins", blank=True) + references: models.ManyToManyField[Reference, Protein] = models.ManyToManyField( + Reference, related_name="proteins", blank=True + ) default_state_id: int | None default_state: models.ForeignKey[State | None] = models.ForeignKey( @@ -259,6 +258,12 @@ class CofactorChoices(models.TextChoices): null=True, on_delete=models.SET_NULL, ) + snapgene_plasmids: models.ManyToManyField[SnapGenePlasmid, Protein] = models.ManyToManyField( + "SnapGenePlasmid", + related_name="proteins", + blank=True, + help_text="Associated SnapGene plasmids", + ) if TYPE_CHECKING: states = RelatedManager["State"]() @@ -268,14 +273,6 @@ class CofactorChoices(models.TextChoices): oser_measurements: models.QuerySet[OSERMeasurement] collection_memberships: models.QuerySet[ProteinCollection] excerpts: models.QuerySet[Excerpt] - snapgene_plasmids: models.ManyToManyField[SnapGenePlasmid, Protein] = models.ManyToManyField() - else: - snapgene_plasmids = models.ManyToManyField( - "SnapGenePlasmid", - related_name="proteins", - blank=True, - help_text="Associated SnapGene plasmids", - ) # managers objects: _ProteinManager[Self] = _ProteinManager() @@ -573,21 +570,19 @@ class State(Fluorophore): # TODO: rename to ProteinState validators=[MinValueValidator(0), MaxValueValidator(1600)], ) + transitions: models.ManyToManyField[State, State] = models.ManyToManyField( + "State", + related_name="transition_state", + verbose_name="State Transitions", + blank=True, + through="StateTransition", + ) if TYPE_CHECKING: - transitions: models.ManyToManyField[State, State] = models.ManyToManyField() transition_state: models.QuerySet[State] transitions_from: models.QuerySet[StateTransition] transitions_to: models.QuerySet[StateTransition] bleach_measurements: models.QuerySet[BleachMeasurement] fluorophore_ptr: Fluorophore # added by Django MTI - else: - transitions = models.ManyToManyField( - "State", - related_name="transition_state", - verbose_name="State Transitions", - blank=True, - through="StateTransition", - ) def save(self, *args, **kwargs) -> None: self.entity_type = Fluorophore.EntityTypes.PROTEIN diff --git a/backend/references/models.py b/backend/references/models.py index 25d103b77..514de6a09 100644 --- a/backend/references/models.py +++ b/backend/references/models.py @@ -43,11 +43,12 @@ class Author(TimeStampedModel): family = models.CharField(max_length=80) given = models.CharField(max_length=80) initials = models.CharField(max_length=10) + publications: models.ManyToManyField[Reference, Author] = models.ManyToManyField( + "Reference", through="ReferenceAuthor" + ) + if TYPE_CHECKING: - publications: models.ManyToManyField[Reference, Author] = models.ManyToManyField() referenceauthor_set: models.QuerySet[ReferenceAuthor] - else: - publications = models.ManyToManyField("Reference", through="ReferenceAuthor") @property def protein_contributions(self): @@ -110,19 +111,8 @@ class Reference(TimeStampedModel): ], help_text="YYYY", ) - if TYPE_CHECKING: - authors: models.ManyToManyField[Author, Reference] = models.ManyToManyField() - referenceauthor_set: models.QuerySet[ReferenceAuthor] - primary_proteins: models.QuerySet[Protein] - proteins: models.QuerySet[Protein] - excerpts: models.QuerySet[Excerpt] - spectra: models.QuerySet[Spectrum] - bleach_measurements: models.QuerySet[BleachMeasurement] - oser_measurements: models.QuerySet[OSERMeasurement] - lineages: models.QuerySet[Lineage] - fluorescencemeasurement_set: models.QuerySet[FluorescenceMeasurement] - else: - authors = models.ManyToManyField("Author", through="ReferenceAuthor") + + authors: models.ManyToManyField[Author, Reference] = models.ManyToManyField("Author", through="ReferenceAuthor") summary = models.CharField(max_length=512, blank=True, help_text="Brief summary of findings") created_by_id: int | None @@ -142,6 +132,17 @@ class Reference(TimeStampedModel): on_delete=models.CASCADE, ) + if TYPE_CHECKING: + referenceauthor_set: models.QuerySet[ReferenceAuthor] + primary_proteins: models.QuerySet[Protein] + proteins: models.QuerySet[Protein] + excerpts: models.QuerySet[Excerpt] + spectra: models.QuerySet[Spectrum] + bleach_measurements: models.QuerySet[BleachMeasurement] + oser_measurements: models.QuerySet[OSERMeasurement] + lineages: models.QuerySet[Lineage] + fluorescencemeasurement_set: models.QuerySet[FluorescenceMeasurement] + class Meta: ordering = ("date",) From 5d99bf1d861b1633f0c289ccab8e4da055aeb35b Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 25 Nov 2025 20:55:37 -0500 Subject: [PATCH 52/57] remove date measured --- .../0059_add_fluorophore_and_new_models.py | 1 - .../models/fluorescence_measurement.py | 1 - backend/proteins/models/fluorophore.py | 5 +- .../tests/test_proteins/test_fluorophore.py | 46 +++++-------------- 4 files changed, 13 insertions(+), 40 deletions(-) diff --git a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py index c18b17699..2c8dab9cb 100644 --- a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py +++ b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py @@ -810,7 +810,6 @@ class Migration(migrations.Migration): fields=[ ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), *abstract_fluorescence_data_fields(), - ("date_measured", models.DateField(null=True, blank=True)), ("conditions", models.TextField(blank=True, help_text="pH, solvent, temp, etc.")), ( "is_trusted", diff --git a/backend/proteins/models/fluorescence_measurement.py b/backend/proteins/models/fluorescence_measurement.py index e11badc63..98913cf55 100644 --- a/backend/proteins/models/fluorescence_measurement.py +++ b/backend/proteins/models/fluorescence_measurement.py @@ -28,7 +28,6 @@ class FluorescenceMeasurement(AbstractFluorescenceData): ) # Metadata specific to the act of measuring - date_measured = models.DateField(null=True, blank=True) conditions = models.TextField(blank=True, help_text="pH, solvent, temp, etc.") # Curator Override diff --git a/backend/proteins/models/fluorophore.py b/backend/proteins/models/fluorophore.py index d85286121..051abc972 100644 --- a/backend/proteins/models/fluorophore.py +++ b/backend/proteins/models/fluorophore.py @@ -132,7 +132,7 @@ def rebuild_attributes(self) -> None: 1. Pinned overrides - Admin has explicitly pinned a measurement for a field 2. Trusted measurements - is_trusted=True on FluorescenceMeasurement 3. Primary reference - Measurement from the owner's primary_reference - 4. Most recent - Fallback by date_measured (most recent first) + 4. others """ measurable_fields = AbstractFluorescenceData.get_measurable_fields() new_values: dict[str, object] = {} @@ -152,13 +152,12 @@ def rebuild_attributes(self) -> None: new_source_map[field] = m.id pinned_fields.add(field) - # Sort by priority: trusted > primary_reference > most recent date + # Sort by priority: trusted > primary_reference measurements = sorted( self.measurements.all(), key=lambda m: ( not m.is_trusted, not (primary_ref_id and m.reference_id == primary_ref_id), - -(m.date_measured.toordinal() if m.date_measured else 0), ), ) diff --git a/backend/tests/test_proteins/test_fluorophore.py b/backend/tests/test_proteins/test_fluorophore.py index e79d1ef43..0f5c4d516 100644 --- a/backend/tests/test_proteins/test_fluorophore.py +++ b/backend/tests/test_proteins/test_fluorophore.py @@ -1,7 +1,5 @@ """Tests for Fluorophore.rebuild_attributes() compositing logic.""" -from datetime import date - import pytest from proteins.models import Dye, DyeState, Protein, State @@ -78,10 +76,10 @@ def test_single_measurement_sets_values(self, state: State): def test_multiple_measurements_first_value_wins(self, state: State): """When multiple measurements exist, first non-null value wins.""" # Measurement with partial data (no qy) - m1 = FM(fluorophore=state, ex_max=488, em_max=509, qy=None, date_measured=date(2020, 1, 1)) + m1 = FM(fluorophore=state, ex_max=488, em_max=509, qy=None) m1.save(rebuild_cache=False) # Measurement with different values - m2 = FM(fluorophore=state, ex_max=500, em_max=520, qy=0.5, date_measured=date(2019, 1, 1)) + m2 = FM(fluorophore=state, ex_max=500, em_max=520, qy=0.5) m2.save(rebuild_cache=False) state.rebuild_attributes() @@ -95,9 +93,9 @@ def test_multiple_measurements_first_value_wins(self, state: State): def test_source_map_tracks_measurement_ids(self, state: State): """source_map should track which measurement each field came from.""" - m1 = FM(fluorophore=state, ex_max=488, qy=None, date_measured=date(2020, 1, 1)) + m1 = FM(fluorophore=state, ex_max=488, qy=None) m1.save(rebuild_cache=False) - m2 = FM(fluorophore=state, ex_max=None, qy=0.5, date_measured=date(2019, 1, 1)) + m2 = FM(fluorophore=state, ex_max=None, qy=0.5) m2.save(rebuild_cache=False) state.rebuild_attributes() @@ -113,10 +111,10 @@ class TestRebuildAttributesTrustedPriority: def test_trusted_measurement_overrides_recent(self, state: State): """A trusted measurement should override a more recent one.""" # Recent but not trusted - m1 = FM(fluorophore=state, ex_max=500, em_max=520, is_trusted=False, date_measured=date(2023, 1, 1)) + m1 = FM(fluorophore=state, ex_max=500, em_max=520, is_trusted=False) m1.save(rebuild_cache=False) # Older but trusted - m2 = FM(fluorophore=state, ex_max=488, em_max=509, is_trusted=True, date_measured=date(2010, 1, 1)) + m2 = FM(fluorophore=state, ex_max=488, em_max=509, is_trusted=True) m2.save(rebuild_cache=False) state.rebuild_attributes() @@ -142,7 +140,6 @@ def test_primary_ref_measurement_overrides_recent(self, state_with_ref: State): reference=other_ref, ex_max=500, em_max=520, - date_measured=date(2023, 1, 1), ) m1.save(rebuild_cache=False) # Older but from primary reference @@ -151,7 +148,6 @@ def test_primary_ref_measurement_overrides_recent(self, state_with_ref: State): reference=primary_ref, ex_max=488, em_max=509, - date_measured=date(2010, 1, 1), ) m2.save(rebuild_cache=False) @@ -174,7 +170,6 @@ def test_primary_ref_on_dye(self, dyestate_with_ref: DyeState): reference=other_ref, ex_max=600, em_max=650, - date_measured=date(2023, 1, 1), ) m1.save(rebuild_cache=False) # From primary reference @@ -183,7 +178,6 @@ def test_primary_ref_on_dye(self, dyestate_with_ref: DyeState): reference=primary_ref, ex_max=550, em_max=580, - date_measured=date(2010, 1, 1), ) m2.save(rebuild_cache=False) @@ -209,7 +203,6 @@ def test_trusted_beats_primary_ref(self, state_with_ref: State): ex_max=488, em_max=509, is_trusted=False, - date_measured=date(2020, 1, 1), ) m1.save(rebuild_cache=False) # Trusted but not from primary reference @@ -218,7 +211,6 @@ def test_trusted_beats_primary_ref(self, state_with_ref: State): ex_max=500, em_max=520, is_trusted=True, - date_measured=date(2010, 1, 1), ) m2.save(rebuild_cache=False) @@ -229,20 +221,6 @@ def test_trusted_beats_primary_ref(self, state_with_ref: State): assert state_with_ref.ex_max == 500 assert state_with_ref.em_max == 520 - def test_date_priority_when_equal_trust_and_ref(self, state: State): - """When trust and reference status are equal, more recent date wins.""" - m1 = FM(fluorophore=state, ex_max=488, em_max=509, date_measured=date(2020, 6, 1)) - m1.save(rebuild_cache=False) - m2 = FM(fluorophore=state, ex_max=500, em_max=520, date_measured=date(2023, 6, 1)) - m2.save(rebuild_cache=False) - - state.rebuild_attributes() - state.refresh_from_db() - - # More recent date wins - assert state.ex_max == 500 - assert state.em_max == 520 - class TestRebuildAttributesPinnedFields: """Test that pinned_source_map provides per-field override.""" @@ -259,7 +237,6 @@ def test_pinned_field_overrides_all_priorities(self, state_with_ref: State): em_max=509, qy=0.67, is_trusted=True, - date_measured=date(2020, 1, 1), ) m_trusted.save(rebuild_cache=False) # Older, not trusted, not primary - lowest normal priority @@ -269,7 +246,6 @@ def test_pinned_field_overrides_all_priorities(self, state_with_ref: State): em_max=520, qy=0.50, is_trusted=False, - date_measured=date(2010, 1, 1), ) m_pinned.save(rebuild_cache=False) @@ -291,9 +267,9 @@ def test_pinned_field_overrides_all_priorities(self, state_with_ref: State): def test_pinned_field_with_null_value_is_skipped(self, state: State): """If a pinned measurement has null for the pinned field, skip it.""" - m1 = FM(fluorophore=state, ex_max=488, qy=0.67, date_measured=date(2020, 1, 1)) + m1 = FM(fluorophore=state, ex_max=488, qy=0.67) m1.save(rebuild_cache=False) - m2 = FM(fluorophore=state, ex_max=500, qy=None, date_measured=date(2010, 1, 1)) + m2 = FM(fluorophore=state, ex_max=500, qy=None) m2.save(rebuild_cache=False) # Pin qy to m2, but m2 has null qy @@ -309,7 +285,7 @@ def test_pinned_field_with_null_value_is_skipped(self, state: State): def test_pinned_nonexistent_measurement_is_ignored(self, state: State): """If pinned measurement doesn't exist, ignore and use waterfall.""" - m = FM(fluorophore=state, qy=0.67, date_measured=date(2020, 1, 1)) + m = FM(fluorophore=state, qy=0.67) m.save(rebuild_cache=False) # Pin to a non-existent measurement ID @@ -372,12 +348,12 @@ def test_measurement_save_triggers_rebuild(self, state: State): def test_measurement_delete_triggers_rebuild(self, state: State): """Deleting a measurement should trigger rebuild_attributes.""" - m1 = FM(fluorophore=state, ex_max=488, em_max=509, date_measured=date(2023, 1, 1)) + m1 = FM(fluorophore=state, ex_max=488, em_max=509) m1.save() state.refresh_from_db() assert state.ex_max == 488 - m2 = FM(fluorophore=state, ex_max=500, em_max=520, date_measured=date(2020, 1, 1)) + m2 = FM(fluorophore=state, ex_max=500, em_max=520) m2.save() state.refresh_from_db() # Still 488 because more recent From 02e3d867b2a3a414fc5c4fc0b31e08c0c52b2cb2 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 25 Nov 2025 20:59:57 -0500 Subject: [PATCH 53/57] remove is_trusted --- .../0059_add_fluorophore_and_new_models.py | 6 -- .../models/fluorescence_measurement.py | 3 - backend/proteins/models/fluorophore.py | 12 ++-- .../tests/test_proteins/test_fluorophore.py | 69 ++----------------- 4 files changed, 11 insertions(+), 79 deletions(-) diff --git a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py index 2c8dab9cb..657945b71 100644 --- a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py +++ b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py @@ -105,7 +105,6 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N fluorophore=state, # State is-a Fluorophore (MTI) reference_id=protein.primary_reference_id, **measurables, - is_trusted=True, # Mark as trusted since it's the original data created_by_id=row["created_by_id"], updated_by_id=row["updated_by_id"], ) @@ -192,7 +191,6 @@ def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> dic fluorophore=dyestate, # DyeState is-a Fluorophore (MTI) reference_id=None, **measurables, - is_trusted=True, created_by_id=row["created_by_id"], updated_by_id=row["updated_by_id"], ) @@ -811,10 +809,6 @@ class Migration(migrations.Migration): ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), *abstract_fluorescence_data_fields(), ("conditions", models.TextField(blank=True, help_text="pH, solvent, temp, etc.")), - ( - "is_trusted", - models.BooleanField(default=False, help_text="If True, this measurement overrides others."), - ), ( "fluorophore", models.ForeignKey( diff --git a/backend/proteins/models/fluorescence_measurement.py b/backend/proteins/models/fluorescence_measurement.py index 98913cf55..e9fb4d42a 100644 --- a/backend/proteins/models/fluorescence_measurement.py +++ b/backend/proteins/models/fluorescence_measurement.py @@ -30,9 +30,6 @@ class FluorescenceMeasurement(AbstractFluorescenceData): # Metadata specific to the act of measuring conditions = models.TextField(blank=True, help_text="pH, solvent, temp, etc.") - # Curator Override - is_trusted = models.BooleanField(default=False, help_text="If True, this measurement overrides others.") - def save(self, *args, rebuild_cache: bool = True, **kwargs) -> None: # Allow opt-out of rebuild during bulk operations to avoid N+1 queries super().save(*args, **kwargs) diff --git a/backend/proteins/models/fluorophore.py b/backend/proteins/models/fluorophore.py index 051abc972..7fca814e1 100644 --- a/backend/proteins/models/fluorophore.py +++ b/backend/proteins/models/fluorophore.py @@ -130,9 +130,8 @@ def rebuild_attributes(self) -> None: Priority order (highest to lowest): 1. Pinned overrides - Admin has explicitly pinned a measurement for a field - 2. Trusted measurements - is_trusted=True on FluorescenceMeasurement - 3. Primary reference - Measurement from the owner's primary_reference - 4. others + 2. Primary reference - Measurement from the owner's primary_reference + 3. Others """ measurable_fields = AbstractFluorescenceData.get_measurable_fields() new_values: dict[str, object] = {} @@ -152,13 +151,10 @@ def rebuild_attributes(self) -> None: new_source_map[field] = m.id pinned_fields.add(field) - # Sort by priority: trusted > primary_reference + # Sort by primary_reference measurements = sorted( self.measurements.all(), - key=lambda m: ( - not m.is_trusted, - not (primary_ref_id and m.reference_id == primary_ref_id), - ), + key=lambda m: (not (primary_ref_id and m.reference_id == primary_ref_id),), ) # Waterfall: first non-null value for each non-pinned field diff --git a/backend/tests/test_proteins/test_fluorophore.py b/backend/tests/test_proteins/test_fluorophore.py index 0f5c4d516..5b8a06fb5 100644 --- a/backend/tests/test_proteins/test_fluorophore.py +++ b/backend/tests/test_proteins/test_fluorophore.py @@ -105,26 +105,6 @@ def test_source_map_tracks_measurement_ids(self, state: State): assert state.source_map["qy"] == m2.id -class TestRebuildAttributesTrustedPriority: - """Test that is_trusted=True measurements take highest priority.""" - - def test_trusted_measurement_overrides_recent(self, state: State): - """A trusted measurement should override a more recent one.""" - # Recent but not trusted - m1 = FM(fluorophore=state, ex_max=500, em_max=520, is_trusted=False) - m1.save(rebuild_cache=False) - # Older but trusted - m2 = FM(fluorophore=state, ex_max=488, em_max=509, is_trusted=True) - m2.save(rebuild_cache=False) - - state.rebuild_attributes() - state.refresh_from_db() - - # Trusted measurement wins - assert state.ex_max == 488 - assert state.em_max == 509 - - class TestRebuildAttributesPrimaryReferencePriority: """Test that primary reference measurements take priority over non-primary.""" @@ -189,63 +169,28 @@ def test_primary_ref_on_dye(self, dyestate_with_ref: DyeState): assert dyestate_with_ref.em_max == 580 -class TestRebuildAttributesPriorityOrder: - """Test the complete priority order: trusted > primary_ref > date.""" - - def test_trusted_beats_primary_ref(self, state_with_ref: State): - """is_trusted should override even primary reference.""" - primary_ref = state_with_ref.protein.primary_reference - - # From primary reference but not trusted - m1 = FM( - fluorophore=state_with_ref, - reference=primary_ref, - ex_max=488, - em_max=509, - is_trusted=False, - ) - m1.save(rebuild_cache=False) - # Trusted but not from primary reference - m2 = FM( - fluorophore=state_with_ref, - ex_max=500, - em_max=520, - is_trusted=True, - ) - m2.save(rebuild_cache=False) - - state_with_ref.rebuild_attributes() - state_with_ref.refresh_from_db() - - # Trusted measurement wins over primary reference - assert state_with_ref.ex_max == 500 - assert state_with_ref.em_max == 520 - - class TestRebuildAttributesPinnedFields: """Test that pinned_source_map provides per-field override.""" def test_pinned_field_overrides_all_priorities(self, state_with_ref: State): - """A pinned field should override even trusted measurements.""" + """A pinned field should override other measurements.""" primary_ref = state_with_ref.protein.primary_reference - # Trusted measurement from primary reference - highest normal priority - m_trusted = FM( + # measurement from primary reference - highest normal priority + m_primary = FM( fluorophore=state_with_ref, reference=primary_ref, ex_max=488, em_max=509, qy=0.67, - is_trusted=True, ) - m_trusted.save(rebuild_cache=False) - # Older, not trusted, not primary - lowest normal priority + m_primary.save(rebuild_cache=False) + # Older, not primary - lowest normal priority m_pinned = FM( fluorophore=state_with_ref, ex_max=500, em_max=520, qy=0.50, - is_trusted=False, ) m_pinned.save(rebuild_cache=False) @@ -256,14 +201,14 @@ def test_pinned_field_overrides_all_priorities(self, state_with_ref: State): state_with_ref.rebuild_attributes() state_with_ref.refresh_from_db() - # ex_max and em_max should come from trusted measurement + # ex_max and em_max should come from primary measurement assert state_with_ref.ex_max == 488 assert state_with_ref.em_max == 509 # qy should come from the pinned measurement assert state_with_ref.qy == 0.50 # source_map should reflect the pinned override assert state_with_ref.source_map["qy"] == m_pinned.id - assert state_with_ref.source_map["ex_max"] == m_trusted.id + assert state_with_ref.source_map["ex_max"] == m_primary.id def test_pinned_field_with_null_value_is_skipped(self, state: State): """If a pinned measurement has null for the pinned field, skip it.""" From 01eeed4b5716b0a996e021c6e71d6fa79c39c7f1 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Wed, 26 Nov 2025 10:27:06 -0500 Subject: [PATCH 54/57] rename Fluorophore to FluorState --- .github/workflows/ci.yml | 2 +- backend/proteins/admin.py | 4 +- backend/proteins/api/serializers.py | 6 +- backend/proteins/factories.py | 4 +- backend/proteins/forms/spectrum.py | 6 +- .../0059_add_fluorophore_and_new_models.py | 130 +++++++++--------- backend/proteins/models/__init__.py | 4 +- backend/proteins/models/dye.py | 8 +- backend/proteins/models/efficiency.py | 8 +- .../models/fluorescence_measurement.py | 12 +- backend/proteins/models/fluorophore.py | 14 +- backend/proteins/models/protein.py | 8 +- backend/proteins/models/spectrum.py | 10 +- backend/proteins/views/ajax.py | 14 +- backend/proteins/views/fret.py | 4 +- backend/proteins/views/microscope.py | 6 +- .../tests/test_proteins/test_fluorophore.py | 40 +++--- 17 files changed, 140 insertions(+), 140 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3885cc51c..890a0c5ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,7 @@ jobs: with: enable-cache: true - name: Check for new migrations - run: uv run backend/manage.py makemigrations --check + run: uv run backend/manage.py makemigrations --check --noinput test: runs-on: ubuntu-latest diff --git a/backend/proteins/admin.py b/backend/proteins/admin.py index e0e31bc55..dd594d657 100644 --- a/backend/proteins/admin.py +++ b/backend/proteins/admin.py @@ -19,7 +19,7 @@ Excerpt, Filter, FilterPlacement, - Fluorophore, + FluorState, Light, Lineage, Microscope, @@ -57,7 +57,7 @@ def _makelink(sp): return f'{sp.get_subtype_display()}{pending}' 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)) diff --git a/backend/proteins/api/serializers.py b/backend/proteins/api/serializers.py index 1297b7f93..ef8a0eafb 100644 --- a/backend/proteins/api/serializers.py +++ b/backend/proteins/api/serializers.py @@ -1,7 +1,7 @@ from rest_framework import serializers from proteins.api._tweaks import ModelSerializer -from proteins.models import Fluorophore, Protein, Spectrum, State, StateTransition +from proteins.models import FluorState, Protein, Spectrum, State, StateTransition class SpectrumSerializer(serializers.ModelSerializer): @@ -30,13 +30,13 @@ class Meta: def get_protein_name(self, obj): # Check if owner_fluor is a State (has protein attribute) - if obj.owner_fluor and obj.owner_fluor.entity_type == Fluorophore.EntityTypes.PROTEIN: + if obj.owner_fluor and obj.owner_fluor.entity_type == FluorState.EntityTypes.PROTEIN: return obj.owner_fluor.protein.name return None def get_protein_slug(self, obj): # Check if owner_fluor is a State (has protein attribute) - if obj.owner_fluor and obj.owner_fluor.entity_type == Fluorophore.EntityTypes.PROTEIN: + if obj.owner_fluor and obj.owner_fluor.entity_type == FluorState.EntityTypes.PROTEIN: return obj.owner_fluor.protein.slug return None diff --git a/backend/proteins/factories.py b/backend/proteins/factories.py index 155dbad9e..ce0818f63 100644 --- a/backend/proteins/factories.py +++ b/backend/proteins/factories.py @@ -26,7 +26,7 @@ from references.factories import ReferenceFactory if TYPE_CHECKING: - from proteins.models import Fluorophore + from proteins.models import FluorState T = TypeVar("T") @@ -135,7 +135,7 @@ def _build_spectral_data(resolver: factory.builder.Resolver): subtype = getattr(resolver, "subtype", None) if (owner_fluor := getattr(resolver, "owner_fluor", None)) is not None: - owner_fluor = cast("Fluorophore", owner_fluor) + owner_fluor = cast("FluorState", owner_fluor) if subtype == "ex": return _mock_spectrum(owner_fluor.ex_max, type="ex") elif subtype == "em": diff --git a/backend/proteins/forms/spectrum.py b/backend/proteins/forms/spectrum.py index b311fcd33..d74f9fa7e 100644 --- a/backend/proteins/forms/spectrum.py +++ b/backend/proteins/forms/spectrum.py @@ -8,7 +8,7 @@ from django.utils.safestring import mark_safe from django.utils.text import slugify -from proteins.models import Dye, DyeState, Fluorophore, Spectrum, State +from proteins.models import Dye, DyeState, FluorState, Spectrum, State from proteins.util.helpers import zip_wave_data from proteins.util.importers import text_to_spectra from proteins.validators import validate_spectrum @@ -145,7 +145,7 @@ def save(self, commit=True): # Get or create the default DyeState for this dye dye_state, _ = DyeState.objects.get_or_create( dye=dye, - name=Fluorophore.DEFAULT_NAME, + name=FluorState.DEFAULT_NAME, defaults={"created_by": self.user}, ) self.instance.owner_fluor = dye_state @@ -226,7 +226,7 @@ def clean_owner(self): dye = Dye.objects.get(slug=slugify(owner)) except Dye.DoesNotExist: return owner - dye_state = dye.states.filter(name=Fluorophore.DEFAULT_NAME).first() + dye_state = dye.states.filter(name=FluorState.DEFAULT_NAME).first() if dye_state and dye_state.spectra.filter(subtype=stype).exists(): stype_display = dye_state.spectra.filter(subtype=stype).first().get_subtype_display() self.add_error( diff --git a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py index 657945b71..679c168e4 100644 --- a/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py +++ b/backend/proteins/migrations/0059_add_fluorophore_and_new_models.py @@ -1,12 +1,12 @@ -"""Major data migration to new Fluorophore + Measurement schema. +"""Major data migration to new FluorState + Measurement schema. NON REVERSIBLE. -1. proteins.State becomes MTI child of new proteins.Fluorophore -2. proteins.Dye becomes container for new proteins.DyeState (MTI child of Fluorophore) +1. proteins.State becomes MTI child of new proteins.FluorState +2. proteins.Dye becomes container for new proteins.DyeState (MTI child of FluorState) 3. proteins.FluorescenceMeasurement created for each old State and DyeState -4. proteins.Spectrum ownership updated to point to new Fluorophore records -5. proteins.OcFluorEff updated to point to new Fluorophore records +4. proteins.Spectrum ownership updated to point to new FluorState records +5. proteins.OcFluorEff updated to point to new FluorState records """ @@ -57,7 +57,7 @@ def _dictfetchall(cursor: CursorWrapper) -> list[dict[str, Any]]: def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: - """Migrate State data from old schema to new Fluorophore + State MTI structure.""" + """Migrate State data from old schema to new FluorState + State MTI structure.""" # Get models from migration state State = apps.get_model("proteins", "State") Protein = apps.get_model("proteins", "Protein") @@ -82,7 +82,7 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N # Get protein for label protein = Protein.objects.get(id=row["protein_id"]) - # Create State (MTI child of Fluorophore) + # Create State (MTI child of FluorState) state = State.objects.create( id=old_id, # Preserve old ID for easier FK updates later created=row["created"], @@ -102,7 +102,7 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N FluorescenceMeasurement.objects.create( id=old_id, # Preserve old ID - fluorophore=state, # State is-a Fluorophore (MTI) + state=state, # State is-a FluorState (MTI) reference_id=protein.primary_reference_id, **measurables, created_by_id=row["created_by_id"], @@ -111,16 +111,16 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N print(f"Migrated {State.objects.count()} State records") - # Reset the Fluorophore and FluorescenceMeasurement ID sequences - # to avoid conflicts when creating Dye fluorophores and their measurements + # Reset the FluoroState and FluorescenceMeasurement ID sequences + # to avoid conflicts when creating DyeStates and their measurements with schema_editor.connection.cursor() as cursor: cursor.execute(""" SELECT setval( - pg_get_serial_sequence('proteins_fluorophore', 'id'), - COALESCE((SELECT MAX(id) FROM proteins_fluorophore), 1) + pg_get_serial_sequence('proteins_fluorstate', 'id'), + COALESCE((SELECT MAX(id) FROM proteins_fluorstate), 1) ) """) - print(f"Reset Fluorophore ID sequence to {cursor.fetchone()[0]}") + print(f"Reset FluoroState ID sequence to {cursor.fetchone()[0]}") cursor.execute(""" SELECT setval( @@ -134,7 +134,7 @@ def migrate_state_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> N def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> dict[int, int]: """Migrate Dye data from old schema to new Dye container + DyeState structure. - Returns a mapping of old Dye ID → new Fluorophore ID for efficient FK updates. + Returns a mapping of old Dye ID → new FluoroState ID for efficient FK updates. """ Dye = apps.get_model("proteins", "Dye") DyeState = apps.get_model("proteins", "DyeState") @@ -170,7 +170,7 @@ def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> dic url=row["url"], ) - # Create DyeState (MTI child of Fluorophore) + # Create DyeState (MTI child of FluoroState) dyestate = DyeState.objects.create( created=row["created"], modified=row["modified"], @@ -188,7 +188,7 @@ def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> dic # Create FluorescenceMeasurement from old Dye data # For dyes, we don't have a primary_reference concept in old schema FluorescenceMeasurement.objects.create( - fluorophore=dyestate, # DyeState is-a Fluorophore (MTI) + state=dyestate, # DyeState is-a FluorState (MTI) reference_id=None, **measurables, created_by_id=row["created_by_id"], @@ -199,7 +199,7 @@ def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> dic dye.save() # Store mapping for efficient FK updates - old_to_new_id_map[old_id] = dyestate.fluorophore_ptr_id + old_to_new_id_map[old_id] = dyestate.fluorstate_ptr_id print(f"Migrated {Dye.objects.count()} Dye records to Dye containers") print(f"Created {DyeState.objects.count()} DyeState records") @@ -207,9 +207,9 @@ def migrate_dye_data(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> dic def update_spectrum_ownership(apps: Apps, schema_editor: BaseDatabaseSchemaEditor, dye_id_map: dict[int, int]) -> None: - """Update Spectrum foreign keys to point to new Fluorophore records.""" + """Update Spectrum foreign keys to point to new FluoroState records.""" with schema_editor.connection.cursor() as cursor: - # Update spectra owned by States - State ID == Fluorophore ID (preserved) + # Update spectra owned by States - State ID == FluoroState ID (preserved) cursor.execute(""" UPDATE proteins_spectrum SET owner_fluor_id = owner_state_id @@ -218,7 +218,7 @@ def update_spectrum_ownership(apps: Apps, schema_editor: BaseDatabaseSchemaEdito state_count = cursor.rowcount print(f"Updated {state_count} spectra owned by States") - # Update spectra owned by Dyes - need mapping from old Dye ID to new Fluorophore ID + # Update spectra owned by Dyes - need mapping from old Dye ID to new FluoroState ID # Use temp table for the mapping (reuse pattern from OcFluorEff) cursor.execute(""" CREATE TEMP TABLE dye_spectrum_mapping ( @@ -246,10 +246,10 @@ def update_spectrum_ownership(apps: Apps, schema_editor: BaseDatabaseSchemaEdito def update_ocfluoreff(apps: Apps, schema_editor: BaseDatabaseSchemaEditor, dye_id_map: dict[int, int]) -> None: - """Update OcFluorEff to use direct FK to Fluorophore. + """Update OcFluorEff to use direct FK to FluoroState. Uses efficient bulk updates: - - States: Direct assignment (State ID == Fluorophore ID) + - States: Direct assignment (State ID == FluoroState ID) - Dyes: Temp table JOIN (using provided old_id → new_id mapping) """ with schema_editor.connection.cursor() as cursor: @@ -352,7 +352,7 @@ def update_ocfluoreff(apps: Apps, schema_editor: BaseDatabaseSchemaEditor, dye_i def populate_emhex_exhex(apps, _schema_editor): - """Populate emhex/exhex for all Fluorophore objects. + """Populate emhex/exhex for all FluorState objects. The historical models don't include the save() logic from AbstractFluorescenceData, so we need to manually calculate these fields after creating the objects. @@ -406,16 +406,16 @@ def wave_to_hex(wavelength, gamma=1): b *= 255 return f"#{int(r):02x}{int(g):02x}{int(b):02x}" - Fluorophore = apps.get_model("proteins", "Fluorophore") + FluorState = apps.get_model("proteins", "FluorState") - fluorophores_to_update = [] - for fluor in Fluorophore.objects.all(): + fluorstates_to_update = [] + for fluor in FluorState.objects.all(): fluor.emhex = "#000" if fluor.is_dark else wave_to_hex(fluor.em_max) fluor.exhex = wave_to_hex(fluor.ex_max) - fluorophores_to_update.append(fluor) + fluorstates_to_update.append(fluor) - Fluorophore.objects.bulk_update(fluorophores_to_update, ["emhex", "exhex"], batch_size=500) - print(f"Populated emhex/exhex for {len(fluorophores_to_update)} fluorophores") + FluorState.objects.bulk_update(fluorstates_to_update, ["emhex", "exhex"], batch_size=500) + print(f"Populated emhex/exhex for {len(fluorstates_to_update)} FluorState records") def migrate_reversion_state_versions(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: @@ -423,22 +423,22 @@ def migrate_reversion_state_versions(apps: Apps, schema_editor: BaseDatabaseSche Background: ----------- - State now inherits from Fluorophore via Multi-Table Inheritance (MTI). + State now inherits from FluorState via Multi-Table Inheritance (MTI). When reversion reverts a revision, it needs Version records for BOTH the parent - (Fluorophore) and child (State) models in the same revision. + (FluorState) and child (State) models in the same revision. Old State versions stored ALL fields in one record: {"model": "proteins.state", "fields": {"name": "x", "ex_max": 488, "protein": 1, ...}} After migration, we need TWO records per revision: - 1. Fluorophore: {"fields": {"name": "x", "ex_max": 488, ...}} - 2. State: {"fields": {"fluorophore_ptr": 402, "protein": 1, "maturation": 13.0}} + 1. FluorState: {"fields": {"name": "x", "ex_max": 488, ...}} + 2. State: {"fields": {"fluorstate_ptr": 402, "protein": 1, "maturation": 13.0}} Both records share the same revision_id, so revision.revert() restores them together. """ ContentType = apps.get_model("contenttypes", "ContentType") - # Fields that remain on State model; everything else moves to Fluorophore parent + # Fields that remain on State model; everything else moves to FluorState parent state_only_fields = {"protein", "maturation", "transitions"} with schema_editor.connection.cursor() as cursor: @@ -447,7 +447,7 @@ def migrate_reversion_state_versions(apps: Apps, schema_editor: BaseDatabaseSche except ContentType.DoesNotExist: # No content types exist yet (fresh database), nothing to migrate return - fluor_ct, _ = ContentType.objects.get_or_create(app_label="proteins", model="fluorophore") + fluor_ct, _ = ContentType.objects.get_or_create(app_label="proteins", model="fluorstate") # Build lookup dict for protein name/slug (used for owner_name/owner_slug) cursor.execute("SELECT id, name, slug FROM proteins_protein") @@ -483,10 +483,10 @@ def migrate_reversion_state_versions(apps: Apps, schema_editor: BaseDatabaseSche protein_id = old_fields.get("protein") owner_name, owner_slug = proteins.get(protein_id, ("", "")) - # Split fields: State-only fields stay on State, rest go to Fluorophore - # State needs fluorophore_ptr to link to its MTI parent - state_fields = {"fluorophore_ptr": pk} - # Fluorophore needs new required fields with sensible defaults + # Split fields: State-only fields stay on State, rest go to FluorState + # State needs fluorstate_ptr to link to its MTI parent + state_fields = {"fluorstate_ptr": pk} + # FluorState needs new required fields with sensible defaults fluor_fields = { "entity_type": "p", "owner_name": owner_name, @@ -497,19 +497,19 @@ def migrate_reversion_state_versions(apps: Apps, schema_editor: BaseDatabaseSche for k, v in old_fields.items(): (state_fields if k in state_only_fields else fluor_fields)[k] = v - # Queue NEW Fluorophore version (same revision_id links them together) + # Queue NEW FluorState version (same revision_id links them together) fluor_inserts.append( ( str(object_id), revision_id, # Same revision as the State version fluor_ct.id, - json.dumps([{"model": "proteins.fluorophore", "pk": pk, "fields": fluor_fields}]), + json.dumps([{"model": "proteins.fluorstate", "pk": pk, "fields": fluor_fields}]), "", # object_repr - not critical for historical versions db, fmt, ) ) - # Queue UPDATE to existing State version (strip out fields that moved to Fluorophore) + # Queue UPDATE to existing State version (strip out fields that moved to FluorState) state_updates.append( ( json.dumps([{"model": "proteins.state", "pk": pk, "fields": state_fields}]), @@ -520,7 +520,7 @@ def migrate_reversion_state_versions(apps: Apps, schema_editor: BaseDatabaseSche logger.warning(f"Skipping malformed reversion State version ID {row['id']}") continue - # Bulk insert new Fluorophore versions + # Bulk insert new FluorState versions if fluor_inserts: cursor.executemany( "INSERT INTO reversion_version " @@ -553,11 +553,11 @@ def migrate_reverse(_apps, _schema_editor): """Reverse migration - not supported. This migration performs a one-way data transformation from the old schema - (separate State and Dye models) to the new schema (MTI-based Fluorophore hierarchy). + (separate State and Dye models) to the new schema (MTI-based FluorState hierarchy). Reversing this migration would require: - 1. Decomposing Fluorophore + State back into old State structure - 2. Decomposing Fluorophore + DyeState back into old Dye structure + 1. Decomposing FluorState + State back into old State structure + 2. Decomposing FluorState + DyeState back into old Dye structure 3. Merging FluorescenceMeasurement data back into parent entities 4. Restoring old spectrum ownership relationships @@ -759,11 +759,11 @@ class Migration(migrations.Migration): ], ), # Step 2: Create all new models - # Fluorophore is the new "State" base model for MTI - # State and DyeState are MTI children of Fluorophore + # FluorState is the new "State" base model for MTI + # State and DyeState are MTI children of FluorState # What was Dye is now is a container model for DyeStates (like Protein for States) migrations.CreateModel( - name="Fluorophore", + name="FluorState", fields=[ ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), *timestamped_mixin_fields(), @@ -796,10 +796,10 @@ class Migration(migrations.Migration): ], options={ "indexes": [ - models.Index(fields=["ex_max"], name="fluorophore_ex_max_idx"), - models.Index(fields=["em_max"], name="fluorophore_em_max_idx"), - models.Index(fields=["owner_name"], name="fluorophore_owner_name_idx"), - models.Index(fields=["entity_type", "is_dark"], name="fluorophore_type_dark_idx"), + models.Index(fields=["ex_max"], name="fluorstate_ex_max_idx"), + models.Index(fields=["em_max"], name="fluorstate_em_max_idx"), + models.Index(fields=["owner_name"], name="fluorstate_owner_name_idx"), + models.Index(fields=["entity_type", "is_dark"], name="fluorstate_type_dark_idx"), ], }, ), @@ -810,11 +810,11 @@ class Migration(migrations.Migration): *abstract_fluorescence_data_fields(), ("conditions", models.TextField(blank=True, help_text="pH, solvent, temp, etc.")), ( - "fluorophore", + "state", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="measurements", - to="proteins.fluorophore", + to="proteins.fluorstate", ), ), ( @@ -857,14 +857,14 @@ class Migration(migrations.Migration): name="DyeState", fields=[ ( - "fluorophore_ptr", + "fluorstate_ptr", models.OneToOneField( auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, - to="proteins.fluorophore", + to="proteins.fluorstate", ), ), ( @@ -877,7 +877,7 @@ class Migration(migrations.Migration): options={ "abstract": False, }, - bases=("proteins.fluorophore",), + bases=("proteins.fluorstate",), ), migrations.AddField( model_name="dye", @@ -890,19 +890,19 @@ class Migration(migrations.Migration): to="proteins.DyeState", ), ), - # Re-create State model as MTI child of Fluorophore + # Re-create State model as MTI child of FluorState migrations.CreateModel( name="State", fields=[ ( - "fluorophore_ptr", + "fluorstate_ptr", models.OneToOneField( auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, - to="proteins.fluorophore", + to="proteins.fluorstate", ), ), ( @@ -937,7 +937,7 @@ class Migration(migrations.Migration): options={ "abstract": False, }, - bases=("proteins.fluorophore",), + bases=("proteins.fluorstate",), ), # Add owner_fluor to Spectrum (nullable for now) migrations.AddField( @@ -948,7 +948,7 @@ class Migration(migrations.Migration): null=True, on_delete=django.db.models.deletion.CASCADE, related_name="spectra", - to="proteins.fluorophore", + to="proteins.fluorstate", ), ), # Add fluor FK to OcFluorEff (nullable for now) @@ -960,7 +960,7 @@ class Migration(migrations.Migration): null=True, on_delete=django.db.models.deletion.CASCADE, related_name="oc_effs", - to="proteins.fluorophore", + to="proteins.fluorstate", ), ), # Add index for spectrum owner_fluor lookups @@ -1022,7 +1022,7 @@ class Migration(migrations.Migration): field=models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="oc_effs", - to="proteins.fluorophore", + to="proteins.fluorstate", ), ), ] diff --git a/backend/proteins/models/__init__.py b/backend/proteins/models/__init__.py index fe93e8db3..f1fde659a 100644 --- a/backend/proteins/models/__init__.py +++ b/backend/proteins/models/__init__.py @@ -4,7 +4,7 @@ from .efficiency import OcFluorEff from .excerpt import Excerpt from .fluorescence_measurement import FluorescenceMeasurement -from .fluorophore import Fluorophore +from .fluorophore import FluorState from .lineage import Lineage from .microscope import FilterPlacement, Microscope, OpticalConfig from .organism import Organism @@ -22,8 +22,8 @@ "Excerpt", "Filter", "FilterPlacement", + "FluorState", "FluorescenceMeasurement", - "Fluorophore", "Light", "Lineage", "Microscope", diff --git a/backend/proteins/models/dye.py b/backend/proteins/models/dye.py index 9bed4d66c..24537b3cc 100644 --- a/backend/proteins/models/dye.py +++ b/backend/proteins/models/dye.py @@ -4,7 +4,7 @@ from django.utils.text import slugify from model_utils.models import TimeStampedModel -from proteins.models.fluorophore import Fluorophore +from proteins.models.fluorophore import FluorState from proteins.models.mixins import Authorable, Product if TYPE_CHECKING: @@ -64,7 +64,7 @@ def get_primary_spectrum(self): # Instead of storing spectral data directly in the SmallMolecule, we link it here. # This allows us to store "Alexa 488 in PBS" and "Alexa 488 in Ethanol" as valid, # separate datasets. -class DyeState(Fluorophore): +class DyeState(FluorState): """Represents a SmallMolecule in a specific environmental context. This holds the actual spectral data. @@ -74,10 +74,10 @@ class DyeState(Fluorophore): dye: models.ForeignKey["Dye"] = models.ForeignKey(Dye, on_delete=models.CASCADE, related_name="states") if TYPE_CHECKING: - fluorophore_ptr: Fluorophore # added by Django MTI + fluorophore_ptr: FluorState # added by Django MTI def save(self, *args, **kwargs): - self.entity_type = Fluorophore.EntityTypes.DYE + self.entity_type = FluorState.EntityTypes.DYE # Cache parent dye info for efficient searching if self.dye_id: self.owner_name = self.dye.name diff --git a/backend/proteins/models/efficiency.py b/backend/proteins/models/efficiency.py index d4a3e7701..be311ac8f 100644 --- a/backend/proteins/models/efficiency.py +++ b/backend/proteins/models/efficiency.py @@ -5,7 +5,7 @@ from django.db.models import F, Max, OuterRef, Q, Subquery from model_utils.models import TimeStampedModel -from proteins.models.fluorophore import Fluorophore +from proteins.models.fluorophore import FluorState from proteins.util.efficiency import oc_efficiency_report if TYPE_CHECKING: @@ -14,7 +14,7 @@ class OcFluorEffQuerySet(models.QuerySet): def outdated(self): - fluor_objs = Fluorophore.objects.filter(id=OuterRef("fluor_id")) + fluor_objs = FluorState.objects.filter(id=OuterRef("fluor_id")) spectra_mod = fluor_objs.annotate(latest_spec=Max("spectra__modified")).values("latest_spec")[:1] fluor_mod = fluor_objs.values("modified")[:1] @@ -30,8 +30,8 @@ class OcFluorEff(TimeStampedModel): oc_id: int oc: models.ForeignKey["OpticalConfig"] = models.ForeignKey("OpticalConfig", on_delete=models.CASCADE) fluor_id: int - fluor: models.ForeignKey[Fluorophore] = models.ForeignKey( - Fluorophore, on_delete=models.CASCADE, related_name="oc_effs" + fluor: models.ForeignKey[FluorState] = models.ForeignKey( + FluorState, on_delete=models.CASCADE, related_name="oc_effs" ) fluor_name = models.CharField(max_length=100, blank=True) ex_eff = models.FloatField( diff --git a/backend/proteins/models/fluorescence_measurement.py b/backend/proteins/models/fluorescence_measurement.py index e9fb4d42a..457827cba 100644 --- a/backend/proteins/models/fluorescence_measurement.py +++ b/backend/proteins/models/fluorescence_measurement.py @@ -7,7 +7,7 @@ from proteins.models.fluorescence_data import AbstractFluorescenceData if TYPE_CHECKING: - from proteins.models import Fluorophore + from proteins.models import FluorState from references.models import Reference @@ -15,9 +15,9 @@ class FluorescenceMeasurement(AbstractFluorescenceData): """Raw data points from a specific reference.""" - fluorophore_id: int - fluorophore: models.ForeignKey[Fluorophore] = models.ForeignKey( - "Fluorophore", related_name="measurements", on_delete=models.CASCADE + state_id: int + state: models.ForeignKey[FluorState] = models.ForeignKey( + "FluorState", related_name="measurements", on_delete=models.CASCADE ) reference_id: int | None reference: models.ForeignKey[Reference | None] = models.ForeignKey( @@ -35,10 +35,10 @@ def save(self, *args, rebuild_cache: bool = True, **kwargs) -> None: super().save(*args, **kwargs) # Keep the parent cache in sync unless explicitly disabled if rebuild_cache: - self.fluorophore.rebuild_attributes() + self.state.rebuild_attributes() def delete(self, *args, **kwargs) -> None: - f = self.fluorophore + f = self.state super().delete(*args, **kwargs) # Always keep the parent cache in sync f.rebuild_attributes() diff --git a/backend/proteins/models/fluorophore.py b/backend/proteins/models/fluorophore.py index 7fca814e1..03eca750f 100644 --- a/backend/proteins/models/fluorophore.py +++ b/backend/proteins/models/fluorophore.py @@ -17,7 +17,7 @@ from proteins.models.spectrum import D3Dict, Spectrum -class FluorophoreManager[T: models.Model](models.Manager): +class FluorStateManager[T: models.Model](models.Manager): _queryset_class: type[QuerySet[T]] def notdark(self): @@ -28,7 +28,7 @@ def with_spectra(self): # The Canonical Parent (The Summary) -class Fluorophore(AbstractFluorescenceData): +class FluorState(AbstractFluorescenceData): """The database table for 'Things That Glow'. Polymorphic Fluorophore Parent. @@ -79,7 +79,7 @@ class EntityTypes(models.TextChoices): pinned_source_map = models.JSONField(default=dict, blank=True) # Managers - objects: FluorophoreManager[Self] = FluorophoreManager() + objects: FluorStateManager[Self] = FluorStateManager() if TYPE_CHECKING: spectra: RelatedManager[Spectrum] @@ -92,10 +92,10 @@ class EntityTypes(models.TextChoices): class Meta: indexes = [ - models.Index(fields=["ex_max"], name="fluorophore_ex_max_idx"), - models.Index(fields=["em_max"], name="fluorophore_em_max_idx"), - models.Index(fields=["owner_name"], name="fluorophore_owner_name_idx"), - models.Index(fields=["entity_type", "is_dark"], name="fluorophore_type_dark_idx"), + models.Index(fields=["ex_max"], name="fluorstate_ex_max_idx"), + models.Index(fields=["em_max"], name="fluorstate_em_max_idx"), + models.Index(fields=["owner_name"], name="fluorstate_owner_name_idx"), + models.Index(fields=["entity_type", "is_dark"], name="fluorstate_type_dark_idx"), ] def __str__(self): diff --git a/backend/proteins/models/protein.py b/backend/proteins/models/protein.py index 76dfbde74..e4933f25b 100644 --- a/backend/proteins/models/protein.py +++ b/backend/proteins/models/protein.py @@ -27,7 +27,7 @@ from proteins import util from proteins.models._sequence_field import SequenceField from proteins.models.collection import ProteinCollection -from proteins.models.fluorophore import Fluorophore +from proteins.models.fluorophore import FluorState from proteins.models.mixins import Authorable from proteins.models.spectrum import Spectrum from proteins.util.helpers import get_base_name, get_color_group, mless, spectra_fig @@ -555,7 +555,7 @@ def first_author(self): return self.primary_reference.first_author.family -class State(Fluorophore): # TODO: rename to ProteinState +class State(FluorState): # TODO: rename to ProteinState protein_id: int protein: models.ForeignKey[Protein] = models.ForeignKey( Protein, @@ -582,10 +582,10 @@ class State(Fluorophore): # TODO: rename to ProteinState transitions_from: models.QuerySet[StateTransition] transitions_to: models.QuerySet[StateTransition] bleach_measurements: models.QuerySet[BleachMeasurement] - fluorophore_ptr: Fluorophore # added by Django MTI + fluorophore_ptr: FluorState # added by Django MTI def save(self, *args, **kwargs) -> None: - self.entity_type = Fluorophore.EntityTypes.PROTEIN + self.entity_type = FluorState.EntityTypes.PROTEIN # Cache parent protein info for efficient searching if self.protein_id: self.owner_name = self.protein.name diff --git a/backend/proteins/models/spectrum.py b/backend/proteins/models/spectrum.py index 5a144ca61..6d3a1c826 100644 --- a/backend/proteins/models/spectrum.py +++ b/backend/proteins/models/spectrum.py @@ -30,7 +30,7 @@ if TYPE_CHECKING: from typing import NotRequired, TypedDict - from proteins.models import Fluorophore + from proteins.models import FluorState class D3Dict(TypedDict): slug: str @@ -370,8 +370,8 @@ class Spectrum(Authorable, StatusModel, TimeStampedModel, AdminURLMixin): # https://lukeplant.me.uk/blog/posts/avoid-django-genericforeignkey/ # Fluorophore encompasses both State (ProteinState) and DyeState via MTI owner_fluor_id: int | None - owner_fluor: models.ForeignKey[Fluorophore | None] = models.ForeignKey( - "Fluorophore", + owner_fluor: models.ForeignKey[FluorState | None] = models.ForeignKey( + "FluorState", null=True, blank=True, on_delete=models.CASCADE, @@ -493,7 +493,7 @@ def clean(self): raise ValidationError(errors) @property - def owner_set(self) -> list[Fluorophore | Filter | Light | Camera | None]: + def owner_set(self) -> list[FluorState | Filter | Light | Camera | None]: return [ self.owner_fluor, self.owner_filter, @@ -502,7 +502,7 @@ def owner_set(self) -> list[Fluorophore | Filter | Light | Camera | None]: ] @property - def owner(self) -> Fluorophore | Filter | Light | Camera | None: + def owner(self) -> FluorState | Filter | Light | Camera | None: return next((x for x in self.owner_set if x), None) # raise AssertionError("No owner is set") diff --git a/backend/proteins/views/ajax.py b/backend/proteins/views/ajax.py index 851de6956..0fdc7776b 100644 --- a/backend/proteins/views/ajax.py +++ b/backend/proteins/views/ajax.py @@ -16,7 +16,7 @@ from fpbase.util import uncache_protein_page from proteins.util.maintain import validate_node -from ..models import Fluorophore, Lineage, Organism, Protein, Spectrum, State +from ..models import FluorState, Lineage, Organism, Protein, Spectrum, State from ..models.spectrum import Camera, Filter, Light logger = logging.getLogger(__name__) @@ -124,7 +124,7 @@ def similar_spectrum_owners(request): camera_ids = [] for s in similars: - if isinstance(s, Fluorophore): + if isinstance(s, FluorState): fluor_ids.append(s.id) elif isinstance(s, Filter): filter_ids.append(s.id) @@ -145,17 +145,17 @@ def similar_spectrum_owners(request): cameras = Camera.objects.filter(id__in=camera_ids).select_related("spectrum") # Combine all objects maintaining order - # For Fluorophores (State/DyeState), use ID-only as key since original `similars` has base "Fluorophore" class + # For Fluorophores (State/DyeState), use ID-only as key since original `similars` has base "FluorState" class similars_dict = {} for item in [*states, *dye_states, *filters, *lights, *cameras]: - if isinstance(item, Fluorophore): + if isinstance(item, FluorState): similars_dict[item.id] = item # Fluorophores: use ID only else: similars_dict[(item.__class__.__name__, item.id)] = item # Others: use (class, ID) similars_optimized = [] for s in similars: - if isinstance(s, Fluorophore): + if isinstance(s, FluorState): key = s.id # For Fluorophores, look up by ID only else: key = (s.__class__.__name__, s.id) @@ -165,11 +165,11 @@ def similar_spectrum_owners(request): "similars": [ { "slug": s.slug, - "name": s.label if isinstance(s, Fluorophore) else s.name, + "name": s.label if isinstance(s, FluorState) else s.name, "url": s.get_absolute_url(), "spectra": ( [sp.get_subtype_display() for sp in s.spectra.all()] - if isinstance(s, Fluorophore) + if isinstance(s, FluorState) else [s.spectrum.get_subtype_display()] ), } diff --git a/backend/proteins/views/fret.py b/backend/proteins/views/fret.py index dafb2d5fc..0f9393d6a 100644 --- a/backend/proteins/views/fret.py +++ b/backend/proteins/views/fret.py @@ -7,7 +7,7 @@ from fpbase.celery import app from fpbase.util import is_ajax -from ..models import Fluorophore +from ..models import FluorState from ..tasks import calc_fret @@ -34,7 +34,7 @@ def fret_chart(request): # Query all fluorophores (States + DyeStates) with required properties # Build display name and sort in the database fluorophores = ( - Fluorophore.objects.exclude(ext_coeff=None) + FluorState.objects.exclude(ext_coeff=None) .exclude(qy=None) .filter(spectra__subtype__in=("ex", "ab")) .annotate( diff --git a/backend/proteins/views/microscope.py b/backend/proteins/views/microscope.py index 1521a8c6a..8d6ec2d73 100644 --- a/backend/proteins/views/microscope.py +++ b/backend/proteins/views/microscope.py @@ -23,7 +23,7 @@ from proteins.models import ( Camera, DyeState, - Fluorophore, + FluorState, Light, Microscope, OcFluorEff, @@ -93,11 +93,11 @@ def scope_report_json(request, pk): # For proteins, use the protein's UUID; for dyes, use the dye's ID (cast to string) owner_id=Case( When( - fluor__entity_type=Fluorophore.EntityTypes.PROTEIN, + fluor__entity_type=FluorState.EntityTypes.PROTEIN, then=F("fluor__state__protein__uuid"), ), When( - fluor__entity_type=Fluorophore.EntityTypes.DYE, + fluor__entity_type=FluorState.EntityTypes.DYE, then=Cast(F("fluor__dyestate__dye__id"), CharField()), ), default=Value(None), diff --git a/backend/tests/test_proteins/test_fluorophore.py b/backend/tests/test_proteins/test_fluorophore.py index 5b8a06fb5..14b61dee4 100644 --- a/backend/tests/test_proteins/test_fluorophore.py +++ b/backend/tests/test_proteins/test_fluorophore.py @@ -64,7 +64,7 @@ class TestRebuildAttributesBasicWaterfall: def test_single_measurement_sets_values(self, state: State): """A single measurement should populate the fluorophore.""" - m = FM(fluorophore=state, ex_max=488, em_max=509, qy=0.67) + m = FM(state=state, ex_max=488, em_max=509, qy=0.67) m.save(rebuild_cache=False) state.rebuild_attributes() state.refresh_from_db() @@ -76,10 +76,10 @@ def test_single_measurement_sets_values(self, state: State): def test_multiple_measurements_first_value_wins(self, state: State): """When multiple measurements exist, first non-null value wins.""" # Measurement with partial data (no qy) - m1 = FM(fluorophore=state, ex_max=488, em_max=509, qy=None) + m1 = FM(state=state, ex_max=488, em_max=509, qy=None) m1.save(rebuild_cache=False) # Measurement with different values - m2 = FM(fluorophore=state, ex_max=500, em_max=520, qy=0.5) + m2 = FM(state=state, ex_max=500, em_max=520, qy=0.5) m2.save(rebuild_cache=False) state.rebuild_attributes() @@ -93,9 +93,9 @@ def test_multiple_measurements_first_value_wins(self, state: State): def test_source_map_tracks_measurement_ids(self, state: State): """source_map should track which measurement each field came from.""" - m1 = FM(fluorophore=state, ex_max=488, qy=None) + m1 = FM(state=state, ex_max=488, qy=None) m1.save(rebuild_cache=False) - m2 = FM(fluorophore=state, ex_max=None, qy=0.5) + m2 = FM(state=state, ex_max=None, qy=0.5) m2.save(rebuild_cache=False) state.rebuild_attributes() @@ -116,7 +116,7 @@ def test_primary_ref_measurement_overrides_recent(self, state_with_ref: State): # Recent but not from primary reference m1 = FM( - fluorophore=state_with_ref, + state=state_with_ref, reference=other_ref, ex_max=500, em_max=520, @@ -124,7 +124,7 @@ def test_primary_ref_measurement_overrides_recent(self, state_with_ref: State): m1.save(rebuild_cache=False) # Older but from primary reference m2 = FM( - fluorophore=state_with_ref, + state=state_with_ref, reference=primary_ref, ex_max=488, em_max=509, @@ -146,7 +146,7 @@ def test_primary_ref_on_dye(self, dyestate_with_ref: DyeState): # Not from primary reference m1 = FM( - fluorophore=dyestate_with_ref, + state=dyestate_with_ref, reference=other_ref, ex_max=600, em_max=650, @@ -154,7 +154,7 @@ def test_primary_ref_on_dye(self, dyestate_with_ref: DyeState): m1.save(rebuild_cache=False) # From primary reference m2 = FM( - fluorophore=dyestate_with_ref, + state=dyestate_with_ref, reference=primary_ref, ex_max=550, em_max=580, @@ -178,7 +178,7 @@ def test_pinned_field_overrides_all_priorities(self, state_with_ref: State): # measurement from primary reference - highest normal priority m_primary = FM( - fluorophore=state_with_ref, + state=state_with_ref, reference=primary_ref, ex_max=488, em_max=509, @@ -187,7 +187,7 @@ def test_pinned_field_overrides_all_priorities(self, state_with_ref: State): m_primary.save(rebuild_cache=False) # Older, not primary - lowest normal priority m_pinned = FM( - fluorophore=state_with_ref, + state=state_with_ref, ex_max=500, em_max=520, qy=0.50, @@ -212,9 +212,9 @@ def test_pinned_field_overrides_all_priorities(self, state_with_ref: State): def test_pinned_field_with_null_value_is_skipped(self, state: State): """If a pinned measurement has null for the pinned field, skip it.""" - m1 = FM(fluorophore=state, ex_max=488, qy=0.67) + m1 = FM(state=state, ex_max=488, qy=0.67) m1.save(rebuild_cache=False) - m2 = FM(fluorophore=state, ex_max=500, qy=None) + m2 = FM(state=state, ex_max=500, qy=None) m2.save(rebuild_cache=False) # Pin qy to m2, but m2 has null qy @@ -230,7 +230,7 @@ def test_pinned_field_with_null_value_is_skipped(self, state: State): def test_pinned_nonexistent_measurement_is_ignored(self, state: State): """If pinned measurement doesn't exist, ignore and use waterfall.""" - m = FM(fluorophore=state, qy=0.67) + m = FM(state=state, qy=0.67) m.save(rebuild_cache=False) # Pin to a non-existent measurement ID @@ -248,9 +248,9 @@ def test_pinned_measurement_from_other_fluorophore_is_ignored(self, protein: Pro state1 = State.objects.create(protein=protein, name="state1") state2 = State.objects.create(protein=protein, name="state2") - m1 = FM(fluorophore=state1, qy=0.67) + m1 = FM(state=state1, qy=0.67) m1.save(rebuild_cache=False) - m_other = FM(fluorophore=state2, qy=0.50) + m_other = FM(state=state2, qy=0.50) m_other.save(rebuild_cache=False) # Try to pin state1's qy to state2's measurement @@ -284,7 +284,7 @@ class TestRebuildAttributesAutoTrigger: def test_measurement_save_triggers_rebuild(self, state: State): """Creating a measurement should trigger rebuild_attributes.""" # Create measurement - should auto-trigger rebuild (rebuild_cache=True by default) - m = FM(fluorophore=state, ex_max=488, em_max=509) + m = FM(state=state, ex_max=488, em_max=509) m.save() state.refresh_from_db() @@ -293,12 +293,12 @@ def test_measurement_save_triggers_rebuild(self, state: State): def test_measurement_delete_triggers_rebuild(self, state: State): """Deleting a measurement should trigger rebuild_attributes.""" - m1 = FM(fluorophore=state, ex_max=488, em_max=509) + m1 = FM(state=state, ex_max=488, em_max=509) m1.save() state.refresh_from_db() assert state.ex_max == 488 - m2 = FM(fluorophore=state, ex_max=500, em_max=520) + m2 = FM(state=state, ex_max=500, em_max=520) m2.save() state.refresh_from_db() # Still 488 because more recent @@ -314,7 +314,7 @@ def test_measurement_delete_triggers_rebuild(self, state: State): def test_rebuild_cache_false_skips_rebuild(self, state: State): """rebuild_cache=False should skip the rebuild.""" - m = FM(fluorophore=state, ex_max=488) + m = FM(state=state, ex_max=488) m.save(rebuild_cache=False) state.refresh_from_db() From 7ddcbfd648bd6e52eb596824cede1e699f698abe Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Wed, 26 Nov 2025 11:08:37 -0500 Subject: [PATCH 55/57] update admin --- backend/proteins/admin.py | 41 ++++++++++++++++++++++++++++------ backend/proteins/models/dye.py | 3 +++ 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/backend/proteins/admin.py b/backend/proteins/admin.py index dd594d657..651430690 100644 --- a/backend/proteins/admin.py +++ b/backend/proteins/admin.py @@ -99,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 @@ -115,9 +113,8 @@ class StateInline(MultipleSpectraOwner, admin.StackedInline): ("ex_max", "em_max"), ("ext_coeff", "qy"), ("twop_ex_max", "twop_peak_gm", "twop_qy"), - ("pka", "maturation"), + "pka", "lifetime", - "bleach_links", "spectra", ) }, @@ -132,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 = [] @@ -149,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") @@ -168,8 +181,22 @@ class LightAdmin(SpectrumOwner, admin.ModelAdmin): @admin.register(Dye) 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(DyeState) diff --git a/backend/proteins/models/dye.py b/backend/proteins/models/dye.py index 24537b3cc..3c26a72d2 100644 --- a/backend/proteins/models/dye.py +++ b/backend/proteins/models/dye.py @@ -48,6 +48,9 @@ class Dye(Authorable, TimeStampedModel, Product): # TODO: rename to SmallMolecu class Meta: abstract = False + def __str__(self) -> str: + return self.name + def save(self, *args, **kwargs): self.slug = slugify(self.name) # Always regenerate, like Protein.save() super().save(*args, **kwargs) From ef6e63179686cd088df2313fd78d2caba2967e14 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Wed, 26 Nov 2025 11:15:01 -0500 Subject: [PATCH 56/57] fix admin --- backend/proteins/admin.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/backend/proteins/admin.py b/backend/proteins/admin.py index 651430690..1649c3575 100644 --- a/backend/proteins/admin.py +++ b/backend/proteins/admin.py @@ -266,6 +266,9 @@ def get_fields(self, request, obj=None): # 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", @@ -284,11 +287,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'{obj.owner}' + 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'{owner}' return mark_safe(link) @admin.display(description="Spectrum Preview") From 50aa2d723c7db936a28c53c3822d100ab5b69779 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Wed, 26 Nov 2025 11:20:24 -0500 Subject: [PATCH 57/57] update view --- backend/proteins/admin.py | 49 ++++++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/backend/proteins/admin.py b/backend/proteins/admin.py index 1649c3575..bd68f9cb3 100644 --- a/backend/proteins/admin.py +++ b/backend/proteins/admin.py @@ -199,12 +199,6 @@ class DyeAdmin(admin.ModelAdmin): inlines = (DyeStateInline,) -@admin.register(DyeState) -class DyeStateAdmin(MultipleSpectraOwner, VersionAdmin): - model = DyeState - ordering = ("-created",) - - @admin.register(Filter) class FilterAdmin(SpectrumOwner, VersionAdmin): model = Filter @@ -401,6 +395,49 @@ def protein_link(self, obj): return mark_safe(f'{obj.protein}') +@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'{obj.dye}') + + class StateTransitionAdmin(VersionAdmin): model = StateTransition list_select_related = ("protein", "from_state", "to_state")