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

cortex-lab / alyx / 19134521230

06 Nov 2025 11:42AM UTC coverage: 85.574% (+0.2%) from 85.337%
19134521230

push

github

web-flow
Timezone parameter in docker (#937)

* Timezone parameter in docker

- This allows users to change the timezone without requiring model migrations.
- This commit itself will require a model migration
- Containers by default would be in UTC
- Users can override Alyx timezone although logs etc. will remain UTC

* Initial form value

* Add migrations

* Admin form tests

* Correct settings import

8340 of 9746 relevant lines covered (85.57%)

0.86 hits per line

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

89.71
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.conf import settings
1✔
7
from django.contrib import admin
1✔
8
from django.contrib.admin.widgets import AdminFileWidget
1✔
9
from django.contrib.contenttypes.admin import GenericTabularInline
1✔
10
from django.contrib.postgres.fields import JSONField
1✔
11
from django.utils.html import format_html, format_html_join
1✔
12
from django.utils.safestring import mark_safe
1✔
13
from django_admin_listfilter_dropdown.filters import RelatedDropdownFilter
1✔
14
from rest_framework.authtoken.models import TokenProxy
1✔
15
from rangefilter.filters import DateRangeFilter
1✔
16

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

21

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

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

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

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

57

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

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

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

74
    def get_form(self, request, obj=None, **kwargs):
1✔
75
        form = super().get_form(request, obj, **kwargs)
1✔
76
        if not obj:
1✔
77
            form.base_fields['timezone'].initial = settings.TIME_ZONE
1✔
78
        return form
1✔
79

80

81
class LabMembershipAdmin(BaseAdmin):
1✔
82
    fields = ['user', 'lab', 'role', 'start_date', 'end_date']
1✔
83
    list_display = fields
1✔
84

85

86
class LabLocationAdmin(BaseAdmin):
1✔
87
    fields = ['name', 'lab']
1✔
88
    list_display = fields
1✔
89
    search_fields = ('lab__name', 'name',)
1✔
90
    ordering = ('lab__name', 'name',)
1✔
91

92

93
class AdminImageWidget(AdminFileWidget):
1✔
94

95
    def render(self, name, value, attrs=None, renderer=None):
1✔
96
        output = []
1✔
97
        if value and getattr(value, "url", None):
1✔
98
            image_url = value.url
×
99
            file_name = str(value)
×
100
            output.append(f'<a href="{image_url}" target="_blank">'
×
101
                          f'<img src="{image_url}" width="400" alt="{file_name}" /></a><br>')
102
        output.append(super(AdminFileWidget, self).render(name, value, attrs, renderer))
1✔
103
        return mark_safe(''.join(output))
1✔
104

105

106
class ImageWidgetAdmin(BaseAdmin):
1✔
107
    image_fields = []
1✔
108

109
    def formfield_for_dbfield(self, db_field, **kwargs):
1✔
110
        if db_field.name in self.image_fields:
1✔
111
            request = kwargs.pop("request", None)  # noqa
1✔
112
            kwargs['widget'] = AdminImageWidget
1✔
113
            return db_field.formfield(**kwargs)
1✔
114
        return super(ImageWidgetAdmin, self).formfield_for_dbfield(db_field, **kwargs)
1✔
115

116

117
class HasImageFilter(DefaultListFilter):
1✔
118
    title = 'image'
1✔
119
    parameter_name = 'has_image'
1✔
120

121
    def lookups(self, request, model_admin):
1✔
122
        return (
1✔
123
            (None, 'All'),
124
            ('image', 'Has image'),
125
            ('no_image', 'No image'),
126
        )
127

128
    def queryset(self, request, queryset):
1✔
129
        if self.value() == 'image':
1✔
130
            return queryset.exclude(image__exact='')
×
131
        if self.value() == 'no_image':
1✔
132
            return queryset.filter(image__exact='')
×
133
        elif self.value is None:
1✔
134
            return queryset.all()
×
135

136

137
class NoteAdmin(ImageWidgetAdmin):
1✔
138
    list_display = ['user', 'date_time', 'content_object', 'text', 'image']
1✔
139
    list_display_links = ['date_time']
1✔
140
    list_filter = [('user', RelatedDropdownFilter),
1✔
141
                   ('date_time', DateRangeFilter),
142
                   ('content_type', RelatedDropdownFilter),
143
                   (HasImageFilter),
144
                   ]
145
    image_fields = ['image']
1✔
146
    fields = ['user', 'date_time', 'text', 'image', 'content_type', 'object_id']
1✔
147
    ordering = ('-date_time',)
1✔
148
    search_fields = ['text']
1✔
149

150

151
class NoteInline(GenericTabularInline):
1✔
152
    model = Note
1✔
153
    extra = 1
1✔
154
    fields = ('user', 'date_time', 'text', 'image')
1✔
155
    image_fields = ('image',)
1✔
156
    ordering = ('-date_time',)
1✔
157

158
    formfield_overrides = {
1✔
159
        models.TextField: {'widget': forms.Textarea(
160
                           attrs={'rows': 3,
161
                                  'cols': 30})},
162
        JSONField: {'widget': forms.Textarea(
163
                    attrs={'rows': 3,
164
                           'cols': 30})},
165
        models.CharField: {'widget': forms.TextInput(attrs={'size': 16})},
166
    }
167

168
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
1✔
169
        # Logged-in user by default.
170
        if db_field.name == 'user':
1✔
171
            kwargs['initial'] = request.user
1✔
172
        return super(NoteInline, self).formfield_for_foreignkey(
1✔
173
            db_field, request, **kwargs
174
        )
175

176
    def formfield_for_dbfield(self, db_field, **kwargs):
1✔
177
        if db_field.name in self.image_fields:
1✔
178
            request = kwargs.pop("request", None)  # noqa
1✔
179
            kwargs['widget'] = AdminImageWidget
1✔
180
            return db_field.formfield(**kwargs)
1✔
181
        return super(NoteInline, self).formfield_for_dbfield(db_field, **kwargs)
1✔
182

183
    def has_delete_permission(self, request, obj=None):
1✔
184
        return False
1✔
185

186

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

192

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

198

199
class FoodAdmin(BaseAdmin):
1✔
200
    fields = ('name', 'description',)
1✔
201
    list_display = fields
1✔
202
    search_fields = ('name',)
1✔
203

204

205
class HousingSubjectAdminInline(admin.TabularInline):
1✔
206
    model = HousingSubject
1✔
207
    extra = 1
1✔
208
    fields = ('subject', 'start_datetime', 'end_datetime')
1✔
209

210
    def get_queryset(self, request):
1✔
211
        qs = super(HousingSubjectAdminInline, self).get_queryset(request)
1✔
212
        return qs.filter(subject__cull__isnull=True)
1✔
213

214

215
class HousingAdminForm(forms.ModelForm):
1✔
216

217
    def __init__(self, *args, **kwargs):
1✔
218
        super().__init__(*args, **kwargs)
1✔
219
        request = self.Meta.formfield_callback.keywords['request']
1✔
220
        self.user = request.user
1✔
221
        lab = self.user.lab_id()
1✔
222
        if lab:
1✔
223
            self.fields['cage_type'].initial = lab[0].cage_type
×
224
            self.fields['enrichment'].initial = lab[0].enrichment
×
225
            self.fields['light_cycle'].initial = lab[0].light_cycle
×
226
            self.fields['food'].initial = lab[0].food
×
227
            self.fields['cage_cleaning_frequency_days'].initial =\
×
228
                lab[0].cage_cleaning_frequency_days
229

230
    class Meta():
1✔
231
        model = Housing
1✔
232
        fields = ['cage_type', 'cage_name',
1✔
233
                  'enrichment', 'light_cycle',
234
                  'food', 'cage_cleaning_frequency_days']
235

236

237
class HousingIsCurrentFilter(DefaultListFilter):
1✔
238
    title = 'Housing Current'
1✔
239
    parameter_name = 'Housing Current'
1✔
240

241
    def lookups(self, request, model_admin):
1✔
242
        return (
1✔
243
            (None, 'Current'),
244
            ('All', 'All'),
245
        )
246

247
    def queryset(self, request, queryset):
1✔
248
        if self.value() is None:
1✔
249
            return queryset.exclude(nsubs=0)
1✔
250

251

252
class HousingAdmin(BaseAdmin):
1✔
253

254
    inlines = [HousingSubjectAdminInline]
1✔
255
    form = HousingAdminForm
1✔
256

257
    fields = ['subjects_l',
1✔
258
              ('cage_type', 'cage_name'),
259
              ('enrichment', 'light_cycle',),
260
              ('food', 'cage_cleaning_frequency_days')]
261
    search_fields = ('housing_subjects__subject__nickname', 'housing_subjects__subject__lab__name')
1✔
262
    list_display = ('cage_l', 'subjects_l', 'subjects_old', 'start',
1✔
263
                    'end', 'subjects_count', 'lab',)
264
    readonly_fields = ('subjects_l',)
1✔
265
    list_filter = (HousingIsCurrentFilter,)
1✔
266

267
    def get_queryset(self, request):
1✔
268
        qs = Housing.objects.annotate(
1✔
269
            nsubs=models.Count('housing_subjects',
270
                               filter=Q(housing_subjects__end_datetime__isnull=True),
271
                               distinct=True))
272
        qs = qs.annotate(start_datetime=models.Min('housing_subjects__start_datetime'))
1✔
273
        qs = qs.annotate(end_datetime=models.Case(models.When(
1✔
274
            nsubs=0, then=models.Max('housing_subjects__end_datetime')),
275
            output_field=models.DateTimeField(),))
276
        return qs
1✔
277

278
    def start(self, obj):
1✔
279
        return obj.start_datetime
×
280

281
    def end(self, obj):
1✔
282
        return obj.end_datetime
×
283

284
    def subjects_count(self, obj):
1✔
285
        return obj.nsubs
×
286
    subjects_count.short_description = '# active'
1✔
287

288
    def cage_l(self, obj):
1✔
289
        return format_html('<a href="{url}">{hou}</a>', url=get_admin_url(obj), hou=obj.cage_name)
×
290
    cage_l.short_description = 'cage'
1✔
291

292
    def subjects_l(self, obj):
1✔
293
        out = format_html_join(', ', '<a href="{}">{}</a>',
1✔
294
                               ((get_admin_url(sub), sub) for sub in obj.subjects_current()))
295
        return format_html(out)
×
296
    subjects_l.short_description = 'subjects'
1✔
297

298
    def subjects_old(self, obj):
1✔
299
        subs = obj.subjects.exclude(pk__in=obj.subjects_current().values_list('pk', flat=True))
×
300
        out = format_html_join(', ', '<a href="{}">{}</a>',
×
301
                               ((get_admin_url(sub), sub) for sub in subs))
302
        return format_html(out)
×
303
    subjects_old.short_description = 'old subjects'
1✔
304

305

306
admin.site.register(Housing, HousingAdmin)
1✔
307
admin.site.register(Lab, LabAdmin)
1✔
308
admin.site.register(LabMembership, LabMembershipAdmin)
1✔
309
admin.site.register(LabLocation, LabLocationAdmin)
1✔
310
admin.site.register(Note, NoteAdmin)
1✔
311
admin.site.register(CageType, CageTypeAdmin)
1✔
312
admin.site.register(Enrichment, EnrichmentAdmin)
1✔
313
admin.site.register(Food, FoodAdmin)
1✔
314
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