Skip to content
Merged

Dev #298

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions mcserver/migrations/0046_session_save_local.py
Original file line number Diff line number Diff line change
@@ -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),
),
]
18 changes: 18 additions & 0 deletions mcserver/migrations/0047_video_saved_local.py
Original file line number Diff line number Diff line change
@@ -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),
),
]
2 changes: 2 additions & 0 deletions mcserver/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down Expand Up @@ -179,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)

Expand Down
6 changes: 3 additions & 3 deletions mcserver/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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',
]
Expand All @@ -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',
]
Expand Down
70 changes: 67 additions & 3 deletions mcserver/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down Expand Up @@ -711,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:
Expand Down Expand Up @@ -788,7 +835,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.video and 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'
Expand All @@ -808,7 +864,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.video or 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
Expand All @@ -835,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,
Expand Down Expand Up @@ -1884,6 +1945,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):
Expand Down Expand Up @@ -2738,4 +2802,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')))
only_public=True, subject_id=request.GET.get('subject_id'), share_token=request.GET.get('share_token')))
63 changes: 62 additions & 1 deletion tests/test_views.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -147,3 +149,62 @@ 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', name='walk-fast'
)
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['trialname'], 'walk-fast')
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_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', name='squat-1'
)
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
)
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['trialname'], 'squat-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'], 3)
Loading