|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
3 | | -from typing import TYPE_CHECKING, Final |
| 3 | +from typing import TYPE_CHECKING, Final, cast |
4 | 4 |
|
5 | 5 | from django.db import models |
6 | 6 | from django.utils.text import slugify |
@@ -134,81 +134,49 @@ def rebuild_attributes(self) -> None: |
134 | 134 | 3. Primary reference - Measurement from the owner's primary_reference |
135 | 135 | 4. Most recent - Fallback by date_measured (most recent first) |
136 | 136 | """ |
137 | | - from proteins.models import FluorescenceMeasurement |
138 | | - |
139 | 137 | measurable_fields = AbstractFluorescenceData.get_measurable_fields() |
140 | 138 | new_values: dict[str, object] = {} |
141 | 139 | new_source_map: dict[str, int] = {} |
142 | | - |
143 | | - # Get primary reference ID for priority sorting |
144 | 140 | primary_ref_id = self._get_primary_reference_id() |
145 | 141 |
|
146 | | - # 1. Handle pinned fields first (admin overrides) |
| 142 | + # Fetch pinned measurements in one query |
| 143 | + pinned_source_map = cast("dict[str, int]", self.pinned_source_map) |
| 144 | + pinned_by_id = {m.id: m for m in self.measurements.filter(id__in=pinned_source_map.values())} |
| 145 | + |
| 146 | + # Handle pinned fields first (admin overrides) |
147 | 147 | pinned_fields: set[str] = set() |
148 | | - for field, measurement_id in self.pinned_source_map.items(): |
149 | | - if field not in measurable_fields: |
150 | | - continue |
151 | | - try: |
152 | | - measurement = FluorescenceMeasurement.objects.get(id=measurement_id, fluorophore=self) |
153 | | - val = getattr(measurement, field) |
154 | | - if val is not None: |
| 148 | + for field, mid in pinned_source_map.items(): |
| 149 | + if field in measurable_fields and (m := pinned_by_id.get(mid)): |
| 150 | + if (val := getattr(m, field)) is not None: |
155 | 151 | new_values[field] = val |
156 | | - new_source_map[field] = measurement.id |
| 152 | + new_source_map[field] = m.id |
157 | 153 | pinned_fields.add(field) |
158 | | - except FluorescenceMeasurement.DoesNotExist: |
159 | | - # Pinned measurement no longer exists, skip it |
160 | | - pass |
161 | | - |
162 | | - # 2. Fetch all measurements for non-pinned fields |
163 | | - measurements = list(self.measurements.select_related("reference").all()) |
164 | 154 |
|
165 | 155 | # Sort by priority: trusted > primary_reference > most recent date |
166 | | - def measurement_priority(m: FluorescenceMeasurement) -> tuple[int, int, str]: |
167 | | - # Lower tuple = higher priority (sorted ascending) |
168 | | - trusted_score = 0 if m.is_trusted else 1 |
169 | | - primary_score = 0 if primary_ref_id and m.reference_id == primary_ref_id else 1 |
170 | | - # Use date_measured for sorting, fallback to empty string (sorts last) |
171 | | - date_str = m.date_measured.isoformat() if m.date_measured else "" |
172 | | - # Negate by reversing string comparison for descending date order |
173 | | - return (trusted_score, primary_score, date_str) |
174 | | - |
175 | | - # Sort: trusted first, then primary ref, then most recent date |
176 | | - measurements.sort(key=measurement_priority) |
177 | | - # Reverse date sorting within groups (we want most recent first) |
178 | | - # Re-sort with proper date handling |
179 | | - measurements.sort( |
| 156 | + measurements = sorted( |
| 157 | + self.measurements.all(), |
180 | 158 | key=lambda m: ( |
181 | | - 0 if m.is_trusted else 1, |
182 | | - 0 if primary_ref_id and m.reference_id == primary_ref_id else 1, |
183 | | - # Invert date for descending order (most recent first) |
| 159 | + not m.is_trusted, |
| 160 | + not (primary_ref_id and m.reference_id == primary_ref_id), |
184 | 161 | -(m.date_measured.toordinal() if m.date_measured else 0), |
185 | | - ) |
| 162 | + ), |
186 | 163 | ) |
187 | 164 |
|
188 | | - # 3. Waterfall Logic: Find the first non-null value for each non-pinned field |
| 165 | + # Waterfall: first non-null value for each non-pinned field |
189 | 166 | for field in measurable_fields: |
190 | 167 | if field in pinned_fields: |
191 | | - continue # Already handled above |
192 | | - |
| 168 | + continue |
193 | 169 | for m in measurements: |
194 | | - val = getattr(m, field) |
195 | | - if val is not None: |
| 170 | + if (val := getattr(m, field)) is not None: |
196 | 171 | new_values[field] = val |
197 | 172 | new_source_map[field] = m.id |
198 | 173 | break |
199 | 174 | else: |
200 | | - # No measurement had a value for this field |
201 | | - # Use field default for non-nullable fields (e.g., is_dark) |
202 | 175 | field_obj = self._meta.get_field(field) |
203 | | - if field_obj.has_default(): |
204 | | - new_values[field] = field_obj.get_default() |
205 | | - else: |
206 | | - new_values[field] = None |
| 176 | + new_values[field] = field_obj.get_default() if field_obj.has_default() else None |
207 | 177 |
|
208 | | - # 4. Update cached values |
209 | 178 | for key, val in new_values.items(): |
210 | 179 | setattr(self, key, val) |
211 | | - |
212 | 180 | self.source_map = new_source_map |
213 | 181 | self.save() |
214 | 182 |
|
|
0 commit comments