diff --git a/exact/exact/datasets/views.py b/exact/exact/datasets/views.py index 58ff5932..4c74b94c 100644 --- a/exact/exact/datasets/views.py +++ b/exact/exact/datasets/views.py @@ -18,7 +18,7 @@ from exact.administration.models import Product from exact.annotations.models import Annotation, AnnotationType from exact.datasets.forms import DatasetForm, MITOS_WSI_CMCDatasetForm, CATCH_DatasetForm -from exact.images.models import ImageSet, Image +from exact.images.models import AuxiliaryFile, ImageSet, Image from exact.images.views import view_imageset diff --git a/exact/exact/images/admin.py b/exact/exact/images/admin.py index 53cbda5f..1907fcf0 100644 --- a/exact/exact/images/admin.py +++ b/exact/exact/images/admin.py @@ -1,8 +1,9 @@ from django.contrib import admin # Register your models here. -from .models import Image, ImageSet, SetTag +from .models import Image, ImageSet, SetTag, AuxiliaryFile admin.site.register(ImageSet) admin.site.register(Image) admin.site.register(SetTag) +admin.site.register(AuxiliaryFile) diff --git a/exact/exact/images/api_views.py b/exact/exact/images/api_views.py index 3247443e..1b4825b4 100644 --- a/exact/exact/images/api_views.py +++ b/exact/exact/images/api_views.py @@ -114,6 +114,10 @@ def filter_annotation_type(self, queryset, field_name, value): return queryset +class AuxiliaryFileViewSet(viewsets.ModelViewSet): + permission_classes = [permissions.DjangoModelPermissions] + serializer_class = serializers.AuxiliaryFileSerializer + class ImageViewSet(viewsets.ModelViewSet): permission_classes = [permissions.DjangoModelPermissions] @@ -458,13 +462,22 @@ def create(self, request): path = Path(path) name = path.name - image = models.Image( - name=name, - image_set=imageset, - checksum=fchecksum) + if (Path(path).suffix.lower().endswith(".csv") or Path(path).suffix.lower().endswith(".txt") or + Path(path).suffix.lower().endswith(".json") or Path(path).suffix.lower().endswith(".sqlite")): + # This is an auxiliary file, not an image. + newFile = models.AuxiliaryFile(image_set=imageset, name=name, filesize=os.path.getsize(path)) + images.append(newFile) + + else: + + + image = models.Image( + name=name, + image_set=imageset, + checksum=fchecksum) - image.save_file(path) - images.append(image) + image.save_file(path) + images.append(image) except Exception as e: errors.append(e.message) diff --git a/exact/exact/images/migrations/0035_auxiliaryfile.py b/exact/exact/images/migrations/0035_auxiliaryfile.py new file mode 100644 index 00000000..3725259d --- /dev/null +++ b/exact/exact/images/migrations/0035_auxiliaryfile.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.18 on 2025-01-21 09:20 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('images', '0034_alter_framedescription_description_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='AuxiliaryFile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=256)), + ('filesize', models.IntegerField()), + ('location', models.CharField(blank=True, max_length=256, null=True)), + ('description', models.TextField(blank=True, max_length=1000, null=True)), + ('time', models.DateTimeField(auto_now_add=True)), + ('associated_to_image', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='images.image')), + ('creator', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ('image_set', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='auxiliary_files', to='images.imageset')), + ], + ), + ] diff --git a/exact/exact/images/models.py b/exact/exact/images/models.py index 8da279f3..f338be43 100644 --- a/exact/exact/images/models.py +++ b/exact/exact/images/models.py @@ -367,10 +367,10 @@ def save_file(self, path:Path): raise def __str__(self): - return u'Image: {0}'.format(self.name) + return u'Image: {0} (Image Set: {1}, Team: {2})'.format(self.name, self.image_set.name, self.image_set.team.name) def __repr__(self): - return u'Image: {0}'.format(self.name) + return u'Image: {0} (Image Set: {1}, Team: {2})'.format(self.name, self.image_set.name, self.image_set.team.name) # Image signals for del the cache @receiver([post_save, post_delete, m2m_changed], sender=Image) @@ -380,6 +380,40 @@ def image_changed_handler(sender, instance, **kwargs): if hasattr(cache, "delete_pattern"): cache.delete_pattern(f"*/api/v1/images/image_sets/{instance.image_set_id}/*") +class AuxiliaryFile(models.Model): + name = models.CharField(max_length=256) + filesize = models.IntegerField() + location = models.CharField(max_length=256, null=True, blank=True) + description = models.TextField(max_length=1000, null=True, blank=True) + associated_to_image = models.ForeignKey(Image, + on_delete=models.SET_NULL, + null=True) + time = models.DateTimeField(auto_now_add=True) + image_set = models.ForeignKey( + 'ImageSet', on_delete=models.CASCADE, related_name='auxiliary_files') + creator = models.ForeignKey(settings.AUTH_USER_MODEL, + default=None, + on_delete=models.SET_NULL, + null=True, + blank=True) + + + def path(self): + return os.path.join(self.image_set.root_path(), self.name) + + def delete(self, *args, **kwargs): + #self.image_set.zip_state = ImageSet.ZipState.INVALID + #self.image_set.save(update_fields=('zip_state',)) + file_path = Path(settings.IMAGE_PATH) / self.path() + if os.path.exists(file_path): + os.remove(file_path) + super(AuxiliaryFile, self).delete(*args, **kwargs) + + def __str__(self): + return '%s (ImageSet: %s, Team: %s) ' % (self.name, self.image_set.name, self.image_set.team.name) + + + class ImageSet(models.Model): class Meta: unique_together = [ @@ -537,7 +571,9 @@ def has_perm(self, permission: str, user: get_user_model()) -> bool: return permission in self.get_perms(user) def __str__(self): - return u'Imageset: {0}'.format(self.name) + return u'Imageset: {0} (Team: {1})'.format(self.name, self.team.name) + + @property def prio_symbol(self): diff --git a/exact/exact/images/serializers.py b/exact/exact/images/serializers.py index 00588421..b7a2d661 100644 --- a/exact/exact/images/serializers.py +++ b/exact/exact/images/serializers.py @@ -35,6 +35,28 @@ class Meta: "FrameDescriptions" : ('exact.images.serializers.FrameDescriptionSerializer', {'read_only': True, 'many':True}), } +class AuxiliaryFileSerializer(FlexFieldsModelSerializer): + class Meta: + model = Image + fields = ( + 'id', + 'name', + 'filename', + 'description', + 'creator', + 'associated_to_image', + 'time', + 'filesize', + 'image_set', + ) + + expandable_fields = { + "image_set": ('exact.images.serializers.ImageSetSerializer', {'read_only': True}), + "associated_to_image": ('exact.images.serializers.ImageSerializer', {'read_only': True}), + } + + + class SetTagSerializer(FlexFieldsModelSerializer): class Meta: diff --git a/exact/exact/images/templates/images/imageset.html b/exact/exact/images/templates/images/imageset.html index 6f89d08d..0fff49d5 100644 --- a/exact/exact/images/templates/images/imageset.html +++ b/exact/exact/images/templates/images/imageset.html @@ -235,6 +235,24 @@

Computer generated images

+ {% if imageset.auxiliary_files.all %} +
+
+

Auxiliary Files

+
+
+ +
+
+ {% endif %} +
diff --git a/exact/exact/images/templates/images/imageset_v2.html b/exact/exact/images/templates/images/imageset_v2.html index 053bae68..47607ad5 100644 --- a/exact/exact/images/templates/images/imageset_v2.html +++ b/exact/exact/images/templates/images/imageset_v2.html @@ -53,7 +53,11 @@ {% endverbatim %} {% verbatim %} - {% } else { %} + {% } else if (file.type == "auxfile") { %} + {% endverbatim %} + + {% verbatim %} + {% } else { %} {% endverbatim %} @@ -90,7 +94,7 @@ + +
+
+
+
diff --git a/exact/exact/images/urls.py b/exact/exact/images/urls.py index e28b1ab0..cbbd1497 100644 --- a/exact/exact/images/urls.py +++ b/exact/exact/images/urls.py @@ -12,6 +12,7 @@ re_path(r'^image/delete/(\d+)/$', views.delete_images, name='delete_images'), re_path(r'^api/image/delete/(\d+)/$', views.delete_images_api, name='delete images (API)'), re_path(r'^api/image/download/(\d+)/$', views.download_image_api, name='download_api'), + re_path(r'^api/auxfile/download/(\d+)/$', views.download_auxfile_api, name='download_auxfile'), re_path(r'^image/copy/(\d+)/(\d+)/$', views.copy_image, name='copy_image'), re_path(r'^api/image/opened/(\d+)/$', views.image_opened, name='image_opened'), diff --git a/exact/exact/images/views.py b/exact/exact/images/views.py index 30513b67..22279a36 100644 --- a/exact/exact/images/views.py +++ b/exact/exact/images/views.py @@ -29,7 +29,7 @@ from rest_framework.settings import api_settings from exact.images.api_views import ImageSetViewSet -from exact.images.serializers import ImageSetSerializer, ImageSerializer, SetTagSerializer, serialize_imageset +from exact.images.serializers import ImageSetSerializer, ImageSerializer, SetTagSerializer, serialize_imageset, AuxiliaryFileSerializer from exact.images.forms import ImageSetCreationForm, ImageSetCreationFormWT, ImageSetEditForm from exact.annotations.views import api_copy_annotation from exact.users.forms import TeamCreationForm @@ -38,7 +38,7 @@ from exact.administration.models import Product from exact.administration.serializers import ProductSerializer -from .models import ImageRegistration, ImageSet, Image, SetTag +from .models import ImageRegistration, ImageSet, Image, SetTag, AuxiliaryFile from .forms import LabelUploadForm, CopyImageSetForm from exact.annotations.models import Annotation, Export, ExportFormat, \ AnnotationType, Verification, LogImageAction @@ -313,12 +313,21 @@ def upload_image(request, imageset_id): path = Path(path) name = path.name - image = Image( - name=name, - image_set=imageset, - checksum=fchecksum) + if (Path(path).suffix.lower().endswith(".csv") or Path(path).suffix.lower().endswith(".txt") or + Path(path).suffix.lower().endswith(".json") or Path(path).suffix.lower().endswith(".sqlite")): + # This is an auxiliary file, not an image. - image.save_file(path) + image = AuxiliaryFile(image_set=imageset, name=name, filesize=os.path.getsize(path), creator = request.user) + image.save() + type='auxfile' + + else: + image = Image( + name=name, + image_set=imageset, + checksum=fchecksum) + type='image' + image.save_file(path) except Exception as e: import sys import traceback @@ -360,11 +369,8 @@ def upload_image(request, imageset_id): if errormessage == '': json_files.append({'name': f.name, 'size': f.size, + 'type': type, 'id' : image.id, - # 'url': reverse('images_imageview', args=(image.id, )), - # 'thumbnailUrl': reverse('images_imageview', args=(image.id, )), - # 'deleteUrl': reverse('images_imagedeleteview', args=(image.id, )), - # 'deleteType': "DELETE", }) else: json_files.append({'name': f.name, @@ -636,6 +642,26 @@ def download_image_api(request, image_id) -> Response: return response +@api_view(['GET']) +def download_auxfile_api(request, auxfile_id) -> Response: + original_image = request.GET.get("original_image", None) + image = get_object_or_404(AuxiliaryFile, id=auxfile_id) + if not image.image_set.has_perm('read', request.user): + return Response({'message': 'you do not have the permission to access this imageset' + }, status=HTTP_403_FORBIDDEN) + + file_path = Path(settings.IMAGE_PATH) / image.path() + if original_image is not None and 'True' == original_image: + file_path = Path(image.original_path()) + + + response = FileResponse(open(str(file_path), 'rb'), content_type='application/octet-stream') + + response['Content-Length'] = os.path.getsize(file_path) + response['Content-Disposition'] = "attachment; filename={}".format(file_path.name) + + return response + @api_view(['GET']) def delete_images_api(request, image_id) -> Response: image = get_object_or_404(Image, id=image_id) diff --git a/exact/exact/users/views.py b/exact/exact/users/views.py index 151e9941..117048a7 100644 --- a/exact/exact/users/views.py +++ b/exact/exact/users/views.py @@ -294,7 +294,6 @@ def user(request, user_id): - print('PW Matching: ',passwordmatching) return render(request, 'users/view_user.html', { 'user': user,