Commit 810bbedb authored by Jakub D's avatar Jakub D
Browse files

Merge branch 'development' into fix/celery-enabled-switch-for-media-classification-models

parents e212fdd0 fad4ce52
Loading
Loading
Loading
Loading
+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
+66 −1
Original line number Diff line number Diff line
@@ -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
@@ -30,6 +32,7 @@ from trapper.apps.accounts.models import (
    UserDataPackage,
    UserFTPAccount,
    UserHttpUploaderAccount,
    UserHttpUploaderAccountFiles,
)
from trapper.apps.media_classification.models import (
    ClassificationProjectRole,
@@ -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",
@@ -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)
+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',
            },
        ),
    ]
+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),
        ),
    ]
+60 −2
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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)
@@ -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