From d497dd9c16d8f419af88cbd873b7f987c578678a Mon Sep 17 00:00:00 2001 From: suhlrich <43877159+suhlrich@users.noreply.github.com> Date: Thu, 25 Jun 2026 14:06:49 -0600 Subject: [PATCH 1/7] add a save_local model and endpoint tested. --- .../migrations/0046_session_save_local.py | 18 +++++++ mcserver/models.py | 1 + mcserver/views.py | 48 ++++++++++++++++++- 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 mcserver/migrations/0046_session_save_local.py diff --git a/mcserver/migrations/0046_session_save_local.py b/mcserver/migrations/0046_session_save_local.py new file mode 100644 index 0000000..1af96f0 --- /dev/null +++ b/mcserver/migrations/0046_session_save_local.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.14 on 2026-06-25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mcserver', '0045_video_islidar'), + ] + + operations = [ + migrations.AddField( + model_name='session', + name='save_local', + field=models.BooleanField(default=False), + ), + ] diff --git a/mcserver/models.py b/mcserver/models.py index 101af3c..c62feb6 100644 --- a/mcserver/models.py +++ b/mcserver/models.py @@ -80,6 +80,7 @@ class Session(models.Model): trashed_at = models.DateTimeField(blank=True, null=True) isMono = models.BooleanField(default=False, db_index=True) + save_local = models.BooleanField(default=False) class Meta: ordering = ['-created_at'] diff --git a/mcserver/views.py b/mcserver/views.py index 183bec2..b633086 100644 --- a/mcserver/views.py +++ b/mcserver/views.py @@ -253,6 +253,52 @@ def get_queryset(self): def api_health_check(self, request): return Response({"status": "True"}) + @action(detail=True, methods=['get', 'patch'], permission_classes=[AllowAny]) + def save_local(self, request, pk): + try: + if pk == 'undefined': + raise ValueError(_("undefined_uuid")) + + session = get_object_or_404(Session, pk=pk) + + if request.method == 'GET': + return Response({"save_local": session.save_local}) + + if not request.user.is_authenticated: + raise NotAuthenticated(_('login_needed')) + + has_permission = ( + IsOwner().has_permission(request, self) and + IsOwner().has_object_permission(request, self, session) + ) or IsAdmin().has_object_permission(request, self, session) or \ + IsBackend().has_object_permission(request, self, session) + + if not has_permission: + raise PermissionDenied(_('permission_denied')) + + save_local = request.data.get('save_local', request.query_params.get('save_local')) + if isinstance(save_local, bool): + session.save_local = save_local + elif isinstance(save_local, str) and save_local.lower() in ['true', 'false']: + session.save_local = save_local.lower() == 'true' + else: + return Response( + {'save_local': ['Expected true or false.']}, + status=status.HTTP_400_BAD_REQUEST + ) + + session.save(update_fields=['save_local', 'updated_at']) + return Response({"save_local": session.save_local}) + + except Http404: + if settings.DEBUG: + raise APIException(_("error") % {"error_message": str(traceback.format_exc())}) + raise NotFound(_("session_uuid_not_found") % {"uuid": str(pk)}) + except ValueError: + if settings.DEBUG: + raise APIException(_("error") % {"error_message": str(traceback.format_exc())}) + raise NotFound(_("session_uuid_not_valid") % {"uuid": str(pk)}) + @action( detail=True, methods=["get", "post"], @@ -2738,4 +2784,4 @@ def data(self, request, pk): return Response(dashboard.get_available_data()) return Response(dashboard.get_available_data( - only_public=True, subject_id=request.GET.get('subject_id'), share_token=request.GET.get('share_token'))) \ No newline at end of file + only_public=True, subject_id=request.GET.get('subject_id'), share_token=request.GET.get('share_token'))) From 112fa7b649fa31908988f79709cb64563330e2b3 Mon Sep 17 00:00:00 2001 From: suhlrich <43877159+suhlrich@users.noreply.github.com> Date: Thu, 25 Jun 2026 14:39:34 -0600 Subject: [PATCH 2/7] add video saved_local boolean and change the endpoint for phone to set it once uploaded --- mcserver/migrations/0047_video_saved_local.py | 18 ++++++++++++++++++ mcserver/models.py | 1 + mcserver/serializers.py | 2 +- mcserver/views.py | 3 +++ 4 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 mcserver/migrations/0047_video_saved_local.py diff --git a/mcserver/migrations/0047_video_saved_local.py b/mcserver/migrations/0047_video_saved_local.py new file mode 100644 index 0000000..8929488 --- /dev/null +++ b/mcserver/migrations/0047_video_saved_local.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.14 on 2026-06-25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mcserver', '0046_session_save_local'), + ] + + operations = [ + migrations.AddField( + model_name='video', + name='saved_local', + field=models.BooleanField(default=False), + ), + ] diff --git a/mcserver/models.py b/mcserver/models.py index c62feb6..bd8a1ed 100644 --- a/mcserver/models.py +++ b/mcserver/models.py @@ -180,6 +180,7 @@ class Video(models.Model): keypoints = models.FileField(blank=True, null=True) parameters = models.JSONField(blank=True, null=True) isLidar = models.BooleanField(default=False) + saved_local = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True, db_index=True) updated_at = models.DateTimeField(auto_now=True, db_index=True) diff --git a/mcserver/serializers.py b/mcserver/serializers.py index 7d54c66..81f05c6 100644 --- a/mcserver/serializers.py +++ b/mcserver/serializers.py @@ -145,7 +145,7 @@ class VideoSerializer(serializers.ModelSerializer): required=False) class Meta: model = Video - fields = ['id', 'trial', 'device_id', 'video', 'video_url', 'video_thumb', 'parameters', 'isLidar', 'created_at', 'updated_at'] + fields = ['id', 'trial', 'device_id', 'video', 'video_url', 'video_thumb', 'parameters', 'isLidar', 'saved_local', 'created_at', 'updated_at'] # Serializers define the API representation. diff --git a/mcserver/views.py b/mcserver/views.py index b633086..64d615d 100644 --- a/mcserver/views.py +++ b/mcserver/views.py @@ -1930,6 +1930,9 @@ def perform_update(self, serializer): if "isLidar" in self.request.data: serializer.validated_data["isLidar"] = str(self.request.data.get("isLidar", "")).lower() == "true" + if "saved_local" in self.request.data: + serializer.validated_data["saved_local"] = str(self.request.data.get("saved_local", "")).lower() == "true" + super().perform_update(serializer) class ResultViewSet(viewsets.ModelViewSet): From f70c03a6a658d593d048ff346f10c32c26660b0c Mon Sep 17 00:00:00 2001 From: suhlrich <43877159+suhlrich@users.noreply.github.com> Date: Thu, 25 Jun 2026 15:14:55 -0600 Subject: [PATCH 3/7] upload status checking for locally saved videos if save_local is true --- mcserver/views.py | 17 +++++++++++++-- tests/test_views.py | 53 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/mcserver/views.py b/mcserver/views.py index 64d615d..473c9a7 100644 --- a/mcserver/views.py +++ b/mcserver/views.py @@ -834,7 +834,16 @@ def get_status(self, request, pk): # if not all videos uploaded then the status is 'uploading' # if results are not ready then processing # otherwise it's ready again - if any([(not v.video) for v in trial.video_set.all()]): + if session.save_local: + is_waiting_for_videos = any([ + (not v.saved_local) for v in trial.video_set.all() + ]) + else: + is_waiting_for_videos = any([ + (not v.video) for v in trial.video_set.all() + ]) + + if is_waiting_for_videos: status = 'uploading' elif trial.result_set.count() == 0: status = 'processing' @@ -854,7 +863,11 @@ def get_status(self, request, pk): n_videos_uploaded = 0 n_cameras_connected = Video.objects.filter(trial=trial).count() for video in Video.objects.filter(trial=trial).all(): - if video.video and video.video.url: + if session.save_local: + video_uploaded = video.saved_local + else: + video_uploaded = video.video and video.video.url + if video_uploaded: n_videos_uploaded = n_videos_uploaded + 1 video_url = None diff --git a/tests/test_views.py b/tests/test_views.py index 7d72ce3..a931561 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,11 +1,13 @@ from unittest import mock +import uuid from django.test import TestCase from rest_framework.permissions import IsAuthenticated from rest_framework.reverse import reverse from mcserver.models import ( - User, AnalysisFunction, AnalysisResult, AnalysisResultState + User, AnalysisFunction, AnalysisResult, AnalysisResultState, + Session, Trial, Video ) from mcserver.serializers import ( AnalysisFunctionSerializer, AnalysisResultSerializer @@ -147,3 +149,52 @@ def test_analysis_result_on_ready_responses_200_if_result_ready(self): ) self.assertEqual(response.status_code, 200) self.assertEqual(response.data, AnalysisResultSerializer(result).data) + + def test_session_status_waits_for_video_field_when_not_save_local(self): + session = Session.objects.create(user=self.user, save_local=False) + trial = Trial.objects.create(session=session, status='stopped') + Video.objects.create( + trial=trial, device_id=uuid.uuid4(), video='uploaded-a.mov', + saved_local=True + ) + pending_video = Video.objects.create( + trial=trial, device_id=uuid.uuid4(), saved_local=True + ) + + self.client.force_login(self.user) + response = self.client.get(f'/sessions/{session.pk}/status/') + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['status'], 'uploading') + self.assertEqual(response.data['n_videos_uploaded'], 1) + + pending_video.video = 'uploaded-b.mov' + pending_video.save(update_fields=['video']) + response = self.client.get(f'/sessions/{session.pk}/status/') + + self.assertEqual(response.data['status'], 'processing') + self.assertEqual(response.data['n_videos_uploaded'], 2) + + def test_session_status_waits_for_saved_local_when_save_local(self): + session = Session.objects.create(user=self.user, save_local=True) + trial = Trial.objects.create(session=session, status='stopped') + Video.objects.create( + trial=trial, device_id=uuid.uuid4(), video='', saved_local=True + ) + pending_video = Video.objects.create( + trial=trial, device_id=uuid.uuid4(), video='', saved_local=False + ) + + self.client.force_login(self.user) + response = self.client.get(f'/sessions/{session.pk}/status/') + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['status'], 'uploading') + self.assertEqual(response.data['n_videos_uploaded'], 1) + + pending_video.saved_local = True + pending_video.save(update_fields=['saved_local']) + response = self.client.get(f'/sessions/{session.pk}/status/') + + self.assertEqual(response.data['status'], 'processing') + self.assertEqual(response.data['n_videos_uploaded'], 2) From 9e5924152e9ffedcb2afd65388381f6da03ed04a Mon Sep 17 00:00:00 2001 From: suhlrich <43877159+suhlrich@users.noreply.github.com> Date: Thu, 25 Jun 2026 15:20:51 -0600 Subject: [PATCH 4/7] save local persist with new session same setup --- mcserver/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mcserver/views.py b/mcserver/views.py index 473c9a7..63e8df2 100644 --- a/mcserver/views.py +++ b/mcserver/views.py @@ -757,6 +757,7 @@ def new_subject(self, request, pk): if not user.is_authenticated: user = User.objects.get(id=1) sessionNew.user = user + sessionNew.save_local = sessionOld.save_local except Http404: if settings.DEBUG: From 4de52a3446428a3e894172bcdeab6f7b732fe5b7 Mon Sep 17 00:00:00 2001 From: suhlrich <43877159+suhlrich@users.noreply.github.com> Date: Thu, 25 Jun 2026 16:17:52 -0600 Subject: [PATCH 5/7] allow video to be uploaded for backwards app compatability --- mcserver/views.py | 4 ++-- tests/test_views.py | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/mcserver/views.py b/mcserver/views.py index 63e8df2..44e97d0 100644 --- a/mcserver/views.py +++ b/mcserver/views.py @@ -837,7 +837,7 @@ def get_status(self, request, pk): # otherwise it's ready again if session.save_local: is_waiting_for_videos = any([ - (not v.saved_local) for v in trial.video_set.all() + (not v.video and not v.saved_local) for v in trial.video_set.all() ]) else: is_waiting_for_videos = any([ @@ -865,7 +865,7 @@ def get_status(self, request, pk): n_cameras_connected = Video.objects.filter(trial=trial).count() for video in Video.objects.filter(trial=trial).all(): if session.save_local: - video_uploaded = video.saved_local + video_uploaded = video.video or video.saved_local else: video_uploaded = video.video and video.video.url if video_uploaded: diff --git a/tests/test_views.py b/tests/test_views.py index a931561..dc85b2b 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -175,9 +175,13 @@ def test_session_status_waits_for_video_field_when_not_save_local(self): self.assertEqual(response.data['status'], 'processing') self.assertEqual(response.data['n_videos_uploaded'], 2) - def test_session_status_waits_for_saved_local_when_save_local(self): + def test_session_status_waits_for_video_or_saved_local_when_save_local(self): session = Session.objects.create(user=self.user, save_local=True) trial = Trial.objects.create(session=session, status='stopped') + Video.objects.create( + trial=trial, device_id=uuid.uuid4(), video='uploaded-a.mov', + saved_local=False + ) Video.objects.create( trial=trial, device_id=uuid.uuid4(), video='', saved_local=True ) @@ -190,11 +194,11 @@ def test_session_status_waits_for_saved_local_when_save_local(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.data['status'], 'uploading') - self.assertEqual(response.data['n_videos_uploaded'], 1) + self.assertEqual(response.data['n_videos_uploaded'], 2) pending_video.saved_local = True pending_video.save(update_fields=['saved_local']) response = self.client.get(f'/sessions/{session.pk}/status/') self.assertEqual(response.data['status'], 'processing') - self.assertEqual(response.data['n_videos_uploaded'], 2) + self.assertEqual(response.data['n_videos_uploaded'], 3) From d85f7d2f3d83b01dbef1843f577f07e8c33fd3ae Mon Sep 17 00:00:00 2001 From: suhlrich <43877159+suhlrich@users.noreply.github.com> Date: Thu, 25 Jun 2026 17:45:33 -0600 Subject: [PATCH 6/7] serialize save_local --- mcserver/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mcserver/serializers.py b/mcserver/serializers.py index 81f05c6..e67fc0d 100644 --- a/mcserver/serializers.py +++ b/mcserver/serializers.py @@ -237,7 +237,7 @@ class Meta: fields = [ 'id', 'user', 'public', 'name', 'sessionName', 'qrcode', 'meta', 'trials', 'server', - 'subject', 'isMono', + 'subject', 'isMono', 'save_local', 'created_at', 'updated_at', 'trashed', 'trashed_at', 'trials_count', 'trashed_trials_count', ] @@ -255,7 +255,7 @@ class Meta: fields = [ 'id', 'user', 'public', 'name', 'sessionName', 'qrcode', 'meta', 'trials', 'server', - 'subject', 'isMono', + 'subject', 'isMono', 'save_local', 'created_at', 'updated_at', 'trashed', 'trashed_at', 'trials_count', 'trashed_trials_count', ] From bf3ae67765750fbf5623709f3150c1574362161b Mon Sep 17 00:00:00 2001 From: suhlrich <43877159+suhlrich@users.noreply.github.com> Date: Thu, 25 Jun 2026 18:05:58 -0600 Subject: [PATCH 7/7] return trialname for ios --- mcserver/views.py | 1 + tests/test_views.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/mcserver/views.py b/mcserver/views.py index 44e97d0..29a577a 100644 --- a/mcserver/views.py +++ b/mcserver/views.py @@ -895,6 +895,7 @@ def get_status(self, request, pk): res = { "status": status, "trial": trial_url, + "trialname": trial.name if trial else None, "video": video_url, "framerate": frameRate, "newSessionURL": newSessionURL, diff --git a/tests/test_views.py b/tests/test_views.py index dc85b2b..7326b40 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -152,7 +152,9 @@ def test_analysis_result_on_ready_responses_200_if_result_ready(self): def test_session_status_waits_for_video_field_when_not_save_local(self): session = Session.objects.create(user=self.user, save_local=False) - trial = Trial.objects.create(session=session, status='stopped') + trial = Trial.objects.create( + session=session, status='stopped', name='walk-fast' + ) Video.objects.create( trial=trial, device_id=uuid.uuid4(), video='uploaded-a.mov', saved_local=True @@ -166,6 +168,7 @@ def test_session_status_waits_for_video_field_when_not_save_local(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.data['status'], 'uploading') + self.assertEqual(response.data['trialname'], 'walk-fast') self.assertEqual(response.data['n_videos_uploaded'], 1) pending_video.video = 'uploaded-b.mov' @@ -177,7 +180,9 @@ def test_session_status_waits_for_video_field_when_not_save_local(self): def test_session_status_waits_for_video_or_saved_local_when_save_local(self): session = Session.objects.create(user=self.user, save_local=True) - trial = Trial.objects.create(session=session, status='stopped') + trial = Trial.objects.create( + session=session, status='stopped', name='squat-1' + ) Video.objects.create( trial=trial, device_id=uuid.uuid4(), video='uploaded-a.mov', saved_local=False @@ -194,6 +199,7 @@ def test_session_status_waits_for_video_or_saved_local_when_save_local(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.data['status'], 'uploading') + self.assertEqual(response.data['trialname'], 'squat-1') self.assertEqual(response.data['n_videos_uploaded'], 2) pending_video.saved_local = True