Skip to content

Commit be9e4e3

Browse files
authored
Merge pull request #1409 from makeabilitylab/1408-awards-badge-crop
Add image preview + square cropping to Awards admin badge (#1408)
2 parents f3fdc49 + 75da029 commit be9e4e3

5 files changed

Lines changed: 59 additions & 8 deletions

File tree

website/admin/award_admin.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
import os
2+
13
from django import forms
24
from django.contrib import admin
35
from django.urls import reverse
46
from django.utils.html import format_html
7+
from easy_thumbnails.files import get_thumbnailer
8+
from image_cropping import ImageCroppingMixin
59
from website.models import Award
610
from website.admin.admin_site import ml_admin_site
711
from sortedm2m_filter_horizontal_widget.forms import SortedFilteredSelectMultiple
@@ -28,12 +32,12 @@ def clean(self):
2832

2933

3034
@admin.register(Award, site=ml_admin_site)
31-
class AwardAdmin(admin.ModelAdmin):
35+
class AwardAdmin(ImageCroppingMixin, admin.ModelAdmin):
3236
form = AwardAdminForm
3337

3438
# get_recipient_names / get_project_names are methods on the Award model;
3539
# their column headers come from each method's short_description.
36-
list_display = ('title', 'organization', 'date',
40+
list_display = ('title', 'get_display_thumbnail', 'organization', 'date',
3741
'get_recipient_names', 'get_project_names', 'award_type')
3842

3943
list_filter = ('award_type', 'date')
@@ -80,7 +84,7 @@ def get_fieldsets(self, request, obj=None):
8084
'fields': ['url', 'description'],
8185
}),
8286
('Display', {
83-
'fields': ['badge', 'badge_alt_text'],
87+
'fields': ['badge', 'badge_cropping', 'badge_alt_text'],
8488
'description': 'Optional. On the Awards page, faculty honors show a medal icon, '
8589
'student awards show the recipient’s photo, and project awards '
8690
'show the project thumbnail. Upload a badge/logo here to override '
@@ -97,4 +101,19 @@ def formfield_for_manytomany(self, db_field, request, **kwargs):
97101
"""
98102
if db_field.name == 'recipients' or db_field.name == 'projects':
99103
kwargs['widget'] = SortedFilteredSelectMultiple()
100-
return super().formfield_for_manytomany(db_field, request, **kwargs)
104+
return super().formfield_for_manytomany(db_field, request, **kwargs)
105+
106+
def get_display_thumbnail(self, obj):
107+
"""Square preview of the uploaded badge (with its crop applied) in the
108+
changelist, mirroring the SponsorAdmin/NewsAdmin logo columns. Awards
109+
without a custom badge fall back to a medal icon on the public page, so
110+
there's nothing to show here."""
111+
if obj.badge and os.path.isfile(obj.badge.path):
112+
thumbnailer = get_thumbnailer(obj.badge)
113+
options = {'size': (50, 50), 'crop': True, 'box': obj.badge_cropping}
114+
thumbnail_url = thumbnailer.get_thumbnail(options).url
115+
return format_html('<img src="{}" height="50" width="50" '
116+
'style="object-fit: cover; border-radius: 5%;"/>', thumbnail_url)
117+
return '—'
118+
119+
get_display_thumbnail.short_description = 'Badge'

website/models/award.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from django.db import models
22
from sortedm2m.fields import SortedManyToManyField
33

4+
from image_cropping import ImageRatioField
5+
46
from website.utils.fileutils import UniquePathAndRename
57
from website.utils.upload_validators import validate_image_upload
68

@@ -72,6 +74,14 @@ class Award(models.Model):
7274
"Student awards default to the recipient's photo and project awards to "
7375
"the project thumbnail; uploading a badge overrides those.")
7476

77+
# Square crop box for the badge, applied on the public Awards page so every
78+
# anchor (badge, portrait, project thumbnail, medal) reads as a uniform square
79+
# tile. Stored as an "x1,y1,x2,y2" string; the admin shows a Cropper.js preview
80+
# before the first save (same pattern as Person.cropping / Sponsor.icon_cropping).
81+
badge_cropping = ImageRatioField('badge', '245x245', size_warning=True)
82+
badge_cropping.help_text = ("Crop the badge to a square using the preview above "
83+
"(no need to save first). Keeps award anchors uniform.")
84+
7585
badge_alt_text = models.CharField(max_length=255, blank=True, null=True)
7686
badge_alt_text.help_text = "Alt text for the badge image. Defaults to the award title if left blank."
7787

website/static/website/css/awards.css

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,11 @@
8080
border-radius: 50%;
8181
}
8282

83-
/* Uploaded emblem/logo: show whole (don't crop) and let it sit on the page. */
83+
/* Uploaded emblem/logo: editors square-crop it in the admin (Award.badge_cropping),
84+
so it fills the same square tile as the portrait/thumbnail anchors for a uniform
85+
row. The shared .award-anchor-img rules already supply object-fit: cover + border. */
8486
.award-anchor-badge {
85-
object-fit: contain;
86-
border: none;
87+
border-radius: var(--border-radius-md);
8788
}
8889

8990
/* Faculty honors: medal icon in a soft circular chip. */

website/templates/snippets/display_award_snippet.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525

2626
<div class="award-anchor" aria-hidden="true">
2727
{% if kind == 'badge' %}
28-
<img class="award-anchor-img award-anchor-badge" src="{{ award.badge.url }}" alt="">
28+
<img class="award-anchor-img award-anchor-badge"
29+
src="{% thumbnail award.badge 245x245 box=award.badge_cropping crop upscale %}" alt="">
2930
{% elif kind == 'portrait' %}
3031
{% with person=award.get_portrait_person %}
3132
<img class="award-anchor-img award-anchor-portrait"

website/tests/test_image_cropping.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,13 @@ def test_person_exposes_crop_fields(self):
172172
self.assertIn("cropping", Person.ratio_fields)
173173
self.assertIn("easter_egg_crop", Person.ratio_fields)
174174

175+
def test_award_badge_exposes_crop_fields(self):
176+
"""Award.badge gained a square crop so its public anchor stays uniform."""
177+
from website.models import Award
178+
179+
self.assertIn("badge", Award.crop_fields)
180+
self.assertIn("badge_cropping", Award.ratio_fields)
181+
175182

176183
# --- Admin uses the Cropper.js widget, not Jcrop ---------------------------
177184

@@ -206,3 +213,16 @@ def test_person_admin_image_field_uses_crop_widget(self):
206213
db_field = Person._meta.get_field("image")
207214
formfield = admin_obj.formfield_for_dbfield(db_field, request)
208215
self.assertIsInstance(formfield.widget, CropImageWidget)
216+
217+
def test_award_admin_badge_field_uses_crop_widget(self):
218+
from django.contrib.auth.models import AnonymousUser
219+
from image_cropping.widgets import CropImageWidget
220+
from website.models import Award
221+
from website.admin.admin_site import ml_admin_site
222+
223+
admin_obj = ml_admin_site._registry[Award]
224+
request = MagicMock()
225+
request.user = AnonymousUser()
226+
db_field = Award._meta.get_field("badge")
227+
formfield = admin_obj.formfield_for_dbfield(db_field, request)
228+
self.assertIsInstance(formfield.widget, CropImageWidget)

0 commit comments

Comments
 (0)