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

cortex-lab / alyx / 13546875483

26 Feb 2025 03:01PM UTC coverage: 84.16% (-0.02%) from 84.184%
13546875483

push

github

web-flow
Feat/notes and narrative (#897)

Resolves issue #642

* Add Notes list view to 'Common' section

* add filterset for notes

* add 'has narrative' filters to Session list view

* remove superfluous calls of filter()

* use regex for filtering narrative

* add 'has_note' filter  to Session list view

* include ContentType with HasNoteFilter / clean-up

* use `\s` instead of ` ` in `HasNarrativeFilter` regex

* flake8

7821 of 9293 relevant lines covered (84.16%)

0.84 hits per line

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

81.31
alyx/misc/admin.py
1
from pytz import all_timezones
1✔
2

3
from django import forms
1✔
4
from django.db import models
1✔
5
from django.db.models import Q
1✔
6
from django.contrib import admin
1✔
7
from django.contrib.admin.widgets import AdminFileWidget
1✔
8
from django.contrib.contenttypes.admin import GenericTabularInline
1✔
9
from django.contrib.postgres.fields import JSONField
1✔
10
from django.utils.html import format_html, format_html_join
1✔
11
from django.utils.safestring import mark_safe
1✔
12
from django_admin_listfilter_dropdown.filters import RelatedDropdownFilter
1✔
13
from rest_framework.authtoken.models import TokenProxy
1✔
14
from rangefilter.filters import DateRangeFilter
1✔
15

16
from misc.models import Note, Lab, LabMembership, LabLocation, CageType, \
1✔
17
    Enrichment, Food, Housing, HousingSubject
18
from alyx.base import BaseAdmin, DefaultListFilter, get_admin_url
1✔
19

20

21
class LabForm(forms.ModelForm):
1✔
22
    def __init__(self, *args, **kwargs):
1✔
23
        super(LabForm, self).__init__(*args, **kwargs)
1✔
24
        # if user has read-only permissions only fields is empty
25
        if not self.is_bound:
1✔
26
            return
1✔
27
        self.fields['reference_weight_pct'].help_text =\
×
28
            'Threshold ratio triggers a warning using the Reference Weight method (0-1)'
29
        self.fields['reference_weight_pct'].label = 'Reference Weight Ratio'
×
30
        self.fields['zscore_weight_pct'].help_text =\
×
31
            'Threshold ratio triggers a warning is raised using the Z-Score method (0-1)'
32
        self.fields['zscore_weight_pct'].label = 'Z-score Weight Ratio'
×
33

34
    def clean_reference_weight_pct(self):
1✔
35
        ref = self.cleaned_data['reference_weight_pct']
×
36
        ref = max(ref, 0)
×
37
        if ref > 1:
×
38
            ref = ref / 100
×
39
        return ref
×
40

41
    def clean_zscore_weight_pct(self):
1✔
42
        ref = self.cleaned_data['zscore_weight_pct']
×
43
        ref = max(ref, 0)
×
44
        if ref > 1:
×
45
            ref = ref / 100
×
46
        return ref
×
47

48
    def clean_timezone(self):
1✔
49
        ref = self.cleaned_data['timezone']
×
50
        if ref not in all_timezones:
×
51
            raise forms.ValidationError(
×
52
                ("Time Zone is incorrect here is the list (column TZ Database Name):  "
53
                 "https://en.wikipedia.org/wiki/List_of_tz_database_time_zones"))
54
        return ref
×
55

56

57
class LabAdmin(BaseAdmin):
1✔
58
    form = LabForm
1✔
59
    generics = ['name', 'institution', 'address', 'timezone',
1✔
60
                'reference_weight_pct', 'zscore_weight_pct']
61
    list_display = ['name', 'institution', 'address', 'timezone', 'local', 'server',
1✔
62
                    'reference_weight_pct', 'zscore_weight_pct']
63
    list_select_related = ['cage_type', 'enrichment', 'food']
1✔
64
    fields = generics + list_select_related + ['cage_cleaning_frequency_days', 'light_cycle',
1✔
65
                                               'repositories']
66

67
    def local(self, obj):
1✔
68
        return ','.join([p.name for p in obj.repositories.filter(globus_is_personal=True)])
1✔
69

70
    def server(self, obj):
1✔
71
        return ','.join([p.name for p in obj.repositories.filter(globus_is_personal=False)])
1✔
72

73

74
class LabMembershipAdmin(BaseAdmin):
1✔
75
    fields = ['user', 'lab', 'role', 'start_date', 'end_date']
1✔
76
    list_display = fields
1✔
77

78

79
class LabLocationAdmin(BaseAdmin):
1✔
80
    fields = ['name', 'lab']
1✔
81
    list_display = fields
1✔
82
    search_fields = ('lab__name', 'name',)
1✔
83
    ordering = ('lab__name', 'name',)
1✔
84

85

86
class AdminImageWidget(AdminFileWidget):
1✔
87

88
    def render(self, name, value, attrs=None, renderer=None):
1✔
89
        output = []
1✔
90
        if value and getattr(value, "url", None):
1✔
91
            image_url = value.url
×
92
            file_name = str(value)
×
93
            output.append(f'<a href="{image_url}" target="_blank">'
×
94
                          f'<img src="{image_url}" width="400" alt="{file_name}" /></a><br>')
95
        output.append(super(AdminFileWidget, self).render(name, value, attrs, renderer))
1✔
96
        return mark_safe(''.join(output))
1✔
97

98

99
class ImageWidgetAdmin(BaseAdmin):
1✔
100
    image_fields = []
1✔
101

102
    def formfield_for_dbfield(self, db_field, **kwargs):
1✔
103
        if db_field.name in self.image_fields:
1✔
104
            request = kwargs.pop("request", None)  # noqa
1✔
105
            kwargs['widget'] = AdminImageWidget
1✔
106
            return db_field.formfield(**kwargs)
1✔
107
        return super(ImageWidgetAdmin, self).formfield_for_dbfield(db_field, **kwargs)
1✔
108

109

110
class HasImageFilter(DefaultListFilter):
1✔
111
    title = 'image'
1✔
112
    parameter_name = 'has_image'
1✔
113

114
    def lookups(self, request, model_admin):
1✔
115
        return (
1✔
116
            (None, 'All'),
117
            ('image', 'Has image'),
118
            ('no_image', 'No image'),
119
        )
120

121
    def queryset(self, request, queryset):
1✔
122
        if self.value() == 'image':
1✔
123
            return queryset.exclude(image__exact='')
×
124
        if self.value() == 'no_image':
1✔
125
            return queryset.filter(image__exact='')
×
126
        elif self.value is None:
1✔
127
            return queryset.all()
×
128

129

130
class NoteAdmin(ImageWidgetAdmin):
1✔
131
    list_display = ['user', 'date_time', 'content_object', 'text', 'image']
1✔
132
    list_display_links = ['date_time']
1✔
133
    list_filter = [('user', RelatedDropdownFilter),
1✔
134
                   ('date_time', DateRangeFilter),
135
                   ('content_type', RelatedDropdownFilter),
136
                   (HasImageFilter),
137
                   ]
138
    image_fields = ['image']
1✔
139
    fields = ['user', 'date_time', 'text', 'image', 'content_type', 'object_id']
1✔
140
    ordering = ('-date_time',)
1✔
141
    search_fields = ['text']
1✔
142

143

144
class NoteInline(GenericTabularInline):
1✔
145
    model = Note
1✔
146
    extra = 1
1✔
147
    fields = ('user', 'date_time', 'text', 'image')
1✔
148
    image_fields = ('image',)
1✔
149
    ordering = ('-date_time',)
1✔
150

151
    formfield_overrides = {
1✔
152
        models.TextField: {'widget': forms.Textarea(
153
                           attrs={'rows': 3,
154
                                  'cols': 30})},
155
        JSONField: {'widget': forms.Textarea(
156
                    attrs={'rows': 3,
157
                           'cols': 30})},
158
        models.CharField: {'widget': forms.TextInput(attrs={'size': 16})},
159
    }
160

161
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
1✔
162
        # Logged-in user by default.
163
        if db_field.name == 'user':
1✔
164
            kwargs['initial'] = request.user
1✔
165
        return super(NoteInline, self).formfield_for_foreignkey(
1✔
166
            db_field, request, **kwargs
167
        )
168

169
    def formfield_for_dbfield(self, db_field, **kwargs):
1✔
170
        if db_field.name in self.image_fields:
1✔
171
            request = kwargs.pop("request", None)  # noqa
1✔
172
            kwargs['widget'] = AdminImageWidget
1✔
173
            return db_field.formfield(**kwargs)
1✔
174
        return super(NoteInline, self).formfield_for_dbfield(db_field, **kwargs)
1✔
175

176
    def has_delete_permission(self, request, obj=None):
1✔
177
        return False
1✔
178

179

180
class CageTypeAdmin(BaseAdmin):
1✔
181
    fields = ('name', 'description',)
1✔
182
    list_display = fields
1✔
183
    search_fields = ('name',)
1✔
184

185

186
class EnrichmentAdmin(BaseAdmin):
1✔
187
    fields = ('name', 'description',)
1✔
188
    list_display = fields
1✔
189
    search_fields = ('name',)
1✔
190

191

192
class FoodAdmin(BaseAdmin):
1✔
193
    fields = ('name', 'description',)
1✔
194
    list_display = fields
1✔
195
    search_fields = ('name',)
1✔
196

197

198
class HousingSubjectAdminInline(admin.TabularInline):
1✔
199
    model = HousingSubject
1✔
200
    extra = 1
1✔
201
    fields = ('subject', 'start_datetime', 'end_datetime')
1✔
202

203
    def get_queryset(self, request):
1✔
204
        qs = super(HousingSubjectAdminInline, self).get_queryset(request)
1✔
205
        return qs.filter(subject__cull__isnull=True)
1✔
206

207

208
class HousingAdminForm(forms.ModelForm):
1✔
209

210
    def __init__(self, *args, **kwargs):
1✔
211
        super().__init__(*args, **kwargs)
1✔
212
        request = self.Meta.formfield_callback.keywords['request']
1✔
213
        self.user = request.user
1✔
214
        lab = self.user.lab_id()
1✔
215
        if lab:
1✔
216
            self.fields['cage_type'].initial = lab[0].cage_type
×
217
            self.fields['enrichment'].initial = lab[0].enrichment
×
218
            self.fields['light_cycle'].initial = lab[0].light_cycle
×
219
            self.fields['food'].initial = lab[0].food
×
220
            self.fields['cage_cleaning_frequency_days'].initial =\
×
221
                lab[0].cage_cleaning_frequency_days
222

223
    class Meta():
1✔
224
        model = Housing
1✔
225
        fields = ['cage_type', 'cage_name',
1✔
226
                  'enrichment', 'light_cycle',
227
                  'food', 'cage_cleaning_frequency_days']
228

229

230
class HousingIsCurrentFilter(DefaultListFilter):
1✔
231
    title = 'Housing Current'
1✔
232
    parameter_name = 'Housing Current'
1✔
233

234
    def lookups(self, request, model_admin):
1✔
235
        return (
1✔
236
            (None, 'Current'),
237
            ('All', 'All'),
238
        )
239

240
    def queryset(self, request, queryset):
1✔
241
        if self.value() is None:
1✔
242
            return queryset.exclude(nsubs=0)
1✔
243

244

245
class HousingAdmin(BaseAdmin):
1✔
246

247
    inlines = [HousingSubjectAdminInline]
1✔
248
    form = HousingAdminForm
1✔
249

250
    fields = ['subjects_l',
1✔
251
              ('cage_type', 'cage_name'),
252
              ('enrichment', 'light_cycle',),
253
              ('food', 'cage_cleaning_frequency_days')]
254
    search_fields = ('housing_subjects__subject__nickname', 'housing_subjects__subject__lab__name')
1✔
255
    list_display = ('cage_l', 'subjects_l', 'subjects_old', 'start',
1✔
256
                    'end', 'subjects_count', 'lab',)
257
    readonly_fields = ('subjects_l',)
1✔
258
    list_filter = (HousingIsCurrentFilter,)
1✔
259

260
    def get_queryset(self, request):
1✔
261
        qs = Housing.objects.annotate(
1✔
262
            nsubs=models.Count('housing_subjects',
263
                               filter=Q(housing_subjects__end_datetime__isnull=True),
264
                               distinct=True))
265
        qs = qs.annotate(start_datetime=models.Min('housing_subjects__start_datetime'))
1✔
266
        qs = qs.annotate(end_datetime=models.Case(models.When(
1✔
267
            nsubs=0, then=models.Max('housing_subjects__end_datetime')),
268
            output_field=models.DateTimeField(),))
269
        return qs
1✔
270

271
    def start(self, obj):
1✔
272
        return obj.start_datetime
×
273

274
    def end(self, obj):
1✔
275
        return obj.end_datetime
×
276

277
    def subjects_count(self, obj):
1✔
278
        return obj.nsubs
×
279
    subjects_count.short_description = '# active'
1✔
280

281
    def cage_l(self, obj):
1✔
282
        return format_html('<a href="{url}">{hou}</a>', url=get_admin_url(obj), hou=obj.cage_name)
×
283
    cage_l.short_description = 'cage'
1✔
284

285
    def subjects_l(self, obj):
1✔
286
        out = format_html_join(', ', '<a href="{}">{}</a>',
1✔
287
                               ((get_admin_url(sub), sub) for sub in obj.subjects_current()))
288
        return format_html(out)
×
289
    subjects_l.short_description = 'subjects'
1✔
290

291
    def subjects_old(self, obj):
1✔
292
        subs = obj.subjects.exclude(pk__in=obj.subjects_current().values_list('pk', flat=True))
×
293
        out = format_html_join(', ', '<a href="{}">{}</a>',
×
294
                               ((get_admin_url(sub), sub) for sub in subs))
295
        return format_html(out)
×
296
    subjects_old.short_description = 'old subjects'
1✔
297

298

299
admin.site.register(Housing, HousingAdmin)
1✔
300
admin.site.register(Lab, LabAdmin)
1✔
301
admin.site.register(LabMembership, LabMembershipAdmin)
1✔
302
admin.site.register(LabLocation, LabLocationAdmin)
1✔
303
admin.site.register(Note, NoteAdmin)
1✔
304
admin.site.register(CageType, CageTypeAdmin)
1✔
305
admin.site.register(Enrichment, EnrichmentAdmin)
1✔
306
admin.site.register(Food, FoodAdmin)
1✔
307
admin.site.unregister(TokenProxy)
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