Loading trapper/test-requirements.txt +1 −0 Original line number Diff line number Diff line coverage httpx>=0.28.1 pytest-django pytest-xdist==3.4.0 factory-boy==2.12.0 Loading trapper/trapper-project/trapper/apps/accounts/admin.py +66 −1 Original line number Diff line number Diff line Loading @@ -12,8 +12,10 @@ from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import Permission, ContentType from django.core.exceptions import ObjectDoesNotExist from django.core.mail import send_mail from django.db.models import Sum from django.shortcuts import render from django.utils.safestring import mark_safe from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ from trapper.apps.research.taxonomy import ResearchProjectRoleType Loading @@ -30,6 +32,7 @@ from trapper.apps.accounts.models import ( UserDataPackage, UserFTPAccount, UserHttpUploaderAccount, UserHttpUploaderAccountFiles, ) from trapper.apps.media_classification.models import ( ClassificationProjectRole, Loading Loading @@ -488,9 +491,51 @@ class UserFTPAccountAdmin(admin.ModelAdmin): ] @admin.register(UserHttpUploaderAccountFiles) class UserHttpUploaderAccountFilesAdmin(admin.ModelAdmin): list_display = ( "file_name", "file_type", "file_size_mb", "account", "uploaded_at", "download_link", ) list_filter = ("file_type", "uploaded_at", "account") search_fields = ("file_name", "account__user__username", "account__user__email") readonly_fields = ( "file_size", "uploaded_at", "download_link", ) raw_id_fields = ("account",) ordering = ("-uploaded_at",) def download_link(self, obj): if obj.pk: return format_html('<a href="{}">Download</a>', obj.get_download_url()) return "-" download_link.short_description = "Download" def file_size_mb(self, obj): mb = obj.file_size / (1024**2) return f"{mb:.2f} MB" file_size_mb.short_description = _("File Size (MB)") @admin.register(UserHttpUploaderAccount) class UserHttpUploaderAccountAdmin(admin.ModelAdmin): list_display = ("user", "quota_files", "quota_size", "created_at", "updated_at") list_display = ( "user", "quota_files", "quota_size_gb", "created_at", "updated_at", "uploaded_files_count", "total_file_size_gb", ) search_fields = ("user__username", "user__email") readonly_fields = [ "created_at", Loading @@ -498,6 +543,26 @@ class UserHttpUploaderAccountAdmin(admin.ModelAdmin): ] raw_id_fields = ("user",) def uploaded_files_count(self, obj): return obj.files.count() uploaded_files_count.short_description = _("Uploaded Files") def quota_size_gb(self, obj): gb = obj.quota_size / (1024**3) return f"{gb:.2f} GB" def total_file_size_gb(self, obj): total_bytes = ( obj.files.aggregate(total_size=Sum("file_size"))["total_size"] or 0 ) gb = total_bytes / (1024**3) quota_gb = obj.quota_size / (1024**3) percent = (gb / quota_gb * 100) if quota_gb > 0 else 0 return f"{gb:.2f} GB ({percent:.1f}%)" total_file_size_gb.short_description = _("Total File Size (GB)") admin.site.register(User, TrapperUserAdmin) admin.site.register(UserProfile) Loading trapper/trapper-project/trapper/apps/accounts/migrations/0029_userhttpuploaderaccountfiles.py 0 → 100644 +32 −0 Original line number Diff line number Diff line # Generated by Django 4.2.17 on 2025-10-22 23:17 import django.core.files.storage from django.db import migrations, models import django.db.models.deletion import trapper.apps.accounts.models class Migration(migrations.Migration): dependencies = [ ('accounts', '0028_alter_userftpaccount_quota_files_and_more'), ] operations = [ migrations.CreateModel( name='UserHttpUploaderAccountFiles', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('file_name', models.CharField(max_length=255)), ('file_type', models.CharField(choices=[('zip', 'ZIP package'), ('yaml', 'YAML definition file'), ('log', 'Log file'), ('other', 'Other file type')], max_length=10)), ('file_size', models.BigIntegerField(default=0)), ('file', models.FileField(storage=django.core.files.storage.FileSystemStorage(location='/app/external_media'), upload_to=trapper.apps.accounts.models.user_http_uploader_file_path)), ('uploaded_at', models.DateTimeField(auto_now_add=True)), ('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='accounts.userhttpuploaderaccount')), ], options={ 'verbose_name': 'User HTTP uploader account file', 'verbose_name_plural': 'User HTTP uploader account files', }, ), ] trapper/trapper-project/trapper/apps/accounts/migrations/0030_alter_userhttpuploaderaccountfiles_file_size.py 0 → 100644 +18 −0 Original line number Diff line number Diff line # Generated by Django 4.2.17 on 2025-10-28 10:24 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('accounts', '0029_userhttpuploaderaccountfiles'), ] operations = [ migrations.AlterField( model_name='userhttpuploaderaccountfiles', name='file_size', field=models.BigIntegerField(blank=True, default=0), ), ] trapper/trapper-project/trapper/apps/accounts/models.py +60 −2 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ from botocore.exceptions import ClientError from django.conf import settings from django.contrib.auth.models import AbstractUser from django.core.exceptions import ObjectDoesNotExist from django.core.files.storage import FileSystemStorage from django.core.mail import send_mail from django.db import models from django.db.models.signals import pre_save, post_save, post_delete Loading @@ -32,7 +33,10 @@ from trapper.apps.accounts.taxonomy import ( PackageType, UserRemoteTaskStatus, ) from trapper.apps.accounts.utils import get_pretty_username, create_external_media from trapper.apps.accounts.utils import ( get_pretty_username, create_external_media, ) from trapper.apps.common.fields import ResizedImageField from trapper.apps.common.fields import SafeTextField from trapper.apps.common.utils.models import delete_old_file Loading Loading @@ -493,6 +497,61 @@ class UserHttpUploaderAccount(models.Model): verbose_name = _("User HTTP uploader account") verbose_name_plural = _("User HTTP uploader accounts") def __str__(self): return f"{self.user.username}" def user_http_uploader_file_path(instance, filename): """ Generate relative path for uploaded files, suitable for FileSystemStorage. Files will be stored in user-specific collections directory. """ return os.path.join(instance.account.user.username, "collections", filename) UserHttpUploaderAccountFileTypes = [ ("zip", "ZIP package"), ("yaml", "YAML definition file"), ("log", "Log file"), ("other", "Other file type"), ] class UserHttpUploaderAccountFiles(models.Model): account = models.ForeignKey( UserHttpUploaderAccount, on_delete=models.CASCADE, related_name="files" ) file_name = models.CharField(max_length=255) file_type = models.CharField( max_length=10, choices=UserHttpUploaderAccountFileTypes ) file_size = models.BigIntegerField(default=0, blank=True) file = models.FileField( upload_to=user_http_uploader_file_path, storage=FileSystemStorage(location=settings.EXTERNAL_MEDIA_ROOT), ) uploaded_at = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = _("User HTTP uploader account file") verbose_name_plural = _("User HTTP uploader account files") def save(self, *args, **kwargs): # Set file_size before saving if not self.file_size and self.file and hasattr(self.file, "size"): self.file_size = self.file.size return super().save(*args, **kwargs) def get_download_url(self): """Get secure download URL for the file""" return reverse("accounts:http_uploader_file_download", kwargs={"pk": self.pk}) @receiver(post_delete, sender=UserHttpUploaderAccountFiles) def delete_http_uploader_file(sender, instance, **kwargs): """Delete the file from storage when the model instance is deleted""" instance.file.delete(False) # Automatically mark citizen science users as demo when DEMO_MODE is active. @receiver(post_save, sender=UserProfile) Loading @@ -500,7 +559,6 @@ def ensure_demo_flag(sender, instance, created, **kwargs): """ Ensure that demo flag is set for citizen science users when DEMO_MODE is active. """ # If demo flag enabled -> ensure FTP account exists if settings.DEMO_MODE and instance.user.is_citizen_science: # Set demo flag on profile if not set Loading Loading
trapper/test-requirements.txt +1 −0 Original line number Diff line number Diff line coverage httpx>=0.28.1 pytest-django pytest-xdist==3.4.0 factory-boy==2.12.0 Loading
trapper/trapper-project/trapper/apps/accounts/admin.py +66 −1 Original line number Diff line number Diff line Loading @@ -12,8 +12,10 @@ from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import Permission, ContentType from django.core.exceptions import ObjectDoesNotExist from django.core.mail import send_mail from django.db.models import Sum from django.shortcuts import render from django.utils.safestring import mark_safe from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ from trapper.apps.research.taxonomy import ResearchProjectRoleType Loading @@ -30,6 +32,7 @@ from trapper.apps.accounts.models import ( UserDataPackage, UserFTPAccount, UserHttpUploaderAccount, UserHttpUploaderAccountFiles, ) from trapper.apps.media_classification.models import ( ClassificationProjectRole, Loading Loading @@ -488,9 +491,51 @@ class UserFTPAccountAdmin(admin.ModelAdmin): ] @admin.register(UserHttpUploaderAccountFiles) class UserHttpUploaderAccountFilesAdmin(admin.ModelAdmin): list_display = ( "file_name", "file_type", "file_size_mb", "account", "uploaded_at", "download_link", ) list_filter = ("file_type", "uploaded_at", "account") search_fields = ("file_name", "account__user__username", "account__user__email") readonly_fields = ( "file_size", "uploaded_at", "download_link", ) raw_id_fields = ("account",) ordering = ("-uploaded_at",) def download_link(self, obj): if obj.pk: return format_html('<a href="{}">Download</a>', obj.get_download_url()) return "-" download_link.short_description = "Download" def file_size_mb(self, obj): mb = obj.file_size / (1024**2) return f"{mb:.2f} MB" file_size_mb.short_description = _("File Size (MB)") @admin.register(UserHttpUploaderAccount) class UserHttpUploaderAccountAdmin(admin.ModelAdmin): list_display = ("user", "quota_files", "quota_size", "created_at", "updated_at") list_display = ( "user", "quota_files", "quota_size_gb", "created_at", "updated_at", "uploaded_files_count", "total_file_size_gb", ) search_fields = ("user__username", "user__email") readonly_fields = [ "created_at", Loading @@ -498,6 +543,26 @@ class UserHttpUploaderAccountAdmin(admin.ModelAdmin): ] raw_id_fields = ("user",) def uploaded_files_count(self, obj): return obj.files.count() uploaded_files_count.short_description = _("Uploaded Files") def quota_size_gb(self, obj): gb = obj.quota_size / (1024**3) return f"{gb:.2f} GB" def total_file_size_gb(self, obj): total_bytes = ( obj.files.aggregate(total_size=Sum("file_size"))["total_size"] or 0 ) gb = total_bytes / (1024**3) quota_gb = obj.quota_size / (1024**3) percent = (gb / quota_gb * 100) if quota_gb > 0 else 0 return f"{gb:.2f} GB ({percent:.1f}%)" total_file_size_gb.short_description = _("Total File Size (GB)") admin.site.register(User, TrapperUserAdmin) admin.site.register(UserProfile) Loading
trapper/trapper-project/trapper/apps/accounts/migrations/0029_userhttpuploaderaccountfiles.py 0 → 100644 +32 −0 Original line number Diff line number Diff line # Generated by Django 4.2.17 on 2025-10-22 23:17 import django.core.files.storage from django.db import migrations, models import django.db.models.deletion import trapper.apps.accounts.models class Migration(migrations.Migration): dependencies = [ ('accounts', '0028_alter_userftpaccount_quota_files_and_more'), ] operations = [ migrations.CreateModel( name='UserHttpUploaderAccountFiles', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('file_name', models.CharField(max_length=255)), ('file_type', models.CharField(choices=[('zip', 'ZIP package'), ('yaml', 'YAML definition file'), ('log', 'Log file'), ('other', 'Other file type')], max_length=10)), ('file_size', models.BigIntegerField(default=0)), ('file', models.FileField(storage=django.core.files.storage.FileSystemStorage(location='/app/external_media'), upload_to=trapper.apps.accounts.models.user_http_uploader_file_path)), ('uploaded_at', models.DateTimeField(auto_now_add=True)), ('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='accounts.userhttpuploaderaccount')), ], options={ 'verbose_name': 'User HTTP uploader account file', 'verbose_name_plural': 'User HTTP uploader account files', }, ), ]
trapper/trapper-project/trapper/apps/accounts/migrations/0030_alter_userhttpuploaderaccountfiles_file_size.py 0 → 100644 +18 −0 Original line number Diff line number Diff line # Generated by Django 4.2.17 on 2025-10-28 10:24 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('accounts', '0029_userhttpuploaderaccountfiles'), ] operations = [ migrations.AlterField( model_name='userhttpuploaderaccountfiles', name='file_size', field=models.BigIntegerField(blank=True, default=0), ), ]
trapper/trapper-project/trapper/apps/accounts/models.py +60 −2 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ from botocore.exceptions import ClientError from django.conf import settings from django.contrib.auth.models import AbstractUser from django.core.exceptions import ObjectDoesNotExist from django.core.files.storage import FileSystemStorage from django.core.mail import send_mail from django.db import models from django.db.models.signals import pre_save, post_save, post_delete Loading @@ -32,7 +33,10 @@ from trapper.apps.accounts.taxonomy import ( PackageType, UserRemoteTaskStatus, ) from trapper.apps.accounts.utils import get_pretty_username, create_external_media from trapper.apps.accounts.utils import ( get_pretty_username, create_external_media, ) from trapper.apps.common.fields import ResizedImageField from trapper.apps.common.fields import SafeTextField from trapper.apps.common.utils.models import delete_old_file Loading Loading @@ -493,6 +497,61 @@ class UserHttpUploaderAccount(models.Model): verbose_name = _("User HTTP uploader account") verbose_name_plural = _("User HTTP uploader accounts") def __str__(self): return f"{self.user.username}" def user_http_uploader_file_path(instance, filename): """ Generate relative path for uploaded files, suitable for FileSystemStorage. Files will be stored in user-specific collections directory. """ return os.path.join(instance.account.user.username, "collections", filename) UserHttpUploaderAccountFileTypes = [ ("zip", "ZIP package"), ("yaml", "YAML definition file"), ("log", "Log file"), ("other", "Other file type"), ] class UserHttpUploaderAccountFiles(models.Model): account = models.ForeignKey( UserHttpUploaderAccount, on_delete=models.CASCADE, related_name="files" ) file_name = models.CharField(max_length=255) file_type = models.CharField( max_length=10, choices=UserHttpUploaderAccountFileTypes ) file_size = models.BigIntegerField(default=0, blank=True) file = models.FileField( upload_to=user_http_uploader_file_path, storage=FileSystemStorage(location=settings.EXTERNAL_MEDIA_ROOT), ) uploaded_at = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = _("User HTTP uploader account file") verbose_name_plural = _("User HTTP uploader account files") def save(self, *args, **kwargs): # Set file_size before saving if not self.file_size and self.file and hasattr(self.file, "size"): self.file_size = self.file.size return super().save(*args, **kwargs) def get_download_url(self): """Get secure download URL for the file""" return reverse("accounts:http_uploader_file_download", kwargs={"pk": self.pk}) @receiver(post_delete, sender=UserHttpUploaderAccountFiles) def delete_http_uploader_file(sender, instance, **kwargs): """Delete the file from storage when the model instance is deleted""" instance.file.delete(False) # Automatically mark citizen science users as demo when DEMO_MODE is active. @receiver(post_save, sender=UserProfile) Loading @@ -500,7 +559,6 @@ def ensure_demo_flag(sender, instance, created, **kwargs): """ Ensure that demo flag is set for citizen science users when DEMO_MODE is active. """ # If demo flag enabled -> ensure FTP account exists if settings.DEMO_MODE and instance.user.is_citizen_science: # Set demo flag on profile if not set Loading