diff --git a/mcserver/admin.py b/mcserver/admin.py index 754ee2a..a15e8b4 100644 --- a/mcserver/admin.py +++ b/mcserver/admin.py @@ -14,6 +14,7 @@ AnalysisResult, AnalysisDashboardTemplate, AnalysisDashboard, + SubjectTags ) from django.contrib.auth.models import Group from django.contrib.auth.admin import UserAdmin, GroupAdmin @@ -54,10 +55,11 @@ class SessionAdmin(admin.ModelAdmin): 'id', 'user', 'subject', 'public', 'created_at', 'updated_at', 'server', + 'status', 'status_changed', 'trashed', 'trashed_at', ) raw_id_fields = ('user', 'subject') - search_fields = ['id'] + search_fields = ['id', 'user__username', "subject__name"] inlines = [TrialInline] actions = ['set_subject'] @@ -92,7 +94,7 @@ class ResultInline(admin.TabularInline): @admin.register(Trial) class TrialAdmin(admin.ModelAdmin): - search_fields = ['id', 'session_id'] + search_fields = ['id', 'name', 'session__id'] list_display = ( 'id', 'name', @@ -111,20 +113,20 @@ class ResultAdmin(admin.ModelAdmin): 'id', 'trial', 'tag', 'media', 'device_id', 'created_at', 'updated_at') - search_fields = ['trial'] + search_fields = ['id', 'trial__id'] raw_id_fields = ('trial',) @admin.register(Video) class VideoAdmin(admin.ModelAdmin): - search_fields = ['trial'] - list_display = ('trial', 'video', 'created_at', 'updated_at') + search_fields = ['id', 'trial__id'] + list_display = ('id', 'trial', 'video', 'created_at', 'updated_at') raw_id_fields = ('trial',) @admin.register(Subject) class SubjectAdmin(admin.ModelAdmin): - search_fields = ['name'] + search_fields = ['name', 'user__username'] list_display = ( 'id', 'name', @@ -141,11 +143,19 @@ class SubjectAdmin(admin.ModelAdmin): ) raw_id_fields = ('user',) +@admin.register(SubjectTags) +class SubjectTagsAdmin(admin.ModelAdmin): + search_fields = ['tag', 'subject__name'] + list_display = ( + 'id', + 'tag', + 'subject', + ) @admin.register(ResetPassword) class ResetPasswordAdmin(admin.ModelAdmin): search_field = ['email'] - list_display = ('email', 'id', 'datetime') + list_display = ['email', 'id', 'datetime'] @admin.register(LogEntry) @@ -184,10 +194,10 @@ class DownloadLogAdmin(admin.ModelAdmin): @admin.register(AnalysisFunction) class AnalysisFunctionAdmin(admin.ModelAdmin): list_display = [ - 'id', 'title', 'is_active', + 'id', 'title', 'description', 'is_active', 'only_for_users_display', 'local_run', 'created_at'] - search_fields = ['title', 'description'] + search_fields = ['title'] raw_id_fields = ['only_for_users'] def only_for_users_display(self, obj): diff --git a/mcserver/migrations/0029_subject_tags_alter_analysisresult_status.py b/mcserver/migrations/0029_subject_tags_alter_analysisresult_status.py new file mode 100644 index 0000000..afee5a8 --- /dev/null +++ b/mcserver/migrations/0029_subject_tags_alter_analysisresult_status.py @@ -0,0 +1,96 @@ +# Generated by Django 4.2 on 2024-02-08 19:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("mcserver", "0028_user_institutional_use"), + ] + + operations = [ + migrations.AddField( + model_name="subject", + name="tags", + field=models.CharField( + blank=True, + choices=[("unimpaired", "Unimpaired")], + max_length=20, + null=True, + ), + ), + migrations.AlterField( + model_name="analysisresult", + name="status", + field=models.IntegerField( + choices=[ + (100, "Continue"), + (101, "Switching Protocols"), + (102, "Processing"), + (103, "Early Hints"), + (200, "OK"), + (201, "Created"), + (202, "Accepted"), + (203, "Non-Authoritative Information"), + (204, "No Content"), + (205, "Reset Content"), + (206, "Partial Content"), + (207, "Multi-Status"), + (208, "Already Reported"), + (226, "IM Used"), + (300, "Multiple Choices"), + (301, "Moved Permanently"), + (302, "Found"), + (303, "See Other"), + (304, "Not Modified"), + (305, "Use Proxy"), + (307, "Temporary Redirect"), + (308, "Permanent Redirect"), + (400, "Bad Request"), + (401, "Unauthorized"), + (402, "Payment Required"), + (403, "Forbidden"), + (404, "Not Found"), + (405, "Method Not Allowed"), + (406, "Not Acceptable"), + (407, "Proxy Authentication Required"), + (408, "Request Timeout"), + (409, "Conflict"), + (410, "Gone"), + (411, "Length Required"), + (412, "Precondition Failed"), + (413, "Request Entity Too Large"), + (414, "Request-URI Too Long"), + (415, "Unsupported Media Type"), + (416, "Requested Range Not Satisfiable"), + (417, "Expectation Failed"), + (418, "I'm a Teapot"), + (421, "Misdirected Request"), + (422, "Unprocessable Entity"), + (423, "Locked"), + (424, "Failed Dependency"), + (425, "Too Early"), + (426, "Upgrade Required"), + (428, "Precondition Required"), + (429, "Too Many Requests"), + (431, "Request Header Fields Too Large"), + (451, "Unavailable For Legal Reasons"), + (500, "Internal Server Error"), + (501, "Not Implemented"), + (502, "Bad Gateway"), + (503, "Service Unavailable"), + (504, "Gateway Timeout"), + (505, "HTTP Version Not Supported"), + (506, "Variant Also Negotiates"), + (507, "Insufficient Storage"), + (508, "Loop Detected"), + (510, "Not Extended"), + (511, "Network Authentication Required"), + ], + default=200, + help_text="Status code function responsed with.", + verbose_name="Status", + ), + ), + ] diff --git a/mcserver/migrations/0030_rename_tags_subject_subject_tags.py b/mcserver/migrations/0030_rename_tags_subject_subject_tags.py new file mode 100644 index 0000000..27b7e6b --- /dev/null +++ b/mcserver/migrations/0030_rename_tags_subject_subject_tags.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2 on 2024-02-08 19:21 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("mcserver", "0029_subject_tags_alter_analysisresult_status"), + ] + + operations = [ + migrations.RenameField( + model_name="subject", + old_name="tags", + new_name="subject_tags", + ), + ] diff --git a/mcserver/migrations/0031_remove_subject_subject_tags_subjecttags.py b/mcserver/migrations/0031_remove_subject_subject_tags_subjecttags.py new file mode 100644 index 0000000..656c961 --- /dev/null +++ b/mcserver/migrations/0031_remove_subject_subject_tags_subjecttags.py @@ -0,0 +1,43 @@ +# Generated by Django 4.2 on 2024-02-08 19:28 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("mcserver", "0030_rename_tags_subject_subject_tags"), + ] + + operations = [ + migrations.RemoveField( + model_name="subject", + name="subject_tags", + ), + migrations.CreateModel( + name="SubjectTags", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("tag", models.TextField()), + ( + "subject", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="mcserver.subject", + ), + ), + ], + options={ + "ordering": ["subject", "tag"], + }, + ), + ] diff --git a/mcserver/migrations/0032_merge_20240319_1339.py b/mcserver/migrations/0032_merge_20240319_1339.py new file mode 100644 index 0000000..cf88bdf --- /dev/null +++ b/mcserver/migrations/0032_merge_20240319_1339.py @@ -0,0 +1,14 @@ +# Generated by Django 3.1.14 on 2024-03-19 20:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('mcserver', '0029_analysisfunction_info'), + ('mcserver', '0031_remove_subject_subject_tags_subjecttags'), + ] + + operations = [ + ] diff --git a/mcserver/migrations/0033_auto_20240319_1339.py b/mcserver/migrations/0033_auto_20240319_1339.py new file mode 100644 index 0000000..989596c --- /dev/null +++ b/mcserver/migrations/0033_auto_20240319_1339.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.14 on 2024-03-19 20:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mcserver', '0032_merge_20240319_1339'), + ] + + operations = [ + migrations.AlterField( + model_name='analysisresult', + name='status', + field=models.IntegerField(choices=[(100, 'Continue'), (101, 'Switching Protocols'), (102, 'Processing'), (200, 'OK'), (201, 'Created'), (202, 'Accepted'), (203, 'Non-Authoritative Information'), (204, 'No Content'), (205, 'Reset Content'), (206, 'Partial Content'), (207, 'Multi-Status'), (208, 'Already Reported'), (226, 'IM Used'), (300, 'Multiple Choices'), (301, 'Moved Permanently'), (302, 'Found'), (303, 'See Other'), (304, 'Not Modified'), (305, 'Use Proxy'), (307, 'Temporary Redirect'), (308, 'Permanent Redirect'), (400, 'Bad Request'), (401, 'Unauthorized'), (402, 'Payment Required'), (403, 'Forbidden'), (404, 'Not Found'), (405, 'Method Not Allowed'), (406, 'Not Acceptable'), (407, 'Proxy Authentication Required'), (408, 'Request Timeout'), (409, 'Conflict'), (410, 'Gone'), (411, 'Length Required'), (412, 'Precondition Failed'), (413, 'Request Entity Too Large'), (414, 'Request-URI Too Long'), (415, 'Unsupported Media Type'), (416, 'Requested Range Not Satisfiable'), (417, 'Expectation Failed'), (421, 'Misdirected Request'), (422, 'Unprocessable Entity'), (423, 'Locked'), (424, 'Failed Dependency'), (426, 'Upgrade Required'), (428, 'Precondition Required'), (429, 'Too Many Requests'), (431, 'Request Header Fields Too Large'), (500, 'Internal Server Error'), (501, 'Not Implemented'), (502, 'Bad Gateway'), (503, 'Service Unavailable'), (504, 'Gateway Timeout'), (505, 'HTTP Version Not Supported'), (506, 'Variant Also Negotiates'), (507, 'Insufficient Storage'), (508, 'Loop Detected'), (510, 'Not Extended'), (511, 'Network Authentication Required')], default=200, help_text='Status code function responsed with.', verbose_name='Status'), + ), + ] diff --git a/mcserver/models.py b/mcserver/models.py index 9b8d62d..1b8561f 100644 --- a/mcserver/models.py +++ b/mcserver/models.py @@ -68,6 +68,9 @@ class Session(models.Model): public = models.BooleanField(blank=False, null=False, default=False) server = models.GenericIPAddressField(null=True, blank=True) + status = models.CharField(max_length=64, default="init", blank=True, db_index=True) + status_changed = models.DateTimeField(null=True, blank=True, default=timezone.now, db_index=True) + subject = models.ForeignKey( 'Subject', blank=True, null=True, related_name='sessions', @@ -335,6 +338,21 @@ def save(self, *args, **kwargs): self.birth_year = timezone.now().year - self.age return super().save(*args, **kwargs) + +class SubjectTags(models.Model): + tag = models.TextField(blank=False, null=False) + subject = models.ForeignKey(to=Subject, on_delete=models.CASCADE, blank=False, null=False) + + class Meta: + ordering = ['subject', 'tag'] + + def __str__(self): + return self.subject.name + " - " + self.tag + + verbose_name = 'Subject Tag' + verbose_name_plural = 'Subject Tags' + + class AnalysisFunction(models.Model): """ This model describes AWS Lambda function object. """ diff --git a/mcserver/serializers.py b/mcserver/serializers.py index 958351f..20fb018 100644 --- a/mcserver/serializers.py +++ b/mcserver/serializers.py @@ -11,6 +11,7 @@ AnalysisResult, AnalysisDashboardTemplate, AnalysisDashboard, + SubjectTags ) from rest_framework.validators import UniqueValidator from django.db.models import Prefetch @@ -190,6 +191,24 @@ class Meta: ] +class SessionStatusSerializer(serializers.ModelSerializer): + class Meta: + model = Session + fields = ['status'] + + +class SessionIdSerializer(serializers.ModelSerializer): + class Meta: + model = Session + fields = ['id'] + + +class SessionFilteringSerializer(serializers.Serializer): + status = serializers.CharField(max_length=64, required=True) + date_range = serializers.ListField(child=serializers.DateField(), required=False) + username = serializers.CharField(max_length=64, required=False) + + class SubjectSerializer(serializers.ModelSerializer): class Meta: model = Subject @@ -210,8 +229,23 @@ class Meta: 'trashed_at' ] + def create(self, validated_data): + # Extract subject_tags from validated_data + subject_tags_data = validated_data.pop('subject_tags', []) + + # Create the subject instance + subject_instance = Subject.objects.create(**validated_data) + + # Create corresponding tags in SubjectTags table + for tag_data in subject_tags_data: + SubjectTags.objects.create(subject=subject_instance, tag=tag_data) + + return subject_instance + class NewSubjectSerializer(serializers.ModelSerializer): + subject_tags = serializers.ListField(write_only=True, required=False) + class Meta: model = Subject fields = [ @@ -222,12 +256,35 @@ class Meta: 'gender', 'sex_at_birth', 'characteristics', + 'subject_tags', ] def to_representation(self, instance): serializer = SubjectSerializer(instance) return serializer.data + def create(self, validated_data): + # Extract subject_tags from validated_data + subject_tags_data = validated_data.pop('subject_tags', []) + + # Create the subject instance + subject_instance = Subject.objects.create(**validated_data) + + # Insert new tags. + for tag_data in subject_tags_data: + SubjectTags.objects.create(subject=subject_instance, tag=tag_data) + + return subject_instance + + +class TagSerializer(serializers.ModelSerializer): + class Meta: + model = SubjectTags + fields = [ + 'tag', + 'subject', + ] + class AnalysisFunctionSerializer(serializers.ModelSerializer): class Meta: diff --git a/mcserver/urls.py b/mcserver/urls.py index 72e8faa..5fed26c 100644 --- a/mcserver/urls.py +++ b/mcserver/urls.py @@ -21,6 +21,7 @@ TrialViewSet, ResultViewSet, SubjectViewSet, + SubjectTagViewSet, DownloadFileOnReadyAPIView, UserCreate, UserDelete, @@ -57,6 +58,7 @@ router.register(r'trials', TrialViewSet) router.register(r'results', ResultViewSet) router.register(r'subjects', SubjectViewSet, "subject") +router.register(r'subject-tags', SubjectTagViewSet, "subject-tags") router.register(r'users', UserViewSet) router.register(r'analysis-dashboards', AnalysisDashboardViewSet, "analysis-dashboard") @@ -114,5 +116,8 @@ AnalysisFunctionsStatesForTrialsAPIView.as_view(), name='analysis-results-statuses-for-trials' ), -# path('accounts/login/', OTPAuthenticationForm.as_view(authentication_form=OTPAuthenticationForm)), + path('subject-tags//get_tags_subject/', SubjectTagViewSet.as_view({'get': 'get_tags_subject'}), + name='get_tags_subject'), + + # path('accounts/login/', OTPAuthenticationForm.as_view(authentication_form=OTPAuthenticationForm)), ] diff --git a/mcserver/views.py b/mcserver/views.py index 781e21d..b18d968 100644 --- a/mcserver/views.py +++ b/mcserver/views.py @@ -33,6 +33,7 @@ Result, ResetPassword, Subject, + SubjectTags, DownloadLog, AnalysisFunction, AnalysisResult, @@ -56,7 +57,8 @@ AnalysisDashboardTemplateSerializer, AnalysisDashboardSerializer, ProfilePictureSerializer, - UserInstitutionalUseSerializer, + UserInstitutionalUseSerializer, + TagSerializer ) from mcserver.utils import send_otp_challenge from mcserver.zipsession import downloadAndZipSession, downloadAndZipSubject @@ -1315,7 +1317,75 @@ def neutral_img(self, request, pk): raise APIException(_("neutral_image_retrieve_error") % {"uuid": str(pk)}) return Response(data) - + + + @action(detail=False, methods=['post'], permission_classes=[IsAdmin | IsBackend | IsOwner]) + def get_session_statuses(self, request): + from .serializers import SessionIdSerializer, SessionFilteringSerializer + try: + filtering_serializer = SessionFilteringSerializer(data=request.data) + serializer = SessionIdSerializer(Session.objects.none(), many=True) + if filtering_serializer.is_valid(): + status_str = filtering_serializer.validated_data.get('status') + date_range = filtering_serializer.validated_data.get('date_range') + filter_kwargs = {'status': status_str} + if date_range: + filter_kwargs['status_changed__gte'] = date_range[0] + filter_kwargs['status_changed__lte'] = date_range[1] + if not IsAdmin().has_permission(request, self) and not IsBackend().has_permission(request, self): + filter_kwargs['user'] = request.user + else: + if 'username' in filtering_serializer.validated_data: + filter_kwargs['user__username'] = filtering_serializer.validated_data.get('username') + + sessions = Session.objects.filter(**filter_kwargs) + serializer = SessionIdSerializer(sessions, many=True) + except Http404: + if settings.DEBUG: + raise Exception(_("error") % {"error_message": str(traceback.format_exc())}) + raise NotFound(_("session_uuid_not_found") % {"uuid": str(pk)}) + except ValueError: + if settings.DEBUG: + raise Exception(_("error") % {"error_message": str(traceback.format_exc())}) + raise NotFound(_("session_uuid_not_valid") % {"uuid": str(pk)}) + except Exception: + if settings.DEBUG: + raise Exception(_("error") % {"error_message": str(traceback.format_exc())}) + raise APIException(_("session_remove_error")) + + return Response(serializer.data) + + + @action(detail=True, methods=['post'], permission_classes=[IsAdmin | IsBackend]) + def set_session_status(self, request, pk): + from .serializers import SessionStatusSerializer + try: + if pk == 'undefined': + raise ValueError(_("undefined_uuid")) + + session = get_object_or_404(Session, pk=pk) + serializer = SessionStatusSerializer(data=request.data) + if serializer.is_valid(): + session.status = serializer.validated_data['status'] + session.status_changed = timezone.now() + session.save() + + except Http404: + if settings.DEBUG: + raise Exception(_("error") % {"error_message": str(traceback.format_exc())}) + raise NotFound(_("session_uuid_not_found") % {"uuid": str(pk)}) + except ValueError: + if settings.DEBUG: + raise Exception(_("error") % {"error_message": str(traceback.format_exc())}) + raise NotFound(_("session_uuid_not_valid") % {"uuid": str(pk)}) + except Exception: + if settings.DEBUG: + raise Exception(_("error") % {"error_message": str(traceback.format_exc())}) + raise APIException(_("session_remove_error")) + + return Response(serializer.data) + + ## Processing machine: # A worker asks whether there is any trial to process @@ -1745,6 +1815,59 @@ def perform_create(self, serializer): raise Exception(_("error") % {"error_message": str(traceback.format_exc())}) raise APIException(_('subject_create_error')) + def perform_update(self, serializer): + try: + serializer.save() + + tags = serializer.context['request'].data['subject_tags'] + + # Get current subject. + subject = Subject.objects.get(id=serializer.context['request'].data['id']) + + # Remove previous tags. + SubjectTags.objects.filter(subject=subject).delete() + + # Insert new tags. + for tag in tags: + SubjectTags.objects.create(subject=subject, tag=tag) + + print(tags) + except Exception: + if settings.DEBUG: + raise Exception(_("error") % {"error_message": str(traceback.format_exc())}) + raise APIException(_('subject_update_error')) + +class SubjectTagViewSet(viewsets.ModelViewSet): + permission_classes = [IsOwner | IsAdmin | IsBackend] + serializer_class = TagSerializer + + def get_queryset(self): + """ + This view should return a list of all the subjects tags + for the currently authenticated user. + """ + user = self.request.user + # Get all subjects associated to a user. + subject = Subject.objects.filter(user=self.request.user) + + # Get tags associated to those subjects. + tags = SubjectTags.objects.filter(subject__in=list(subject)) + + return tags + + @action(detail=False, methods=['get']) + def get_tags_subject(self, request, subject_id): + # Get subject associated to that id. + subject = Subject.objects.get(id=subject_id, user=self.request.user) + + # Get tags associated to the subject. + tags = list(SubjectTags.objects.filter(subject=subject).values()) + + return Response(tags, status=200) + + + + class DownloadFileOnReadyAPIView(APIView): permission_classes = (AllowAny,)