Skip to content

Commit

Permalink
Merge 94ab097 into aadfa12
Browse files Browse the repository at this point in the history
  • Loading branch information
k1o0 committed May 23, 2024
2 parents aadfa12 + 94ab097 commit c0ef6d5
Show file tree
Hide file tree
Showing 19 changed files with 322 additions and 73 deletions.
28 changes: 12 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 @@ -422,10 +417,11 @@ class WaterTypeAdmin(BaseActionAdmin):


class SurgeryAdmin(BaseActionAdmin):
list_display = ['subject_l', 'date', 'users_l', 'procedures_l', 'narrative', 'projects']
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')
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

0 comments on commit c0ef6d5

Please sign in to comment.