Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move implant weight to surgery model #836

Merged
merged 20 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ab0406a
Issue #835
k1o0 Feb 14, 2024
8c4cbe5
BaseTests.ar check response data is dict instead of OrderedDict
Mar 21, 2024
4b28460
Do not add subject's projects to session projects upon save
Mar 21, 2024
fbc9501
Merge pull request #855 from cortex-lab/dev
mayofaulkner May 2, 2024
58a3a1f
Merge pull request #857 from cortex-lab/dev
chris-langfield May 6, 2024
9a7a118
GitHub Actions generated requirements_frozen.txt
invalid-email-address May 6, 2024
6b0ebf4
add channels labels dataset type
May 7, 2024
b474d2a
Merge pull request #860 from cortex-lab/channels_labels
chris-langfield May 7, 2024
b16d183
GitHub Actions generated requirements_frozen.txt
invalid-email-address May 7, 2024
97eb0fa
Merge branch 'dev' into surgeryImplantWeight
May 10, 2024
94ab097
Remove hardcoded threshold; subtract reference implant weight from re…
May 23, 2024
ad12b43
remove user from the UCL imports so his account is unsynced
oliche Jun 4, 2024
860522d
GitHub Actions generated requirements_frozen.txt
invalid-email-address Jun 4, 2024
b4e6ff6
Merge branch 'master' into surgeryImplantWeight
Jun 7, 2024
8addcf8
Do not add current implant weight to reference/zscore weights; accoun…
Jun 7, 2024
e50086a
Bugfix in move surgeries migration
k1o0 Jun 12, 2024
d09d99e
Surgery implant weight a required field
k1o0 Jun 12, 2024
4a7e378
Merge branch 'confirmation' into surgeryImplantWeight
k1o0 Jun 12, 2024
e9257f6
Remove pattern from mic data dataset type fixture
k1o0 Jun 12, 2024
bb4a249
Fix incorrect ONE link in docs
Jun 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 19 additions & 16 deletions alyx/actions/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,15 +244,6 @@ def session_l(self, obj):


class WaterRestrictionForm(forms.ModelForm):
implant_weight = forms.FloatField()

def save(self, commit=True):
implant_weight = self.cleaned_data.get('implant_weight')
subject = self.cleaned_data.get('subject', None)
if implant_weight:
subject.implant_weight = implant_weight
subject.save()
return super(WaterRestrictionForm, self).save(commit=commit)

class Meta:
model = WaterRestriction
Expand All @@ -272,22 +263,20 @@ def formfield_for_foreignkey(self, db_field, request, **kwargs):
def get_form(self, request, obj=None, **kwargs):
form = super(WaterRestrictionAdmin, self).get_form(request, obj, **kwargs)
subject = getattr(obj, 'subject', None)
iw = getattr(subject, 'implant_weight', None)
rw = subject.water_control.weight() if subject else None
form.base_fields['implant_weight'].initial = iw
if self.has_change_permission(request, obj):
form.base_fields['reference_weight'].initial = rw or 0
return form

form = WaterRestrictionForm

fields = ['subject', 'implant_weight', 'reference_weight',
'start_time', 'end_time', 'water_type', 'users', 'narrative']
fields = ['subject', 'reference_weight', 'start_time',
'end_time', 'water_type', 'users', 'narrative', 'implant_weight']
list_display = ('subject_w', 'start_time_l', 'end_time_l', 'water_type', 'weight',
'weight_ref') + WaterControl._columns[3:] + ('projects',)
list_select_related = ('subject',)
list_display_links = ('start_time_l', 'end_time_l')
readonly_fields = ('weight',) # WaterControl._columns[1:]
readonly_fields = ('weight', 'implant_weight') # WaterControl._columns[1:]
ordering = ['-start_time', 'subject__nickname']
search_fields = ['subject__nickname', 'subject__projects__name']
list_filter = [ResponsibleUserListFilter,
Expand Down Expand Up @@ -361,6 +350,12 @@ def given_water_total(self, obj):
return '%.2f' % obj.subject.water_control.given_water_total()
given_water_total.short_description = 'water tot'

def implant_weight(self, obj):
if not obj.subject:
return
return '%.2f' % (obj.subject.water_control.implant_weight() or 0.)
implant_weight.short_description = 'implant weight'

def has_change_permission(self, request, obj=None):
# setting to override edition of water restrictions in the settings.lab file
override = getattr(settings, 'WATER_RESTRICTIONS_EDITABLE', False)
Expand Down Expand Up @@ -421,11 +416,19 @@ class WaterTypeAdmin(BaseActionAdmin):
list_display_links = ('name',)


class SurgeryActionForm(BaseActionForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['implant_weight'].required = True


class SurgeryAdmin(BaseActionAdmin):
list_display = ['subject_l', 'date', 'users_l', 'procedures_l', 'narrative', 'projects']
form = SurgeryActionForm
list_display = ['subject_l', 'date', 'users_l', 'procedures_l',
'narrative', 'projects', 'implant_weight']
list_select_related = ('subject',)

fields = BaseActionAdmin.fields + ['outcome_type']
fields = BaseActionAdmin.fields + ['outcome_type', 'implant_weight']
list_display_links = ['date']
search_fields = ('subject__nickname', 'subject__projects__name')
list_filter = [SubjectAliveListFilter,
Expand Down
1 change: 1 addition & 0 deletions alyx/actions/migrations/0022_project_to_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

logger = logging.getLogger(__name__)


def project2projects(apps, schema_editor):
"""
Find sessions where the project field (singular) value is not in the projects (plural) many-to-many
Expand Down
24 changes: 24 additions & 0 deletions alyx/actions/migrations/0024_surgery_implant_weight.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 4.2.10 on 2024-03-15 13:53

import django.core.validators
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('actions', '0023_remove_session_project'),
]

operations = [
migrations.AddField(
model_name='surgery',
name='implant_weight',
field=models.FloatField(blank=True, default=0, help_text='Implant weight in grams', validators=[django.core.validators.MinValueValidator(0)]),
preserve_default=False,
),
migrations.AddConstraint(
model_name='surgery',
constraint=models.CheckConstraint(check=models.Q(('implant_weight__gte', 0)), name='implant_weight_gte_0'),
),
]
71 changes: 71 additions & 0 deletions alyx/actions/migrations/0025_move_implant_weight.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Generated by Django 4.2.10 on 2024-03-15 13:53
"""
In 2024-03 the Subject.implant_weight field was removed and the Surgery.implant_weight field
was added. This script 'saves' the non-zero implant weights to the Subject.JSON field and if
unambiguous, saves the implant weight to the relevent surgery.
"""
import logging
from pathlib import Path
from datetime import datetime, timezone

from django.db import migrations
from django.core import serializers

import alyx

logger = logging.getLogger(__name__)


def move_implant_weight(apps, schema_editor):
Subject = apps.get_model('subjects', 'Subject')
ProcedureType = apps.get_model('actions', 'ProcedureType')
try:
headplate_implant = ProcedureType.objects.get(name='Headplate implant')
except ProcedureType.DoesNotExist:
headplate_implant = None
query = Subject.objects.filter(implant_weight__gt=0)
now = datetime.now(timezone.utc).isoformat()
n = 0
for subject in query:
# Add implant weight to JSON field for prosperity
iw = subject.implant_weight
json = subject.json or {}
d = {'implant_weight': [{'value': iw, 'datetime': now}]}
if 'history' in json:
json['history'].update(d)
else:
json['history'] = d
subject.json = json
subject.save()
# If possible, add implant weight to previous surgery
surgeries = subject.actions_surgerys.filter(procedures__name__icontains='implant').distinct()
if surgeries.count() == 0:
# If no surgeries contain an implant procedure, attempt to find one surgery where
# implant or headplate are mentioned in the narrative
surgeries = subject.actions_surgerys.filter(narrative__iregex='.*(headplate|implant).*')
# If there is an unambiguous result, set the surgery implant weight
if surgeries.count() == 1:
surgery = surgeries.first()
surgery.implant_weight = iw
# If headplate is mentioned in the narrative, add Headplate implant procedure
# to the surgeries procedures list
if headplate_implant and 'headplate' in surgery.narrative.lower():
surgery.procedures.add(headplate_implant)
surgery.save()
n += 1

logger.info(f'implant weights: {query.count():,g} subjects; {n:,g} surgeries updated')
if query.count():
filename = now[:19].replace(':', '-') + '_subject-implant-weight.json'
filepath = Path(alyx.__file__).parents[2].joinpath('data', filename)
with open(filepath, 'w') as fp:
fp.write(serializers.serialize('json', query, fields=['implant_weight']))


class Migration(migrations.Migration):

dependencies = [
('actions', '0024_surgery_implant_weight'),
]

operations = [migrations.RunPython(move_implant_weight)]
7 changes: 7 additions & 0 deletions alyx/actions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,15 @@ class Surgery(BaseAction):
default=_default_surgery_location,
help_text="The physical location at which the surgery was "
"performed")
implant_weight = models.FloatField(null=False, blank=True, validators=[MinValueValidator(0)],
help_text="Implant weight in grams")

class Meta:
verbose_name_plural = "surgeries"
constraints = [
models.CheckConstraint(
check=models.Q(implant_weight__gte=0), name="implant_weight_gte_0"),
]

def save(self, *args, **kwargs):
# Issue #422.
Expand Down Expand Up @@ -336,6 +342,7 @@ def save(self, *args, **kwargs):
self.reference_weight = w[1]
# makes sure the closest weighing is one week around, break if not
assert abs(w[0] - self.start_time) < timedelta(days=7)

output = super(WaterRestriction, self).save(*args, **kwargs)
# When creating a water restriction, the subject's protocol number should be changed to 3
# (request by Charu in 03/2022)
Expand Down
37 changes: 28 additions & 9 deletions alyx/actions/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from actions.water_control import to_date
from actions.models import (
WaterAdministration, WaterRestriction, WaterType, Weighing,
Notification, NotificationRule, create_notification)
Notification, NotificationRule, create_notification, Surgery, ProcedureType)
from actions.notifications import check_water_administration, check_weighed
from misc.models import LabMember, LabMembership, Lab
from subjects.models import Subject
Expand Down Expand Up @@ -40,6 +40,18 @@ def setUp(self):
Lab.objects.create(name='zscore', reference_weight_pct=0, zscore_weight_pct=0.85)
Lab.objects.create(name='rweigh', reference_weight_pct=0.85, zscore_weight_pct=0)
Lab.objects.create(name='mixed', reference_weight_pct=0.425, zscore_weight_pct=0.425)
# create some surgeries to go with it (for testing implant weight in calculations)
date = self.start_date - datetime.timedelta(days=7)
surgery0 = Surgery.objects.create(subject=self.sub, implant_weight=4.56, start_time=date)
implant_proc, _ = ProcedureType.objects.get_or_create(name='Headplate implant')
surgery0.procedures.add(implant_proc.pk)
date = self.start_date + datetime.timedelta(days=10)
surgery1 = Surgery.objects.create(subject=self.sub, implant_weight=0., start_time=date)
date = self.start_date + datetime.timedelta(days=25)
surgery2 = Surgery.objects.create(subject=self.sub, implant_weight=7., start_time=date)
surgery2.procedures.add(implant_proc.pk)
self.surgeries = [surgery0, surgery1, surgery2]

# Create an initial Water Restriction
start_wr = self.start_date + datetime.timedelta(days=self.rwind)
water_type = WaterType.objects.get(name='Hydrogel 5% Citric Acid')
Expand Down Expand Up @@ -68,29 +80,36 @@ def test_water_control_thresholds(self):
self.sub.save()
wc = self.sub.reinit_water_control()
wc.expected_weight()
self.assertAlmostEqual(self.wei[self.rwind], wc.reference_weight())
self.assertAlmostEqual(self.wei[self.rwind], wc.expected_weight())
# expected weight should be different to reference weight as the implant weight changes
expected = self.wei[self.rwind] + (wc.implant_weights[1][1] - wc.implant_weights[0][1])
self.assertAlmostEqual(expected, wc.reference_weight())
self.assertAlmostEqual(expected, wc.expected_weight())
# test implant weight values
self.assertEqual([4.56, 7.0], [x[1] for x in wc.implant_weights])
self.assertEqual(7.0, wc.implant_weight())
self.assertEqual(4.56, wc.implant_weight(self.start_date))
self.assertEqual(4.56, wc.reference_implant_weight_at())
# test computation on zscore weight lab alone
self.sub.lab = Lab.objects.get(name='zscore')
self.sub.save()
wc = self.sub.reinit_water_control()
wc.expected_weight()
self.assertAlmostEqual(self.wei[self.rwind], wc.reference_weight())
zscore = wc.zscore_weight()
self.assertAlmostEqual(zscore, 38.049183673469386)
self.assertEqual(zscore, wc.expected_weight())
# test computation on mixed lab
self.sub.lab = Lab.objects.get(name='mixed')
self.sub.save()
wc = self.sub.reinit_water_control()
self.assertAlmostEqual(self.wei[self.rwind], wc.reference_weight())
self.assertAlmostEqual(expected, wc.reference_weight())
self.assertAlmostEqual(wc.expected_weight(), (wc.reference_weight() + zscore) / 2)
# test that the thresholds are all above 70%
self.assertTrue(all([thrsh[0] > 0.4 for thrsh in wc.thresholds]))
self.assertTrue(all(thrsh[0] > 0.4 for thrsh in wc.thresholds))
# if we change the reference weight of the water restriction, this should change in wc too
self.assertAlmostEqual(wc.reference_weight(), self.wr.reference_weight)
self.assertAlmostEqual(expected, wc.reference_weight())
self.wr.reference_weight = self.wr.reference_weight + 1
self.wr.save()
wc = self.sub.reinit_water_control()
self.assertAlmostEqual(wc.reference_weight(), self.wr.reference_weight)
self.assertAlmostEqual(expected + 1, wc.reference_weight())


class NotificationTests(TestCase):
Expand Down
17 changes: 10 additions & 7 deletions alyx/actions/tests_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from alyx.base import BaseTests
from subjects.models import Subject, Project
from misc.models import Lab, Note, ContentType
from actions.models import Session, WaterType, WaterAdministration
from actions.models import Session, WaterType, WaterAdministration, Surgery, ProcedureType
from data.models import Dataset, DatasetType


Expand All @@ -22,10 +22,13 @@ def setUp(self):
self.lab02 = Lab.objects.create(name='awesomelab')
self.projectX = Project.objects.create(name='projectX')
self.projectY = Project.objects.create(name='projectY')
# Set an implant weight.
self.subject.implant_weight = 4.56
self.subject.save()
self.test_protocol = 'test_passoire'
# Create a surgery with an implant weight.
self.surgery = Surgery.objects.create(
subject=self.subject, implant_weight=4.56, start_time=now() - timedelta(days=7))
implant_proc, _ = ProcedureType.objects.get_or_create(name='Headplate implant')
self.surgery.procedures.add(implant_proc.pk)
self.subject.save()

def tearDown(self):
base.DISABLE_MAIL = False
Expand Down Expand Up @@ -305,9 +308,9 @@ def test_surgeries(self):
sr = self.ar(self.client.get(reverse('surgeries-list',)))
self.assertTrue(ns > 0)
self.assertTrue(len(sr) == ns)
self.assertTrue(set(sr[0].keys()) == set(
['id', 'subject', 'name', 'json', 'narrative', 'start_time', 'end_time',
'outcome_type', 'lab', 'location', 'users', 'procedures']))
self.assertTrue(set(sr[0].keys()) == {
'id', 'subject', 'name', 'json', 'narrative', 'start_time', 'end_time',
'outcome_type', 'lab', 'location', 'users', 'procedures', 'implant_weight'})

def test_list_retrieve_water_restrictions(self):
url = reverse('water-restriction-list')
Expand Down
6 changes: 4 additions & 2 deletions alyx/actions/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import timedelta, date
from datetime import timedelta, date, datetime
from operator import itemgetter

from one.alf.spec import QC
Expand Down Expand Up @@ -457,7 +457,9 @@ def get(self, request, format=None, nickname=None):
end_date = request.query_params.get('end_date', None)
subject = Subject.objects.get(nickname=nickname)
records = subject.water_control.to_jsonable(start_date=start_date, end_date=end_date)
data = {'subject': nickname, 'implant_weight': subject.implant_weight,
date_str = datetime.strptime(start_date, '%Y-%m-%d') if start_date else None
ref_iw = subject.water_control.reference_implant_weight_at(date_str)
data = {'subject': nickname, 'implant_weight': ref_iw,
'reference_weight_pct': subject.water_control.reference_weight_pct,
'zscore_weight_pct': subject.water_control.zscore_weight_pct,
'records': records}
Expand Down
Loading
Loading