From 9dd779f4fa6b954919de731ac207122a5af5f868 Mon Sep 17 00:00:00 2001 From: Phuoc Anh Kha Le <76896393+anhkha2003@users.noreply.github.com> Date: Wed, 17 Jul 2024 21:05:42 -0500 Subject: [PATCH] Add image uploading feature for organization (#122) --- dmoj/settings.py | 1 + judge/admin/organization.py | 1 + judge/forms.py | 23 +++++++- judge/migrations/0189_organization_image.py | 21 +++++++ judge/models/profile.py | 7 +++ judge/scripts/migrate_organization_image.py | 64 +++++++++++++++++++++ judge/views/contests.py | 2 +- judge/views/organization.py | 2 +- templates/organization/form.html | 2 +- templates/organization/home.html | 5 +- templates/organization/list.html | 4 +- templates/organization/tag.html | 4 +- templates/recent-organization.html | 4 +- templates/site-logo-fragment.html | 4 +- 14 files changed, 130 insertions(+), 14 deletions(-) create mode 100644 judge/migrations/0189_organization_image.py create mode 100644 judge/scripts/migrate_organization_image.py diff --git a/dmoj/settings.py b/dmoj/settings.py index ee4d1a1..2867a2a 100644 --- a/dmoj/settings.py +++ b/dmoj/settings.py @@ -85,6 +85,7 @@ DMOJ_STATS_SUBMISSION_RESULT_COLORS = { "ERR": "#ffa71c", } DMOJ_PROFILE_IMAGE_ROOT = "profile_images" +DMOJ_ORGANIZATION_IMAGE_ROOT = "organization_images" DMOJ_TEST_FORMATTER_ROOT = "test_formatter" MARKDOWN_STYLES = {} diff --git a/judge/admin/organization.py b/judge/admin/organization.py index b9d1c0b..a6ccc8b 100644 --- a/judge/admin/organization.py +++ b/judge/admin/organization.py @@ -33,6 +33,7 @@ class OrganizationAdmin(VersionAdmin): "short_name", "is_open", "about", + "organization_image", "logo_override_image", "slots", "registrant", diff --git a/judge/forms.py b/judge/forms.py index 832e042..baa92a8 100644 --- a/judge/forms.py +++ b/judge/forms.py @@ -195,16 +195,32 @@ class EditOrganizationForm(ModelForm): "slug", "short_name", "about", - "logo_override_image", + "organization_image", "admins", "is_open", ] - widgets = {"admins": Select2MultipleWidget()} + widgets = { + "admins": Select2MultipleWidget(), + "organization_image": ImageWidget, + } if HeavyPreviewPageDownWidget is not None: widgets["about"] = HeavyPreviewPageDownWidget( preview=reverse_lazy("organization_preview") ) + def __init__(self, *args, **kwargs): + super(EditOrganizationForm, self).__init__(*args, **kwargs) + self.fields["organization_image"].required = False + + def clean_organization_image(self): + organization_image = self.cleaned_data.get("organization_image") + if organization_image: + if organization_image.size > 5 * 1024 * 1024: + raise ValidationError( + _("File size exceeds the maximum allowed limit of 5MB.") + ) + return organization_image + class AddOrganizationForm(ModelForm): class Meta: @@ -214,7 +230,7 @@ class AddOrganizationForm(ModelForm): "slug", "short_name", "about", - "logo_override_image", + "organization_image", "is_open", ] widgets = {} @@ -226,6 +242,7 @@ class AddOrganizationForm(ModelForm): def __init__(self, *args, **kwargs): self.request = kwargs.pop("request", None) super(AddOrganizationForm, self).__init__(*args, **kwargs) + self.fields["organization_image"].required = False def save(self, commit=True): res = super(AddOrganizationForm, self).save(commit=False) diff --git a/judge/migrations/0189_organization_image.py b/judge/migrations/0189_organization_image.py new file mode 100644 index 0000000..4307a24 --- /dev/null +++ b/judge/migrations/0189_organization_image.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.21 on 2024-07-08 00:05 + +from django.db import migrations, models +import judge.models.profile + + +class Migration(migrations.Migration): + + dependencies = [ + ("judge", "0188_official_contest"), + ] + + operations = [ + migrations.AddField( + model_name="organization", + name="organization_image", + field=models.ImageField( + null=True, upload_to=judge.models.profile.organization_image_path + ), + ), + ] diff --git a/judge/models/profile.py b/judge/models/profile.py index 02a87bd..412e77b 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -48,6 +48,12 @@ def profile_image_path(profile, filename): return os.path.join(settings.DMOJ_PROFILE_IMAGE_ROOT, new_filename) +def organization_image_path(organization, filename): + tail = filename.split(".")[-1] + new_filename = f"organization_{organization.id}.{tail}" + return os.path.join(settings.DMOJ_ORGANIZATION_IMAGE_ROOT, new_filename) + + class Organization(models.Model): name = models.CharField(max_length=128, verbose_name=_("organization title")) slug = models.SlugField( @@ -104,6 +110,7 @@ class Organization(models.Model): null=True, blank=True, ) + organization_image = models.ImageField(upload_to=organization_image_path, null=True) logo_override_image = models.CharField( verbose_name=_("Logo override image"), default="", diff --git a/judge/scripts/migrate_organization_image.py b/judge/scripts/migrate_organization_image.py new file mode 100644 index 0000000..9134d21 --- /dev/null +++ b/judge/scripts/migrate_organization_image.py @@ -0,0 +1,64 @@ +# Download organization images from "logo_override_image" and upload to organization_images folder to use "organization_image" +# In folder online_judge, run python3 manage.py shell < judge/scripts/migrate_organization_image.py + +import os +import requests +from urllib.parse import urlparse +from django.core.files.base import ContentFile +from django.core.files.storage import default_storage +from django.conf import settings +from django.db import transaction +from judge.models import Organization + + +def is_valid_image_url(url): + try: + parsed_url = urlparse(url) + _, ext = os.path.splitext(parsed_url.path) + return ext.lower() in [".jpg", ".jpeg", ".png", ".gif", ".svg"] + except Exception as e: + return False + + +def download_image(url): + response = requests.get(url) + response.raise_for_status() + return ContentFile(response.content) + + +def organization_image_path(organization, filename): + tail = filename.split(".")[-1] + new_filename = f"organization_{organization.id}.{tail}" + return os.path.join(settings.DMOJ_ORGANIZATION_IMAGE_ROOT, new_filename) + + +@transaction.atomic +def migrate_images(): + print("Start") + organizations = Organization.objects.all() + for org in organizations: + if org.logo_override_image: + if is_valid_image_url(org.logo_override_image): + try: + # Download the image + image_content = download_image(org.logo_override_image) + # Determine the file extension + file_ext = org.logo_override_image.split(".")[-1] + filename = f"organization_{org.id}.{file_ext}" + # Save the image to the new location + new_path = organization_image_path(org, filename) + saved_path = default_storage.save(new_path, image_content) + # Update the organization_image field + org.organization_image = saved_path + org.save() + print(f"Image for organization {org.id} migrated successfully.") + except Exception as e: + print(f"Failed to migrate image for organization {org.id}: {e}") + else: + print( + f"Invalid image URL for organization {org.id}: {org.logo_override_image}" + ) + print("Finish") + + +migrate_images() diff --git a/judge/views/contests.py b/judge/views/contests.py index 1b5a2be..e1ca582 100644 --- a/judge/views/contests.py +++ b/judge/views/contests.py @@ -423,7 +423,7 @@ class ContestMixin(object): ): context[ "logo_override_image" - ] = self.object.organizations.first().logo_override_image + ] = self.object.organizations.first().organization_image.url return context diff --git a/judge/views/organization.py b/judge/views/organization.py index ddfce76..37bfdb3 100644 --- a/judge/views/organization.py +++ b/judge/views/organization.py @@ -130,7 +130,7 @@ class OrganizationMixin(OrganizationBase): context["is_admin"] = self.is_admin(self.organization) context["can_edit"] = self.can_edit_organization(self.organization) context["organization"] = self.organization - context["logo_override_image"] = self.organization.logo_override_image + context["organization_image"] = self.organization.organization_image context["organization_subdomain"] = ( ("http" if settings.DMOJ_SSL == 0 else "https") + "://" diff --git a/templates/organization/form.html b/templates/organization/form.html index 167704e..d2598ec 100644 --- a/templates/organization/form.html +++ b/templates/organization/form.html @@ -1,4 +1,4 @@ -