Add image uploading feature for organization (#122)
This commit is contained in:
parent
c00db58cb1
commit
9dd779f4fa
14 changed files with 130 additions and 14 deletions
|
@ -85,6 +85,7 @@ DMOJ_STATS_SUBMISSION_RESULT_COLORS = {
|
||||||
"ERR": "#ffa71c",
|
"ERR": "#ffa71c",
|
||||||
}
|
}
|
||||||
DMOJ_PROFILE_IMAGE_ROOT = "profile_images"
|
DMOJ_PROFILE_IMAGE_ROOT = "profile_images"
|
||||||
|
DMOJ_ORGANIZATION_IMAGE_ROOT = "organization_images"
|
||||||
DMOJ_TEST_FORMATTER_ROOT = "test_formatter"
|
DMOJ_TEST_FORMATTER_ROOT = "test_formatter"
|
||||||
|
|
||||||
MARKDOWN_STYLES = {}
|
MARKDOWN_STYLES = {}
|
||||||
|
|
|
@ -33,6 +33,7 @@ class OrganizationAdmin(VersionAdmin):
|
||||||
"short_name",
|
"short_name",
|
||||||
"is_open",
|
"is_open",
|
||||||
"about",
|
"about",
|
||||||
|
"organization_image",
|
||||||
"logo_override_image",
|
"logo_override_image",
|
||||||
"slots",
|
"slots",
|
||||||
"registrant",
|
"registrant",
|
||||||
|
|
|
@ -195,16 +195,32 @@ class EditOrganizationForm(ModelForm):
|
||||||
"slug",
|
"slug",
|
||||||
"short_name",
|
"short_name",
|
||||||
"about",
|
"about",
|
||||||
"logo_override_image",
|
"organization_image",
|
||||||
"admins",
|
"admins",
|
||||||
"is_open",
|
"is_open",
|
||||||
]
|
]
|
||||||
widgets = {"admins": Select2MultipleWidget()}
|
widgets = {
|
||||||
|
"admins": Select2MultipleWidget(),
|
||||||
|
"organization_image": ImageWidget,
|
||||||
|
}
|
||||||
if HeavyPreviewPageDownWidget is not None:
|
if HeavyPreviewPageDownWidget is not None:
|
||||||
widgets["about"] = HeavyPreviewPageDownWidget(
|
widgets["about"] = HeavyPreviewPageDownWidget(
|
||||||
preview=reverse_lazy("organization_preview")
|
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 AddOrganizationForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -214,7 +230,7 @@ class AddOrganizationForm(ModelForm):
|
||||||
"slug",
|
"slug",
|
||||||
"short_name",
|
"short_name",
|
||||||
"about",
|
"about",
|
||||||
"logo_override_image",
|
"organization_image",
|
||||||
"is_open",
|
"is_open",
|
||||||
]
|
]
|
||||||
widgets = {}
|
widgets = {}
|
||||||
|
@ -226,6 +242,7 @@ class AddOrganizationForm(ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.request = kwargs.pop("request", None)
|
self.request = kwargs.pop("request", None)
|
||||||
super(AddOrganizationForm, self).__init__(*args, **kwargs)
|
super(AddOrganizationForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields["organization_image"].required = False
|
||||||
|
|
||||||
def save(self, commit=True):
|
def save(self, commit=True):
|
||||||
res = super(AddOrganizationForm, self).save(commit=False)
|
res = super(AddOrganizationForm, self).save(commit=False)
|
||||||
|
|
21
judge/migrations/0189_organization_image.py
Normal file
21
judge/migrations/0189_organization_image.py
Normal file
|
@ -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
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -48,6 +48,12 @@ def profile_image_path(profile, filename):
|
||||||
return os.path.join(settings.DMOJ_PROFILE_IMAGE_ROOT, new_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):
|
class Organization(models.Model):
|
||||||
name = models.CharField(max_length=128, verbose_name=_("organization title"))
|
name = models.CharField(max_length=128, verbose_name=_("organization title"))
|
||||||
slug = models.SlugField(
|
slug = models.SlugField(
|
||||||
|
@ -104,6 +110,7 @@ class Organization(models.Model):
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
organization_image = models.ImageField(upload_to=organization_image_path, null=True)
|
||||||
logo_override_image = models.CharField(
|
logo_override_image = models.CharField(
|
||||||
verbose_name=_("Logo override image"),
|
verbose_name=_("Logo override image"),
|
||||||
default="",
|
default="",
|
||||||
|
|
64
judge/scripts/migrate_organization_image.py
Normal file
64
judge/scripts/migrate_organization_image.py
Normal file
|
@ -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()
|
|
@ -423,7 +423,7 @@ class ContestMixin(object):
|
||||||
):
|
):
|
||||||
context[
|
context[
|
||||||
"logo_override_image"
|
"logo_override_image"
|
||||||
] = self.object.organizations.first().logo_override_image
|
] = self.object.organizations.first().organization_image.url
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
|
@ -130,7 +130,7 @@ class OrganizationMixin(OrganizationBase):
|
||||||
context["is_admin"] = self.is_admin(self.organization)
|
context["is_admin"] = self.is_admin(self.organization)
|
||||||
context["can_edit"] = self.can_edit_organization(self.organization)
|
context["can_edit"] = self.can_edit_organization(self.organization)
|
||||||
context["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"] = (
|
context["organization_subdomain"] = (
|
||||||
("http" if settings.DMOJ_SSL == 0 else "https")
|
("http" if settings.DMOJ_SSL == 0 else "https")
|
||||||
+ "://"
|
+ "://"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<form action="" method="post">
|
<form action="" method="post" enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% if form.errors %}
|
{% if form.errors %}
|
||||||
<div class="alert alert-danger alert-dismissable">
|
<div class="alert alert-danger alert-dismissable">
|
||||||
|
|
|
@ -10,7 +10,10 @@
|
||||||
{% block middle_title %}
|
{% block middle_title %}
|
||||||
<div class="page-title">
|
<div class="page-title">
|
||||||
<div class="tabs" style="border: none;">
|
<div class="tabs" style="border: none;">
|
||||||
<h2><img src="{{logo_override_image}}" style="height: 3rem; vertical-align: middle; border-radius: 5px;">
|
<h2>
|
||||||
|
{% if organization_image %}
|
||||||
|
<img src="{{organization_image.url}}" style="height: 3rem; vertical-align: middle; border-radius: 5px;">
|
||||||
|
{% endif %}
|
||||||
{{title}}
|
{{title}}
|
||||||
</h2>
|
</h2>
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
|
|
|
@ -53,8 +53,8 @@
|
||||||
<div class="organization-container">
|
<div class="organization-container">
|
||||||
{% for org in queryset %}
|
{% for org in queryset %}
|
||||||
<div class="organization-card" style="cursor: pointer;" onclick="location.href='{{ org.get_absolute_url() }}';">
|
<div class="organization-card" style="cursor: pointer;" onclick="location.href='{{ org.get_absolute_url() }}';">
|
||||||
{% if org.logo_override_image %}
|
{% if org.organization_image %}
|
||||||
<img class="org-logo" loading="lazy" src="{{ org.logo_override_image }}">
|
<img class="org-logo" loading="lazy" src="{{ org.organization_image.url }}">
|
||||||
{% else %}
|
{% else %}
|
||||||
<img class="org-logo" loading="lazy" src="{{ static('icons/icon.svg') }}" onerror="this.onerror=null;this.src='{{ static('icons/logo.svg') }}';">
|
<img class="org-logo" loading="lazy" src="{{ static('icons/icon.svg') }}" onerror="this.onerror=null;this.src='{{ static('icons/logo.svg') }}';">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<a href="{{ org.get_absolute_url() }}">
|
<a href="{{ org.get_absolute_url() }}">
|
||||||
<span class="organization-tag" style="gap: 0.2em;">
|
<span class="organization-tag" style="gap: 0.2em;">
|
||||||
{% if org.logo_override_image %}
|
{% if org.organization_image %}
|
||||||
<img class="user-img" style="height: 1.5em; width: 1.5em;" loading="lazy" src="{{ org.logo_override_image }}">
|
<img class="user-img" style="height: 1.5em; width: 1.5em;" loading="lazy" src="{{ org.organization_image.url }}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ org.name }}
|
{{ org.name }}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
<div class="toggled sidebox-content">
|
<div class="toggled sidebox-content">
|
||||||
{% for organization in recent_organizations %}
|
{% for organization in recent_organizations %}
|
||||||
<a href="{{ url('organization_home', organization.pk, organization.slug) }}" class="organization-row">
|
<a href="{{ url('organization_home', organization.pk, organization.slug) }}" class="organization-row">
|
||||||
{% if organization.logo_override_image %}
|
{% if organization.organization_image %}
|
||||||
<img class="org-logo user-img" loading="lazy" src="{{ organization.logo_override_image }}">
|
<img class="org-logo user-img" loading="lazy" src="{{ organization.organization_image.url }}">
|
||||||
{% else %}
|
{% else %}
|
||||||
<img class="org-logo" loading="lazy" src="{{ static('icons/icon.svg') }}" onerror="{{static('icons/logo.svg')}}">
|
<img class="org-logo" loading="lazy" src="{{ static('icons/icon.svg') }}" onerror="{{static('icons/logo.svg')}}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
{% if request.in_contest_mode and request.participation.contest.logo_override_image %}
|
{% if request.in_contest_mode and request.participation.contest.logo_override_image %}
|
||||||
<img src="{{ request.participation.contest.logo_override_image|camo }}" alt="{{ SITE_NAME }}" height="44" style="border: none">
|
<img src="{{ request.participation.contest.logo_override_image|camo }}" alt="{{ SITE_NAME }}" height="44" style="border: none">
|
||||||
{% elif request.organization %}
|
{% elif request.organization %}
|
||||||
<img src="{{ request.organization.logo_override_image|camo }}" alt="{{ SITE_NAME }}" height="44" style="border: none">
|
<img src="{{ request.organization.organization_image.url|camo }}" alt="{{ SITE_NAME }}" height="44" style="border: none">
|
||||||
|
{% elif organization_image is defined and organization_image %}
|
||||||
|
<img src="{{ organization_image.url|camo }}" alt="{{ SITE_NAME }}" height="44" style="border: none">
|
||||||
{% elif logo_override_image is defined and logo_override_image %}
|
{% elif logo_override_image is defined and logo_override_image %}
|
||||||
<img src="{{ logo_override_image|camo }}" alt="{{ SITE_NAME }}" height="44" style="border: none">
|
<img src="{{ logo_override_image|camo }}" alt="{{ SITE_NAME }}" height="44" style="border: none">
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
Loading…
Reference in a new issue