Skip to content

Commit fb283a2

Browse files
authored
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
1 parent fbf7640 commit fb283a2

File tree

8 files changed

+117
-11
lines changed

8 files changed

+117
-11
lines changed

alyx/alyx/environment_template.env

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ DJANGO_DEBUG=False
1010
DJANGO_LOG_LEVEL=INFO
1111
DJANGO_SECRET_KEY='create_a_secret_key_here_and_make_sure_you_use_single_quotes'
1212
GLOBUS_CLIENT_ID=525cc543-8ccb-4d11-8036-af332da5eafd
13+
TZ=UTC # Alyx datetime timezone. See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
1314
# DJANGO_MEDIA_ROOT=https://alyx-uploaded.s3.eu-west-2.amazonaws.com/uploaded/ # add if s3
1415
# DJANGO_MEDIA_ROOT=/home/olivier/scratch/uploaded # add if local instance (for docker deployment leave it commented)
1516
# DJANGO_TABLES_ROOT=https://ibl-brain-wide-map-public.s3.amazonaws.com/caches/alyx # add if s3
16-
# DJANGO_TABLES_ROOT=/home/olivier/scratch/uploaded # add if local instance (for docker deployment leave it commented)
17+
# DJANGO_TABLES_ROOT=/home/olivier/scratch/uploaded # add if local instance (for docker deployment leave it commented)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.2.8 on 2025-11-05 23:51
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('data', '0021_alter_dataset_collection_alter_dataset_hash_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='datarepository',
15+
name='timezone',
16+
field=models.CharField(blank=True, default='Africa/Djibouti', help_text='Timezone of the server (see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)', max_length=64),
17+
),
18+
]

alyx/misc/admin.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from django import forms
44
from django.db import models
55
from django.db.models import Q
6+
from django.conf import settings
67
from django.contrib import admin
78
from django.contrib.admin.widgets import AdminFileWidget
89
from django.contrib.contenttypes.admin import GenericTabularInline
@@ -49,7 +50,7 @@ def clean_timezone(self):
4950
ref = self.cleaned_data['timezone']
5051
if ref not in all_timezones:
5152
raise forms.ValidationError(
52-
("Time Zone is incorrect here is the list (column TZ Database Name): "
53+
("Time Zone is incorrect. Here is the list (column TZ Database Name): "
5354
"https://en.wikipedia.org/wiki/List_of_tz_database_time_zones"))
5455
return ref
5556

@@ -70,6 +71,12 @@ def local(self, obj):
7071
def server(self, obj):
7172
return ','.join([p.name for p in obj.repositories.filter(globus_is_personal=False)])
7273

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

7481
class LabMembershipAdmin(BaseAdmin):
7582
fields = ['user', 'lab', 'role', 'start_date', 'end_date']
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.2.8 on 2025-11-05 23:51
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('misc', '0011_alter_lab_name'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='lab',
15+
name='timezone',
16+
field=models.CharField(blank=True, help_text='Timezone of the server (see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)', max_length=64),
17+
),
18+
]

alyx/misc/models.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,10 @@
1919
from django.utils import timezone
2020

2121
from alyx.base import BaseModel, modify_fields, ALF_SPEC
22-
from alyx.settings import TIME_ZONE, UPLOADED_IMAGE_WIDTH, DEFAULT_LAB_PK
2322

2423

2524
def default_lab():
26-
return DEFAULT_LAB_PK
25+
return settings.DEFAULT_LAB_PK
2726

2827

2928
class LabMember(AbstractUser):
@@ -78,7 +77,7 @@ class Lab(BaseModel):
7877
institution = models.CharField(max_length=255, blank=True)
7978
address = models.CharField(max_length=255, blank=True)
8079
timezone = models.CharField(
81-
max_length=64, blank=True, default=TIME_ZONE,
80+
max_length=64, blank=True,
8281
help_text="Timezone of the server "
8382
"(see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)")
8483

@@ -106,6 +105,10 @@ class Lab(BaseModel):
106105
def __str__(self):
107106
return self.name
108107

108+
def save(self, *args, timezone=None, **kwargs):
109+
self.timezone = timezone or self.timezone or settings.TIME_ZONE
110+
super().save(*args, **kwargs)
111+
109112

110113
class LabMembership(BaseModel):
111114
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
@@ -158,7 +161,7 @@ def save(self, image_width=None, **kwargs):
158161
with Image.open(self.image) as im:
159162
with BytesIO() as output:
160163
# Compute new size by keeping the aspect ratio.
161-
width = int(image_width or UPLOADED_IMAGE_WIDTH)
164+
width = int(image_width or settings.UPLOADED_IMAGE_WIDTH)
162165
wpercent = width / float(im.size[0])
163166
height = int((float(im.size[1]) * float(wpercent)))
164167
im.thumbnail((width, height))

alyx/misc/tests_admin.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from unittest.mock import MagicMock
2+
3+
from django.test import TestCase, Client, override_settings
4+
from django.contrib.admin.sites import AdminSite
5+
6+
from misc.models import LabMember, Lab
7+
from misc.management.commands.set_user_permissions import Command
8+
from misc.admin import LabAdmin
9+
10+
11+
class TestLabAdminViews(TestCase):
12+
13+
def setUp(self):
14+
self.client = Client()
15+
self.user = LabMember.objects.create_user(
16+
username='foo', password='bar123', email='[email protected]')
17+
self.user.is_staff = self.user.is_active = True # used by below Command instance
18+
self.user.save()
19+
20+
self.superuser = LabMember.objects.create_superuser(
21+
username='admin', password='admin', email='[email protected]')
22+
Command().handle() # set user group permissions
23+
self.client.login(username='foo', password='bar123')
24+
25+
@override_settings(TIME_ZONE='Africa/Djibouti')
26+
def test_admin_add_form_view(self):
27+
"""Test the lab add form view."""
28+
response = self.client.get('/admin/misc/lab/add/', follow=True)
29+
self.assertEqual(response.status_code, 200)
30+
self.client.login(username='admin', password='admin')
31+
response = self.client.get('/admin/misc/lab/add/', follow=True)
32+
self.assertEqual(response.status_code, 200)
33+
la = LabAdmin(model=Lab, admin_site=AdminSite())
34+
request = MagicMock()
35+
request.user = self.user
36+
LabForm = la.get_form(request)
37+
# Check initial value of timezone field
38+
form = LabForm(data={})
39+
self.assertEqual(form.fields['timezone'].initial, 'Africa/Djibouti')
40+
# Check form cleaning
41+
data = {
42+
'timezone': 'Africa/Djibouti',
43+
'name': 'TestLab',
44+
'reference_weight_pct': 0.8,
45+
'zscore_weight_pct': 0
46+
}
47+
form = LabForm(data=data)
48+
self.assertTrue(form.is_valid())
49+
data['timezone'] = 'Africa/Zaire' # Invalid timezone
50+
form = LabForm(data=data)
51+
self.assertFalse(form.is_valid())
52+
error = form.errors.get('timezone')[0]
53+
self.assertIn('Time Zone is incorrect. Here is the list', error)

deploy/docker/Dockerfile_base

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ FROM ubuntu/apache2:2.4-22.04_beta
22

33
# python unbuffered allows to get real-time logs
44
ENV PYTHONUNBUFFERED=1
5-
ENV TZ=Europe/London
5+
ARG TZ=UTC
6+
ENV TZ=${TZ}
67
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
78

89
RUN cat /etc/lsb-release && apt-get update

deploy/docker/settings_lab-deploy.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import os
2+
import zoneinfo
23
from textwrap import dedent
34

45
LANGUAGE_CODE = 'en-us'
5-
TIME_ZONE = 'Europe/London'
6+
TIME_ZONE = os.getenv('TZ', 'Europe/London').strip()
7+
if TIME_ZONE not in zoneinfo.available_timezones():
8+
raise ValueError(f'Invalid TIME_ZONE: "{TIME_ZONE}". '
9+
'Please set a valid timezone with TZ env variable. '
10+
'See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.')
611
GLOBUS_CLIENT_ID = os.getenv('GLOBUS_CLIENT_ID')
7-
SUBJECT_REQUEST_EMAIL_FROM = 'alyx@internationalbrainlab.org'
12+
SUBJECT_REQUEST_EMAIL_FROM = os.getenv('APACHE_SERVER_ADMIN', 'alyx@localhost')
813
DEFAULT_SOURCE = 'IBL'
914
DEFAULT_PROTOCOL = '1'
1015
SUPERUSERS = ('root',)
@@ -13,8 +18,8 @@
1318
DEFAULT_LAB_NAME = 'cortexlab'
1419
WATER_RESTRICTIONS_EDITABLE = False # if set to True, all users can edit water restrictions
1520
DEFAULT_LAB_PK = '4027da48-7be3-43ec-a222-f75dffe36872'
16-
SESSION_REPO_URL = \
17-
"http://ibl.flatironinstitute.org/{lab}/Subjects/{subject}/{date}/{number:03d}/"
21+
SESSION_REPO_URL = f'https://{os.getenv("APACHE_SERVER_NAME", "localhost")}/'
22+
SESSION_REPO_URL += "{lab}/Subjects/{subject}/{date}/{number:03d}/"
1823
NARRATIVE_TEMPLATES = {
1924
'Headplate implant': dedent('''
2025
== General ==

0 commit comments

Comments
 (0)