diff --git a/alyx/jobs/tests.py b/alyx/jobs/tests.py index 848b2d54..5d6e15bd 100644 --- a/alyx/jobs/tests.py +++ b/alyx/jobs/tests.py @@ -68,7 +68,7 @@ def test_cleanup(self): """Test for cleanup action.""" # First run in dry mode, expect submit_delete to not be called n = self.n_tasks - 10 - before_date = (self.base - timedelta(days=n)).date() + before_date = (self.base - timedelta(days=n - .1)).date() with patch.object(self.command.stdout, 'write') as stdout_mock: self.command.handle(action='cleanup', before=str(before_date), dry=True) stdout_mock.assert_called() diff --git a/alyx/subjects/admin.py b/alyx/subjects/admin.py index 14b7084b..c318ea41 100755 --- a/alyx/subjects/admin.py +++ b/alyx/subjects/admin.py @@ -1,3 +1,5 @@ +import uuid + from django import forms from django_admin_listfilter_dropdown.filters import RelatedDropdownFilter from django.contrib import admin @@ -836,10 +838,12 @@ def queryset(self, request, queryset): return queryset.all() -def _bp_subjects(line, sex): +def _bp_subjects(line, sex, current_subjects=None): # All alive subjects of the given sex. qs = Subject.objects.filter( - sex=sex, responsible_user__is_stock_manager=True, cull__isnull=True) + Q(sex=sex, responsible_user__is_stock_manager=True, cull__isnull=True) | + Q(pk__in=[current_subjects] if isinstance(current_subjects, uuid.UUID) else []), + ) qs = qs.order_by('nickname') ids = [item.id for item in qs] if ids: @@ -876,7 +880,9 @@ def __init__(self, *args, **kwargs): for w in ('father', 'mother1', 'mother2'): sex = 'M' if w == 'father' else 'F' if w in self.fields: - self.fields[w].queryset = _bp_subjects(self.instance.line, sex) + self.fields[w].queryset = _bp_subjects( + self.instance.line, sex, current_subjects= + getattr(getattr(self.instance, w), 'id', None)) def save(self, commit=True): cage = self.cleaned_data.get('cage') @@ -1076,7 +1082,7 @@ def formfield_for_foreignkey(self, db_field, request=None, **kwargs): return field if db_field.name in ('father', 'mother1', 'mother2'): sex = 'M' if db_field.name == 'father' else 'F' - field.queryset = _bp_subjects(obj, sex) + field.queryset = _bp_subjects(obj, sex, BreedingPair.objects.filter(line=obj).values_list(f'{db_field.name}__pk')) return field diff --git a/alyx/subjects/tests_admin.py b/alyx/subjects/tests_admin.py index 04a41f9b..c44ceb0f 100644 --- a/alyx/subjects/tests_admin.py +++ b/alyx/subjects/tests_admin.py @@ -7,9 +7,45 @@ from alyx.test_base import setup_admin_subject_user from misc.models import LabMember -from subjects.models import Subject +from subjects.models import Subject, BreedingPair from actions.models import Cull, CullMethod, CullReason -from subjects.admin import CullForm, SubjectAdmin +from subjects.admin import CullForm, SubjectAdmin, BreedingPairAdminForm + + +class TestBreedingPairAdmin(TestCase): + fixtures = ['misc.lab.json'] + + def setUp(self): + self.site = AdminSite() + setup_admin_subject_user(self) + + def test_set_end_date_with_culled_subject(self): + lab_member_stock_manager = LabMember.objects.create_user( + username='stock_manager', password='bar123', email='foo@example.com', is_staff=True, is_active=True, is_stock_manager=True) + kwargs_subjects = dict(birth_date=date(2025, 1, 1), lab=self.lab, responsible_user=lab_member_stock_manager) + father = Subject.objects.create(nickname='father', sex='M', **kwargs_subjects) + mother1 = Subject.objects.create(nickname='mother1', sex='F', **kwargs_subjects) + breeding_pair = BreedingPair.objects.create(father=father, mother1=mother1, mother2=None) + # the form is valid as both parents are alive + form_data = { + 'json': breeding_pair.json, + 'description': breeding_pair.description, + 'name': breeding_pair.name, + 'line': breeding_pair.line, + 'start_date': breeding_pair.start_date, + 'end_date': breeding_pair.end_date, + 'father': breeding_pair.father, + 'mother1': breeding_pair.mother1, + 'mother2': breeding_pair.mother2, + } + form_instance = BreedingPairAdminForm(data=form_data, instance=breeding_pair) + self.assertTrue(form_instance.is_valid()) + # the form is still valid once the father is culled: it can“t be set on a new breeding pair, + # but it can remain on this one and the form is valid + Cull(subject=father, user=lab_member_stock_manager, date=date(2025, 6, 1)) + father.save() + form_instance = BreedingPairAdminForm(data=form_data, instance=breeding_pair) + self.assertTrue(form_instance.is_valid()) class TestSubjectCullForm(TestCase): diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile index f020f4ff..b6f0a494 100644 --- a/deploy/docker/Dockerfile +++ b/deploy/docker/Dockerfile @@ -4,12 +4,13 @@ COPY settings-deploy.py /var/www/alyx/alyx/alyx/settings.py COPY settings_lab-deploy.py /var/www/alyx/alyx/alyx/settings_lab.py COPY settings_lab-user.py* /var/www/alyx/alyx/alyx/settings_lab.py -ARG alyx_branch=deploy +ARG alyx_branch=dev +ARG iblalyx_branch=deploy RUN git -C /var/www/alyx fetch origin ${alyx_branch} RUN git -C /var/www/alyx checkout -B ${alyx_branch} origin/${alyx_branch} -RUN git -C /home/iblalyx fetch origin ${alyx_branch} -RUN git -C /home/iblalyx checkout -B ${alyx_branch} origin/${alyx_branch} +RUN git -C /home/iblalyx fetch origin ${iblalyx_branch} +RUN git -C /home/iblalyx checkout -B ${iblalyx_branch} origin/${iblalyx_branch} RUN pip install -r /var/www/alyx/requirements.txt diff --git a/deploy/docker/Dockerfile_base b/deploy/docker/Dockerfile_base index 417c3c7f..1a17b5b9 100644 --- a/deploy/docker/Dockerfile_base +++ b/deploy/docker/Dockerfile_base @@ -1,13 +1,13 @@ -FROM ubuntu/apache2:latest +FROM ubuntu/apache2:2.4-22.04_beta # python unbuffered allows to get real-time logs -ENV PYTHONUNBUFFERED 1 +ENV PYTHONUNBUFFERED=1 ENV TZ=Europe/London RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone - +RUN cat /etc/lsb-release && apt-get update # Install services, packages and perform cleanup -RUN apt-get update && apt-get install -y \ +RUN apt-get install -y \ awscli \ bash \ build-essential \ @@ -23,8 +23,8 @@ RUN apt-get update && apt-get install -y \ wget # The apt-add-repository command is installed by software-properties common above -ARG PYTHON=python3.12 -RUN apt-get update && apt-get install -y ${PYTHON} ${PYTHON}-venv +ARG PYTHON=python3.13 +RUN apt-add-repository -y ppa:deadsnakes/ppa && apt-get update && apt-get install -y ${PYTHON} ${PYTHON}-venv # Clean up RUN apt-get clean && rm -rf /var/lib/apt/lists/* @@ -32,7 +32,7 @@ RUN apt-get clean && rm -rf /var/lib/apt/lists/* RUN echo "root:Docker!" | chpasswd # Alyx application installation -RUN git clone --branch master https://github.com/cortex-lab/alyx.git /var/www/alyx +RUN git clone --branch dev https://github.com/cortex-lab/alyx.git /var/www/alyx # Best practice for configuring python venv ENV VIRTUAL_ENV=/var/www/alyx/.venv RUN ${PYTHON} -m venv ${VIRTUAL_ENV} @@ -48,11 +48,11 @@ RUN git clone --branch main https://github.com/int-brain-lab/iblalyx.git /home/i && pip install --no-cache-dir -r /home/iblalyx/requirements.txt # Apache ENVs -ENV APACHE_RUN_USER www-data -ENV APACHE_RUN_GROUP www-data -ENV APACHE_LOCK_DIR /var/lock/apache2 -ENV APACHE_LOG_DIR /var/log/apache2 -ENV APACHE_PID_FILE /var/run/apache2/apache2.pid +ENV APACHE_RUN_USER=www-data +ENV APACHE_RUN_GROUP=www-data +ENV APACHE_LOCK_DIR=/var/lock/apache2 +ENV APACHE_LOG_DIR=/var/log/apache2 +ENV APACHE_PID_FILE=/var/run/apache2/apache2.pid RUN mkdir -p ${APACHE_LOG_DIR} \ && touch ${APACHE_LOG_DIR}/django.log \