diff --git a/Pipfile b/Pipfile
index 0e4cc09c..4b631d87 100644
--- a/Pipfile
+++ b/Pipfile
@@ -17,6 +17,7 @@ django-hashid-field = "*"
django-celery-beat = "*"
flower = "*"
discord-py = "==2.5.0"
+pillow = "*"
[dev-packages]
diff --git a/Pipfile.lock b/Pipfile.lock
index 357419c1..75779158 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "d94bae63abd3a5b6ef5877241a4a1b29af21cf2b07471fcddae3a5225a533974"
+ "sha256": "551ef30ba23afe0e95563298d016a31e0eebc65afe08f6af5e972fed1c9db345"
},
"pipfile-spec": 6,
"requires": {
@@ -757,6 +757,84 @@
"markers": "python_version >= '3.8'",
"version": "==24.2"
},
+ "pillow": {
+ "hashes": [
+ "sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83",
+ "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96",
+ "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65",
+ "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a",
+ "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352",
+ "sha256:3362c6ca227e65c54bf71a5f88b3d4565ff1bcbc63ae72c34b07bbb1cc59a43f",
+ "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20",
+ "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c",
+ "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114",
+ "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49",
+ "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91",
+ "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0",
+ "sha256:4db853948ce4e718f2fc775b75c37ba2efb6aaea41a1a5fc57f0af59eee774b2",
+ "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5",
+ "sha256:54251ef02a2309b5eec99d151ebf5c9904b77976c8abdcbce7891ed22df53884",
+ "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e",
+ "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c",
+ "sha256:5bb94705aea800051a743aa4874bb1397d4695fb0583ba5e425ee0328757f196",
+ "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756",
+ "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861",
+ "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269",
+ "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1",
+ "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb",
+ "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a",
+ "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081",
+ "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1",
+ "sha256:89dbdb3e6e9594d512780a5a1c42801879628b38e3efc7038094430844e271d8",
+ "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90",
+ "sha256:8e275ee4cb11c262bd108ab2081f750db2a1c0b8c12c1897f27b160c8bd57bbc",
+ "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5",
+ "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1",
+ "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3",
+ "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35",
+ "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f",
+ "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c",
+ "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2",
+ "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2",
+ "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf",
+ "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65",
+ "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b",
+ "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442",
+ "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2",
+ "sha256:ab8a209b8485d3db694fa97a896d96dd6533d63c22829043fd9de627060beade",
+ "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482",
+ "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe",
+ "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc",
+ "sha256:b20be51b37a75cc54c2c55def3fa2c65bb94ba859dde241cd0a4fd302de5ae0a",
+ "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec",
+ "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3",
+ "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a",
+ "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07",
+ "sha256:bf902d7413c82a1bfa08b06a070876132a5ae6b2388e2712aab3a7cbc02205c6",
+ "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f",
+ "sha256:c1eec9d950b6fe688edee07138993e54ee4ae634c51443cfb7c1e7613322718e",
+ "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192",
+ "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0",
+ "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6",
+ "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73",
+ "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f",
+ "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6",
+ "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547",
+ "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9",
+ "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457",
+ "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8",
+ "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26",
+ "sha256:e5449ca63da169a2e6068dd0e2fcc8d91f9558aba89ff6d02121ca8ab11e79e5",
+ "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab",
+ "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070",
+ "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71",
+ "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9",
+ "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.9'",
+ "version": "==11.1.0"
+ },
"prometheus-client": {
"hashes": [
"sha256:252505a722ac04b0456be05c05f75f45d760c2911ffc45f2a06bcaed9f3ae3fb",
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 8ebe30a7..f60c613a 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -33,6 +33,7 @@ kombu==5.5.0; python_version >= '3.8'
multidict==6.1.0; python_version >= '3.8'
mysqlclient==2.2.7; python_version >= '3.8'
packaging==24.2; python_version >= '3.8'
+pillow==11.1.0; python_version >= '3.9'
prometheus-client==0.21.1; python_version >= '3.8'
prompt-toolkit==3.0.50; python_full_version >= '3.8.0'
propcache==0.3.0; python_version >= '3.9'
diff --git a/requirements.txt b/requirements.txt
index 8ebe30a7..f60c613a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -33,6 +33,7 @@ kombu==5.5.0; python_version >= '3.8'
multidict==6.1.0; python_version >= '3.8'
mysqlclient==2.2.7; python_version >= '3.8'
packaging==24.2; python_version >= '3.8'
+pillow==11.1.0; python_version >= '3.9'
prometheus-client==0.21.1; python_version >= '3.8'
prompt-toolkit==3.0.50; python_full_version >= '3.8.0'
propcache==0.3.0; python_version >= '3.9'
diff --git a/src/contestsuite/urls.py b/src/contestsuite/urls.py
index cacb9dca..4d09ee97 100644
--- a/src/contestsuite/urls.py
+++ b/src/contestsuite/urls.py
@@ -16,6 +16,8 @@
from django.contrib import admin
from django.urls import include, path
+from django.conf import settings
+from django.conf.urls.static import static
urlpatterns = [
path('', include('core.urls')),
@@ -27,4 +29,4 @@
path('lfg/', include('lfg.urls')),
path('manage/', include('manager.urls')),
path('register/', include('register.urls')),
-]
+] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
\ No newline at end of file
diff --git a/src/core/admin.py b/src/core/admin.py
index ce04b437..1a5371f8 100644
--- a/src/core/admin.py
+++ b/src/core/admin.py
@@ -1,18 +1,20 @@
from django.contrib import admin
from django.contrib.auth.models import User
+from django.utils.html import mark_safe
+from . import models
from import_export import resources
from import_export.admin import ImportExportModelAdmin
class UserResource(resources.ModelResource):
- """
- Attach User model to Django-Import-Export
- https://django-import-export.readthedocs.io/en/latest/getting_started.html#creating-a-resource
- """
- class Meta:
- model = User
- fields = ('first_name', 'last_name', 'email', 'is_active', 'profile__checked_in')
+ """
+ Attach User model to Django-Import-Export
+ https://django-import-export.readthedocs.io/en/latest/getting_started.html#creating-a-resource
+ """
+ class Meta:
+ model = User
+ fields = ('first_name', 'last_name', 'email', 'is_active', 'profile__checked_in')
class UserAdmin(ImportExportModelAdmin):
@@ -20,13 +22,28 @@ class UserAdmin(ImportExportModelAdmin):
Django-Import-Export resource admin intrgration
https://django-import-export.readthedocs.io/en/latest/advanced_usage.html#admin-integration
"""
-
+
resource_class = UserResource
list_display = ("last_name", "first_name", "username", "email", "is_active",)
list_filter = ("is_active",)
search_fields = ["last_name", "first_name", "username", "email"]
+class SponsorAdmin(admin.ModelAdmin):
+ """
+ Define Sponsor model interface in Django Admin.
+ https://docs.djangoproject.com/en/4.2/ref/contrib/admin/#modeladmin-objects
+ """
+
+ list_display = ('name', 'url', 'logo_thumbnail', 'message')
+ search_fields = ['name', 'message']
+
+ def logo_thumbnail(self, obj):
+ if obj.logo:
+ return mark_safe(f'')
+ return '-'
+
# Re-register User model for django-import-export integration
admin.site.unregister(User)
admin.site.register(User, UserAdmin)
+admin.site.register(models.Sponsor, SponsorAdmin)
diff --git a/src/core/migrations/0001_initial.py b/src/core/migrations/0001_initial.py
new file mode 100644
index 00000000..fae81f7b
--- /dev/null
+++ b/src/core/migrations/0001_initial.py
@@ -0,0 +1,24 @@
+# Generated by Django 4.2.20 on 2025-03-30 15:49
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Sponsor',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=200, unique=True)),
+ ('logo', models.ImageField(upload_to='sponsors')),
+ ('url', models.URLField(blank=True)),
+ ('message', models.TextField(blank=True)),
+ ],
+ ),
+ ]
diff --git a/src/core/models.py b/src/core/models.py
index 71a83623..f76119f7 100644
--- a/src/core/models.py
+++ b/src/core/models.py
@@ -1,3 +1,25 @@
from django.db import models
# Create your models here.
+
+class Sponsor(models.Model):
+ """
+ Contest sponsor model. Each model stores the details of a contest sponsor, including
+ name, logo, link to sponsor's website, and message.
+
+ name (CharField): the sponsor name (unique)
+
+ logo (ImageField): the sponsor logo
+
+ url (URLField): the sponsor URL
+
+ message (TextField): the sponsor message
+ """
+
+ name = models.CharField(max_length=200, unique=True)
+ logo = models.ImageField(upload_to='sponsors')
+ url = models.URLField(blank=True)
+ message = models.TextField(blank=True)
+
+ def __str__(self):
+ return self.name
\ No newline at end of file
diff --git a/src/core/templates/core/index.html b/src/core/templates/core/index.html
index 50f0744d..8a6253b5 100644
--- a/src/core/templates/core/index.html
+++ b/src/core/templates/core/index.html
@@ -98,21 +98,35 @@
This programming contest is made possible by...
+{% endif %} + + + +No sponsors available.
+ {% endfor %} +