• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cortex-lab / alyx / 19922926026

04 Dec 2025 08:45AM UTC coverage: 85.756% (-0.03%) from 85.787%
19922926026

Pull #962

github

web-flow
Merge dc99b461a into f2bc6be09
Pull Request #962: Water restrictions in UW

8254 of 9625 relevant lines covered (85.76%)

0.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.86
alyx/actions/admin.py
1
import base64
1✔
2
import json
1✔
3
import logging
1✔
4

5
from django import forms
1✔
6
from django.conf import settings
1✔
7
from django.contrib import admin
1✔
8
from django.contrib.auth import get_user_model
1✔
9
from django.db.models import Case, When, Exists, OuterRef
1✔
10
from django.urls import reverse
1✔
11
from django.utils.html import format_html
1✔
12
from django_admin_listfilter_dropdown.filters import RelatedDropdownFilter
1✔
13
from django.contrib.admin import TabularInline
1✔
14
from django.contrib.contenttypes.models import ContentType
1✔
15
from rangefilter.filters import DateRangeFilter
1✔
16

17
from alyx.base import (BaseAdmin, DefaultListFilter, BaseInlineAdmin, get_admin_url)
1✔
18
from .models import (OtherAction, ProcedureType, Session, EphysSession, Surgery, VirusInjection,
1✔
19
                     WaterAdministration, WaterRestriction, Weighing, WaterType,
20
                     Notification, NotificationRule, Cull, CullReason, CullMethod, ImagingSession
21
                     )
22
from data.models import Dataset, FileRecord
1✔
23
from misc.admin import NoteInline
1✔
24
from misc.models import Note
1✔
25
from subjects.models import Subject
1✔
26
from .water_control import WaterControl
1✔
27
from experiments.models import ProbeInsertion, FOV
1✔
28
from jobs.models import Task
1✔
29

30
logger = logging.getLogger(__name__)
1✔
31

32

33
# Filters
34
# ------------------------------------------------------------------------------------------------
35

36
class ResponsibleUserListFilter(DefaultListFilter):
1✔
37
    title = 'responsible user'
1✔
38
    parameter_name = 'responsible_user'
1✔
39

40
    def lookups(self, request, model_admin):
1✔
41
        return (
1✔
42
            (None, 'Me'),
43
            ('all', 'All'),
44
        )
45

46
    def queryset(self, request, queryset):
1✔
47
        if self.value() is None:
1✔
48
            return queryset.filter(subject__responsible_user=request.user)
1✔
49
        elif self.value == 'all':
×
50
            return queryset.all()
×
51

52

53
class SubjectAliveListFilter(DefaultListFilter):
1✔
54
    title = 'alive'
1✔
55
    parameter_name = 'alive'
1✔
56

57
    def lookups(self, request, model_admin):
1✔
58
        return (
1✔
59
            (None, 'Yes'),
60
            ('n', 'No'),
61
            ('all', 'All'),
62
        )
63

64
    def queryset(self, request, queryset):
1✔
65
        if self.value() is None:
1✔
66
            return queryset.filter(subject__cull__isnull=True)
1✔
67
        if self.value() == 'n':
×
68
            return queryset.exclude(subject__cull__isnull=True)
×
69
        elif self.value == 'all':
×
70
            return queryset.all()
×
71

72

73
class ActiveFilter(DefaultListFilter):
1✔
74
    title = 'active'
1✔
75
    parameter_name = 'active'
1✔
76

77
    def lookups(self, request, model_admin):
1✔
78
        return (
1✔
79
            (None, 'All'),
80
            ('active', 'Active'),
81
        )
82

83
    def queryset(self, request, queryset):
1✔
84
        if self.value() == 'active':
1✔
85
            return queryset.filter(start_time__isnull=False,
×
86
                                   end_time__isnull=True,
87
                                   )
88
        elif self.value is None:
1✔
89
            return queryset.all()
×
90

91

92
class CreatedByListFilter(DefaultListFilter):
1✔
93
    title = 'users'
1✔
94
    parameter_name = 'users'
1✔
95

96
    def lookups(self, request, model_admin):
1✔
97
        return (
×
98
            (None, 'Me'),
99
            ('all', 'All'),
100
        )
101

102
    def queryset(self, request, queryset):
1✔
103
        if self.value() is None:
×
104
            return queryset.filter(users=request.user)
×
105
        elif self.value == 'all':
×
106
            return queryset.all()
×
107

108

109
class HasNarrativeFilter(DefaultListFilter):
1✔
110
    title = 'narrative'
1✔
111
    parameter_name = 'has_narrative'
1✔
112

113
    def lookups(self, request, model_admin):
1✔
114
        return (
1✔
115
            (None, 'All'),
116
            ('narrative', 'Has narrative'),
117
            ('no_narrative', 'No narrative'),
118
        )
119

120
    def queryset(self, request, queryset):
1✔
121
        if self.value is not None:
1✔
122
            regex_string = r'^(?:\s*|auto-generated session)$'
1✔
123
            if self.value() == 'narrative':
1✔
124
                return queryset.exclude(narrative__regex=regex_string)
×
125
            if self.value() == 'no_narrative':
1✔
126
                return queryset.filter(narrative__regex=regex_string)
×
127
        else:
128
            return queryset.all()
×
129

130

131
class HasNoteFilter(DefaultListFilter):
1✔
132
    title = 'note'
1✔
133
    parameter_name = 'has_note'
1✔
134

135
    def lookups(self, request, model_admin):
1✔
136
        return (
1✔
137
            (None, 'All'),
138
            ('notes', 'Has notes'),
139
            ('no_notes', 'No notes'),
140
        )
141

142
    def queryset(self, request, queryset):
1✔
143
        if self.value is not None:
1✔
144
            notes_subquery = Note.objects.filter(
1✔
145
                content_type=ContentType.objects.get_for_model(Session),
146
                object_id=OuterRef('pk')
147
            )
148
            if self.value() == 'notes':
1✔
149
                return queryset.filter(Exists(notes_subquery))
×
150
            if self.value() == 'no_notes':
1✔
151
                return queryset.exclude(Exists(notes_subquery))
×
152
        else:
153
            return queryset.all()
×
154

155

156
def _bring_to_front(ids, id):
1✔
157
    if id in ids:
×
158
        ids.remove(id)
×
159
    return [id] + ids
×
160

161

162
# Admin
163
# ------------------------------------------------------------------------------------------------
164
class BaseActionForm(forms.ModelForm):
1✔
165
    def __init__(self, *args, **kwargs):
1✔
166
        super(BaseActionForm, self).__init__(*args, **kwargs)
1✔
167
        if 'users' in self.fields:
1✔
168
            self.fields['users'].queryset = get_user_model().objects.all().order_by('username')
1✔
169
        if 'user' in self.fields:
1✔
170
            self.fields['user'].queryset = get_user_model().objects.all().order_by('username')
1✔
171
        # restricts the subject choices only to managed subjects
172
        if 'subject' in self.fields and not (
1✔
173
                self.current_user.is_stock_manager or self.current_user.is_superuser):
174
            inst = self.instance
1✔
175
            ids = [s.id for s in Subject.objects.filter(responsible_user=self.current_user,
1✔
176
                                                        cull__isnull=True).order_by('nickname')]
177
            if getattr(inst, 'subject', None):
1✔
178
                ids = _bring_to_front(ids, inst.subject.pk)
×
179
            if getattr(self, 'last_subject_id', None):
1✔
180
                ids = _bring_to_front(ids, self.last_subject_id)
×
181
            # These ids first in the list of subjects.
182
            if ids:
1✔
183
                preserved = Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(ids)])
1✔
184
                self.fields['subject'].queryset = Subject.objects.filter(
1✔
185
                    pk__in=ids).order_by(preserved, 'nickname')
186
            else:
187
                self.fields['subject'].queryset = Subject.objects.filter(
×
188
                    cull__isnull=True).order_by('nickname')
189

190

191
class BaseActionAdmin(BaseAdmin):
1✔
192
    fields = ['subject', 'start_time', 'end_time', 'users',
1✔
193
              'location', 'lab', 'procedures', 'narrative']
194
    readonly_fields = ['subject_l']
1✔
195

196
    form = BaseActionForm
1✔
197

198
    def subject_l(self, obj):
1✔
199
        url = get_admin_url(obj.subject)
1✔
200
        return format_html('<a href="{url}">{subject}</a>', subject=obj.subject or '-', url=url)
1✔
201
    subject_l.short_description = 'subject'
1✔
202
    subject_l.admin_order_field = 'subject__nickname'
1✔
203

204
    def projects(self, obj):
1✔
205
        return ', '.join(p.name for p in obj.subject.projects.all())
1✔
206

207
    def _get_last_subject(self, request):
1✔
208
        return getattr(request, 'session', {}).get('last_subject_id', None)
1✔
209

210
    def get_form(self, request, obj=None, **kwargs):
1✔
211
        form = super(BaseActionAdmin, self).get_form(request, obj, **kwargs)
1✔
212
        form.current_user = request.user
1✔
213
        form.last_subject_id = self._get_last_subject(request)
1✔
214
        return form
1✔
215

216
    def change_view(self, request, object_id, extra_context=None, **kwargs):
1✔
217
        context = extra_context or {}
1✔
218
        context = _pass_narrative_templates(context)
1✔
219
        return super(BaseActionAdmin, self).change_view(
1✔
220
            request, object_id, extra_context=context, **kwargs)
221

222
    def add_view(self, *args, extra_context=None):
1✔
223
        context = extra_context or {}
1✔
224
        context = _pass_narrative_templates(context)
1✔
225
        return super(BaseActionAdmin, self).add_view(*args, extra_context=context)
1✔
226

227
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
1✔
228
        # Logged-in user by default.
229
        if db_field.name == 'user':
1✔
230
            kwargs['initial'] = request.user
1✔
231
        if db_field.name == 'subject':
1✔
232
            subject_id = self._get_last_subject(request)
1✔
233
            if subject_id:
1✔
234
                subject = Subject.objects.filter(id=subject_id).first()
1✔
235
                if subject:
1✔
236
                    kwargs['initial'] = subject
1✔
237
        return super(BaseActionAdmin, self).formfield_for_foreignkey(
1✔
238
            db_field, request, **kwargs
239
        )
240

241
    def formfield_for_manytomany(self, db_field, request, **kwargs):
1✔
242
        # Logged-in user by default.
243
        if db_field.name == 'users':
1✔
244
            kwargs['initial'] = [request.user]
1✔
245
        return super(BaseActionAdmin, self).formfield_for_manytomany(
1✔
246
            db_field, request, **kwargs
247
        )
248

249
    def save_model(self, request, obj, form, change):
1✔
250
        subject = getattr(obj, 'subject', None)
×
251
        if subject:
×
252
            getattr(request, 'session', {})['last_subject_id'] = subject.id.hex
×
253
        super(BaseActionAdmin, self).save_model(request, obj, form, change)
×
254

255

256
class OtherActionAdmin(BaseActionAdmin):
1✔
257
    list_display = ['subject_l', 'start_time', 'procedures_l', 'users_l', 'narrative', 'projects']
1✔
258
    list_select_related = ('subject',)
1✔
259
    ordering = ('-start_time', 'subject__nickname')
1✔
260
    search_fields = ['subject__nickname', 'subject__projects__name']
1✔
261
    list_filter = [ResponsibleUserListFilter,
1✔
262
                   ('subject', RelatedDropdownFilter),
263
                   ('users', RelatedDropdownFilter),
264
                   ('start_time', DateRangeFilter),
265
                   ('end_time', DateRangeFilter),
266
                   ]
267

268
    def users_l(self, obj):
1✔
269
        return ', '.join(map(str, obj.users.all()))
×
270

271
    def procedures_l(self, obj):
1✔
272
        return ', '.join(map(str, obj.procedures.all()))
×
273

274
    users_l.short_description = 'users'
1✔
275
    procedures_l.short_description = 'proformed procedures'
1✔
276

277

278
class ProcedureTypeAdmin(BaseActionAdmin):
1✔
279
    fields = ['name', 'description']
1✔
280
    ordering = ['name']
1✔
281

282

283
class WaterAdministrationForm(forms.ModelForm):
1✔
284
    def __init__(self, *args, **kwargs):
1✔
285
        super(WaterAdministrationForm, self).__init__(*args, **kwargs)
1✔
286
        # Show subjects that are on water restriction first.
287
        qs = (WaterRestriction
1✔
288
              .objects
289
              .select_related('subject')
290
              .filter(
291
                  start_time__isnull=False, end_time__isnull=True, subject__death_date__isnull=True
292
              )
293
              .order_by('subject__nickname'))
294
        ids = list(qs.values_list('subject', flat=True))
1✔
295
        if getattr(self, 'last_subject_id', None):
1✔
296
            ids = [self.last_subject_id] + ids
1✔
297
        # These ids first in the list of subjects, if any ids
298
        if not self.fields:
1✔
299
            return
×
300
        if ids:
1✔
301
            preserved = Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(ids)])
1✔
302
            subjects = Subject.objects.order_by(preserved, 'nickname')
1✔
303
        else:
304
            subjects = Subject.objects.order_by('nickname')
1✔
305
        # filters the subjects by the current user: responsible users, allowed users
306
        self.fields['subject'].queryset = self.current_user.get_allowed_subjects(subjects)
1✔
307
        self.fields['user'].queryset = get_user_model().objects.all().order_by('username')
1✔
308
        self.fields['water_administered'].widget.attrs.update({'autofocus': 'autofocus'})
1✔
309

310
    def clean(self):
1✔
311
        cleaned_data = super(WaterAdministrationForm, self).clean()
1✔
312
        if cleaned_data['subject']:
1✔
313
            death_date = cleaned_data['subject'].death_date
1✔
314
            date_time = cleaned_data['date_time']
1✔
315
            if death_date and date_time and death_date < date_time.date():
1✔
316
                self.add_error('date_time', 'Date must be before subject death date.')
1✔
317
        return cleaned_data
1✔
318

319

320
class WaterAdministrationAdmin(BaseActionAdmin):
1✔
321
    form = WaterAdministrationForm
1✔
322

323
    fields = ['subject', 'date_time', 'water_administered', 'water_type', 'adlib', 'user',
1✔
324
              'session_l']
325
    list_display = ['subject_l', 'water_administered', 'user', 'date_time', 'water_type',
1✔
326
                    'adlib', 'session_l', 'projects']
327
    list_display_links = ('water_administered', )
1✔
328
    list_select_related = ('subject', 'user')
1✔
329
    ordering = ['-date_time', 'subject__nickname']
1✔
330
    search_fields = ['subject__nickname', 'subject__projects__name']
1✔
331
    list_filter = [ResponsibleUserListFilter, ('subject', RelatedDropdownFilter)]
1✔
332
    readonly_fields = ['session_l', ]
1✔
333

334
    def session_l(self, obj):
1✔
335
        url = get_admin_url(obj.session)
1✔
336
        return format_html('<a href="{url}">{session}</a>', session=obj.session or '-', url=url)
1✔
337
    session_l.short_description = 'Session'
1✔
338
    session_l.allow_tags = True
1✔
339

340

341
class WaterRestrictionForm(forms.ModelForm):
1✔
342

343
    class Meta:
1✔
344
        model = WaterRestriction
1✔
345
        fields = '__all__'
1✔
346

347

348
class WaterRestrictionAdmin(BaseActionAdmin):
1✔
349
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
1✔
350
        if db_field.name == 'subject':
1✔
351
            obj = None
1✔
352
            kwargs['queryset'] = Subject.objects.filter(cull__isnull=True).order_by('nickname')
1✔
353
            # here if the form is of an existing water restriction, get the subject
354
            if request.resolver_match is not None:
1✔
355
                object_id = request.resolver_match.kwargs.get('object_id')
×
356
                obj = self.get_object(request, object_id) if object_id else None
×
357
            if obj is not None:
1✔
358
                kwargs['queryset'] = (kwargs['queryset'] | Subject.objects.filter(pk=obj.subject.pk)).order_by('nickname')
×
359
                kwargs['initial'] = obj.subject
×
360
            else:
361
                subject_id = self._get_last_subject(request)
1✔
362
                if subject_id:
1✔
363
                    subject = Subject.objects.get(id=subject_id)
×
364
                    kwargs['initial'] = subject
×
365
        return super(BaseActionAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
1✔
366

367
    def get_form(self, request, obj=None, **kwargs):
1✔
368
        form = super(WaterRestrictionAdmin, self).get_form(request, obj, **kwargs)
1✔
369
        subject = getattr(obj, 'subject', None)
1✔
370
        rw = subject.water_control.weight() if subject else None
1✔
371
        if self.has_change_permission(request, obj):
1✔
372
            form.base_fields['reference_weight'].initial = rw or 0
1✔
373
        return form
1✔
374

375
    form = WaterRestrictionForm
1✔
376

377
    fields = ['subject', 'reference_weight', 'start_time',
1✔
378
              'end_time', 'water_type', 'users', 'narrative', 'implant_weight']
379
    list_display = ('subject_w', 'start_time_l', 'end_time_l', 'water_type', 'weight',
1✔
380
                    'weight_ref') + WaterControl._columns[3:] + ('projects',)
381
    list_select_related = ('subject',)
1✔
382
    list_display_links = ('start_time_l', 'end_time_l')
1✔
383
    readonly_fields = ('weight', 'implant_weight')  # WaterControl._columns[1:]
1✔
384
    ordering = ['-start_time', 'subject__nickname']
1✔
385
    search_fields = ['subject__nickname', 'subject__projects__name']
1✔
386
    list_filter = [ResponsibleUserListFilter,
1✔
387
                   ('subject', RelatedDropdownFilter),
388
                   ActiveFilter,
389
                   ]
390

391
    def subject_w(self, obj):
1✔
392
        url = reverse('water-history', kwargs={'subject_id': obj.subject.id})
1✔
393
        return format_html('<a href="{url}">{name}</a>', url=url, name=obj.subject.nickname)
1✔
394
    subject_w.short_description = 'subject'
1✔
395
    subject_w.admin_order_field = 'subject'
1✔
396

397
    def start_time_l(self, obj):
1✔
398
        return obj.start_time.date()
1✔
399
    start_time_l.short_description = 'start date'
1✔
400
    start_time_l.admin_order_field = 'start_time'
1✔
401

402
    def end_time_l(self, obj):
1✔
403
        if obj.end_time:
1✔
404
            return obj.end_time.date()
1✔
405
        else:
406
            return obj.end_time
1✔
407
    end_time_l.short_description = 'end date'
1✔
408
    end_time_l.admin_order_field = 'end_time'
1✔
409

410
    def weight(self, obj):
1✔
411
        if not obj.subject:
1✔
412
            return
×
413
        return '%.1f' % obj.subject.water_control.weight()
1✔
414
    weight.short_description = 'weight'
1✔
415

416
    def weight_ref(self, obj):
1✔
417
        if not obj.subject:
1✔
418
            return
×
419
        return '%.1f' % obj.subject.water_control.reference_weight()
1✔
420

421
    def expected_weight(self, obj):
1✔
422
        if not obj.subject:
1✔
423
            return
×
424
        return '%.1f' % obj.subject.water_control.expected_weight()
1✔
425
    expected_weight.short_description = 'weight exp'
1✔
426

427
    def percentage_weight(self, obj):
1✔
428
        if not obj.subject:
1✔
429
            return
×
430
        return '%.1f' % obj.subject.water_control.percentage_weight()
1✔
431
    percentage_weight.short_description = 'weight pct'
1✔
432

433
    def min_weight(self, obj):
1✔
434
        if not obj.subject:
1✔
435
            return
×
436
        return '%.1f' % obj.subject.water_control.min_weight()
1✔
437
    min_weight.short_description = 'weight min'
1✔
438

439
    def given_water_reward(self, obj):
1✔
440
        if not obj.subject:
1✔
441
            return
×
442
        return '%.2f' % obj.subject.water_control.given_water_reward()
1✔
443
    given_water_reward.short_description = 'water reward'
1✔
444

445
    def given_water_supplement(self, obj):
1✔
446
        if not obj.subject:
1✔
447
            return
×
448
        return '%.2f' % obj.subject.water_control.given_water_supplement()
1✔
449
    given_water_supplement.short_description = 'water suppl'
1✔
450

451
    def given_water_total(self, obj):
1✔
452
        if not obj.subject:
1✔
453
            return
×
454
        return '%.2f' % obj.subject.water_control.given_water_total()
1✔
455
    given_water_total.short_description = 'water tot'
1✔
456

457
    def implant_weight(self, obj):
1✔
458
        if not obj.subject:
1✔
459
            return
×
460
        return '%.2f' % (obj.subject.water_control.implant_weight() or 0.)
1✔
461
    implant_weight.short_description = 'implant weight'
1✔
462

463
    def has_change_permission(self, request, obj=None):
1✔
464
        # setting to override edition of water restrictions in the settings.lab file
465
        override = getattr(settings, 'WATER_RESTRICTIONS_EDITABLE', False)
1✔
466
        if override:
1✔
467
            return True
×
468
        else:
469
            return super(WaterRestrictionAdmin, self).has_change_permission(request, obj=obj)
1✔
470

471
    def expected_water(self, obj):
1✔
472
        if not obj.subject:
1✔
473
            return
×
474
        return '%.2f' % obj.subject.water_control.expected_water()
1✔
475
    expected_water.short_description = 'water exp'
1✔
476

477
    def excess_water(self, obj):
1✔
478
        if not obj.subject:
1✔
479
            return
×
480
        return '%.2f' % obj.subject.water_control.excess_water()
1✔
481
    excess_water.short_description = 'water excess'
1✔
482

483
    def is_water_restricted(self, obj):
1✔
484
        return obj.is_active()
1✔
485
    is_water_restricted.short_description = 'is active'
1✔
486
    is_water_restricted.boolean = True
1✔
487

488

489
class WeighingForm(BaseActionForm):
1✔
490
    def __init__(self, *args, **kwargs):
1✔
491
        super(WeighingForm, self).__init__(*args, **kwargs)
1✔
492
        if 'subject' in self.fields:
1✔
493
            # Order by alive subjects first
494
            ids = list(Subject
1✔
495
                       .objects
496
                       .filter(death_date__isnull=True)
497
                       .order_by('nickname')
498
                       .only('pk')
499
                       .values_list('pk', flat=True))
500
        # These ids first in the list of subjects, if any ids
501
        if ids:
1✔
502
            preserved = Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(ids)])
1✔
503
            subjects = Subject.objects.order_by(preserved, 'nickname')
1✔
504
        else:
505
            subjects = Subject.objects.order_by('nickname')
×
506
        self.fields['subject'].queryset = self.current_user.get_allowed_subjects(subjects)
1✔
507

508
        if self.fields.keys():
1✔
509
            self.fields['weight'].widget.attrs.update({'autofocus': 'autofocus'})
1✔
510

511

512
class WeighingAdmin(BaseActionAdmin):
1✔
513
    list_display = ['subject_l', 'weight', 'percentage_weight', 'date_time', 'projects']
1✔
514
    list_select_related = ('subject',)
1✔
515
    fields = ['subject', 'date_time', 'weight', 'user']
1✔
516
    ordering = ('-date_time',)
1✔
517
    list_display_links = ('weight',)
1✔
518
    search_fields = ['subject__nickname', 'subject__projects__name']
1✔
519
    list_filter = [ResponsibleUserListFilter,
1✔
520
                   ('subject', RelatedDropdownFilter)]
521

522
    form = WeighingForm
1✔
523

524
    def percentage_weight(self, obj):
1✔
525
        wc = obj.subject.water_control
1✔
526
        return wc.percentage_weight_html(date=obj.date_time)
1✔
527
    percentage_weight.short_description = 'Weight %'
1✔
528

529

530
class WaterTypeAdmin(BaseActionAdmin):
1✔
531
    list_display = ['name', 'json']
1✔
532
    fields = ['name', 'json']
1✔
533
    ordering = ('name',)
1✔
534
    list_display_links = ('name',)
1✔
535

536

537
class SurgeryAdmin(BaseActionAdmin):
1✔
538
    form = BaseActionForm
1✔
539
    list_display = ['subject_l', 'date', 'users_l', 'procedures_l',
1✔
540
                    'narrative', 'projects', 'implant_weight']
541
    list_select_related = ('subject',)
1✔
542

543
    fields = BaseActionAdmin.fields + ['outcome_type', 'implant_weight']
1✔
544
    list_display_links = ['date']
1✔
545
    search_fields = ('subject__nickname', 'subject__projects__name')
1✔
546
    list_filter = [SubjectAliveListFilter,
1✔
547
                   ResponsibleUserListFilter,
548
                   ('subject__line', RelatedDropdownFilter),
549
                   ]
550
    ordering = ['-start_time']
1✔
551
    inlines = [NoteInline]
1✔
552

553
    def date(self, obj):
1✔
554
        return obj.start_time.date()
1✔
555
    date.admin_order_field = 'start_time'
1✔
556

557
    def users_l(self, obj):
1✔
558
        return ', '.join(map(str, obj.users.all()))
1✔
559
    users_l.short_description = 'users'
1✔
560

561
    def procedures_l(self, obj):
1✔
562
        return ', '.join(map(str, obj.procedures.all()))
1✔
563
    procedures_l.short_description = 'procedures'
1✔
564

565
    def get_queryset(self, request):
1✔
566
        return super(SurgeryAdmin, self).get_queryset(request).prefetch_related(
1✔
567
            'users', 'procedures')
568

569

570
class DatasetInline(BaseInlineAdmin):
1✔
571
    show_change_link = True
1✔
572
    model = Dataset
1✔
573
    extra = 1
1✔
574
    fields = ('name', 'dataset_type', 'collection', '_online', 'version', 'qc',
1✔
575
              'created_by', 'created_datetime')
576
    readonly_fields = fields
1✔
577
    ordering = ('name',)
1✔
578

579
    def _online(self, obj):
1✔
580
        return obj.is_online
1✔
581
    _online.short_description = 'On server'
1✔
582
    _online.boolean = True
1✔
583

584

585
class WaterAdminInline(BaseInlineAdmin):
1✔
586
    model = WaterAdministration
1✔
587
    extra = 0
1✔
588
    fields = ('name', 'water_administered', 'water_type')
1✔
589
    readonly_fields = ('name', 'water_administered', 'water_type')
1✔
590

591

592
class TasksAdminInline(BaseInlineAdmin):
1✔
593
    model = Task
1✔
594
    extra = 0
1✔
595
    fields = ('status', 'name', 'version', 'parents', 'datetime', 'arguments')
1✔
596
    readonly_fields = ('name', 'version', 'parents', 'datetime', 'arguments')
1✔
597
    ordering = ('status',)
1✔
598

599

600
def _pass_narrative_templates(context):
1✔
601
    context['narrative_templates'] = \
1✔
602
        base64.b64encode(json.dumps(settings.NARRATIVE_TEMPLATES).encode('utf-8')).decode('utf-8')
603
    return context
1✔
604

605

606
class SessionAdmin(BaseActionAdmin):
1✔
607
    list_display = ['subject_l', 'start_time', 'number', 'lab', 'dataset_count',
1✔
608
                    'task_protocol', 'qc', 'user_list', 'project_']
609
    list_display_links = ['start_time']
1✔
610
    fields = BaseActionAdmin.fields + [
1✔
611
        'repo_url', 'qc', 'extended_qc', 'projects', ('type', 'task_protocol', ), 'number',
612
        'n_correct_trials', 'n_trials', 'weighing', 'auto_datetime']
613
    list_filter = [('users', RelatedDropdownFilter),
1✔
614
                   ('start_time', DateRangeFilter),
615
                   ('projects', RelatedDropdownFilter),
616
                   ('lab', RelatedDropdownFilter),
617
                   (HasNarrativeFilter),
618
                   (HasNoteFilter),
619
                   ]
620
    search_fields = ('subject__nickname', 'lab__name', 'projects__name', 'users__username',
1✔
621
                     'task_protocol', 'pk')
622
    ordering = ('-start_time', 'task_protocol', 'lab')
1✔
623
    inlines = [WaterAdminInline, TasksAdminInline, DatasetInline, NoteInline]
1✔
624
    readonly_fields = ['repo_url', 'task_protocol', 'weighing', 'qc', 'extended_qc',
1✔
625
                       'auto_datetime']
626

627
    def get_form(self, request, obj=None, **kwargs):
1✔
628
        from subjects.admin import Project
1✔
629
        from django.db.models import Q
1✔
630
        form = super(SessionAdmin, self).get_form(request, obj, **kwargs)
1✔
631
        if form.base_fields and not request.user.is_superuser:
1✔
632
            # the projects edit box is limited to projects with no user or containing current user
633
            current_proj = obj.projects.all() if obj else None
×
634
            form.base_fields['projects'].queryset = Project.objects.filter(
×
635
                Q(users=request.user.pk) | Q(users=None) | Q(pk__in=current_proj)
636
            ).distinct()
637
        return form
1✔
638

639
    def project_(self, obj):
1✔
640
        return [getattr(p, 'name', None) for p in obj.projects.all()]
1✔
641

642
    def repo_url(self, obj):
1✔
643
        url = settings.SESSION_REPO_URL.format(
1✔
644
            lab=obj.subject.lab.name,
645
            subject=obj.subject.nickname,
646
            date=obj.start_time.date(),
647
            number=obj.number or 0,
648
        )
649
        return format_html(
1✔
650
            '<a href="{url}">{url}</a>'.format(url=url))
651

652
    def user_list(self, obj):
1✔
653
        return ', '.join(map(str, obj.users.all()))
1✔
654
    user_list.short_description = 'users'
1✔
655

656
    def dataset_count(self, ses):
1✔
657
        cs = FileRecord.objects.filter(dataset__in=ses.data_dataset_session_related.all(),
1✔
658
                                       data_repository__globus_is_personal=False,
659
                                       exists=True).values_list('relative_path').distinct().count()
660
        cr = FileRecord.objects.filter(dataset__in=ses.data_dataset_session_related.all(),
1✔
661
                                       ).values_list('relative_path').distinct().count()
662
        if cr == 0:
1✔
663
            return '-'
1✔
664
        col = '008000' if cr == cs else '808080'  # green if all files uploaded on server
×
665
        return format_html('<b><a style="color: #{};">{}</a></b>', col, '{:2.0f}'.format(cr))
×
666
    dataset_count.short_description = '# datasets'
1✔
667

668
    def weighing(self, obj):
1✔
669
        wei = Weighing.objects.filter(date_time=obj.start_time)
1✔
670
        if not wei:
1✔
671
            return ''
1✔
672
        url = reverse('admin:%s_%s_change' % (wei[0]._meta.app_label, wei[0]._meta.model_name),
×
673
                      args=[wei[0].id])
674
        return format_html('<b><a href="{url}" ">{} g </a></b>', wei[0].weight, url=url)
×
675
    weighing.short_description = 'weight before session'
1✔
676

677

678
class ProbeInsertionInline(TabularInline):
1✔
679
    fk_name = "session"
1✔
680
    show_change_link = True
1✔
681
    model = ProbeInsertion
1✔
682
    fields = ('name', 'model')
1✔
683
    extra = 0
1✔
684

685

686
class FOVInline(TabularInline):
1✔
687
    fk_name = 'session'
1✔
688
    show_change_link = True
1✔
689
    model = FOV
1✔
690
    fields = ('name', 'imaging_type')
1✔
691
    extra = 0
1✔
692

693

694
class EphysSessionAdmin(SessionAdmin):
1✔
695
    inlines = [ProbeInsertionInline, TasksAdminInline, WaterAdminInline, DatasetInline, NoteInline]
1✔
696

697
    def get_queryset(self, request):
1✔
698
        qs = super(EphysSessionAdmin, self).get_queryset(request)
1✔
699
        return qs.filter(procedures__name__icontains='ephys')
1✔
700

701

702
class ImagingSessionAdmin(SessionAdmin):
1✔
703
    inlines = [FOVInline, TasksAdminInline, WaterAdminInline, DatasetInline, NoteInline]
1✔
704
    list_filter = [('users', RelatedDropdownFilter),
1✔
705
                   ('start_time', DateRangeFilter),
706
                   ('projects', RelatedDropdownFilter),
707
                   ('lab', RelatedDropdownFilter),
708
                   ('field_of_view__imaging_type', RelatedDropdownFilter)]
709

710
    def get_queryset(self, request):
1✔
711
        qs = super(ImagingSessionAdmin, self).get_queryset(request)
1✔
712
        return qs.filter(procedures__name__icontains='imaging')
1✔
713

714

715
class NotificationUserFilter(DefaultListFilter):
1✔
716
    title = 'notification users'
1✔
717
    parameter_name = 'users'
1✔
718

719
    def lookups(self, request, model_admin):
1✔
720
        return (
1✔
721
            (None, 'Me'),
722
            ('all', 'All'),
723
        )
724

725
    def queryset(self, request, queryset):
1✔
726
        if self.value() is None:
1✔
727
            return queryset.filter(users__in=[request.user])
1✔
728
        elif self.value == 'all':
×
729
            return queryset.all()
×
730

731

732
class NotificationAdmin(BaseAdmin):
1✔
733
    list_display = ('title', 'subject', 'users_l',
1✔
734
                    'send_at', 'sent_at',
735
                    'status', 'notification_type')
736
    search_fields = ('notification_type', 'subject__nickname', 'title')
1✔
737
    list_filter = (NotificationUserFilter, 'notification_type')
1✔
738
    fields = ('title', 'notification_type', 'subject', 'message',
1✔
739
              'users', 'status', 'send_at', 'sent_at')
740
    ordering = ('-send_at',)
1✔
741

742
    def users_l(self, obj):
1✔
743
        return sorted(map(str, obj.users.all()))
×
744

745

746
class NotificationRuleAdmin(BaseAdmin):
1✔
747
    list_display = ('notification_type', 'user', 'subjects_scope')
1✔
748
    search_fields = ('notification_type', 'user__username', 'subject_scope')
1✔
749
    fields = ('notification_type', 'user', 'subjects_scope')
1✔
750

751
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
1✔
752
        if db_field.name == 'user':
1✔
753
            kwargs['initial'] = request.user.id
1✔
754
        return super(NotificationRuleAdmin, self).formfield_for_foreignkey(
1✔
755
            db_field, request, **kwargs
756
        )
757

758

759
class CullAdmin(BaseAdmin):
1✔
760
    list_display = ('date', 'subject_l', 'user', 'cull_reason', 'cull_method', 'projects')
1✔
761
    search_fields = ('user__username', 'subject__nickname', 'subject__projects__name')
1✔
762
    fields = ('date', 'subject', 'user', 'cull_reason', 'cull_method', 'description')
1✔
763
    ordering = ('-date',)
1✔
764

765
    def subject_l(self, obj):
1✔
766
        url = get_admin_url(obj.subject)
×
767
        return format_html('<a href="{url}">{subject}</a>', subject=obj.subject or '-', url=url)
×
768
    subject_l.short_description = 'subject'
1✔
769

770
    def projects(self, obj):
1✔
771
        return ', '.join(p.name for p in obj.subject.projects.all())
×
772

773

774
admin.site.register(ProcedureType, ProcedureTypeAdmin)
1✔
775
admin.site.register(Weighing, WeighingAdmin)
1✔
776
admin.site.register(WaterAdministration, WaterAdministrationAdmin)
1✔
777
admin.site.register(WaterRestriction, WaterRestrictionAdmin)
1✔
778

779
admin.site.register(Session, SessionAdmin)
1✔
780
admin.site.register(EphysSession, EphysSessionAdmin)
1✔
781
admin.site.register(ImagingSession, ImagingSessionAdmin)
1✔
782
admin.site.register(OtherAction, OtherActionAdmin)
1✔
783
admin.site.register(VirusInjection, BaseActionAdmin)
1✔
784

785
admin.site.register(Surgery, SurgeryAdmin)
1✔
786
admin.site.register(WaterType, WaterTypeAdmin)
1✔
787

788
admin.site.register(Notification, NotificationAdmin)
1✔
789
admin.site.register(NotificationRule, NotificationRuleAdmin)
1✔
790

791
admin.site.register(Cull, CullAdmin)
1✔
792
admin.site.register(CullReason, BaseAdmin)
1✔
793
admin.site.register(CullMethod, BaseAdmin)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc