diff --git a/website/admin/artifact_admin.py b/website/admin/artifact_admin.py
index fc84e0e3..2f119856 100644
--- a/website/admin/artifact_admin.py
+++ b/website/admin/artifact_admin.py
@@ -15,7 +15,9 @@ class ArtifactAdmin(admin.ModelAdmin):
# search_fields are used for auto-complete, see:
# https://docs.djangoproject.com/en/3.0/ref/contrib/admin/#django.contrib.admin.ModelAdmin.autocomplete_fields
- search_fields = ['title', 'forum_name']
+ # Includes author first/last name so artifacts are findable by who wrote them
+ # (Django auto-applies DISTINCT for the M2M join). Subclasses may extend this.
+ search_fields = ['title', 'forum_name', 'authors__first_name', 'authors__last_name']
fieldsets = [
(None, {'fields': ['title', 'authors', 'date']}),
diff --git a/website/admin/award_admin.py b/website/admin/award_admin.py
index 1e6293dd..162c87c4 100644
--- a/website/admin/award_admin.py
+++ b/website/admin/award_admin.py
@@ -44,6 +44,8 @@ class AwardAdmin(admin.ModelAdmin):
ordering = ('-date',)
+ date_hierarchy = 'date' # Year/month/day drill-down (awards are browsed by year)
+
def get_fieldsets(self, request, obj=None):
# Built at request time so reverse() can resolve the Publications admin URL.
publications_url = reverse('admin:website_publication_changelist')
diff --git a/website/admin/grant_admin.py b/website/admin/grant_admin.py
index 1599e694..0b249516 100644
--- a/website/admin/grant_admin.py
+++ b/website/admin/grant_admin.py
@@ -9,7 +9,10 @@ class GrantAdmin(ArtifactAdmin):
# search_fields are used for auto-complete, see:
# https://docs.djangoproject.com/en/3.0/ref/contrib/admin/#django.contrib.admin.ModelAdmin.autocomplete_fields
- search_fields = ['title', 'date', 'forum_name']
+ # Dropped 'date' (string-searching a DateField is unhelpful); added PI/Co-PI
+ # (author) and sponsor name so grants are findable by people and funder.
+ search_fields = ['title', 'forum_name', 'authors__first_name',
+ 'authors__last_name', 'sponsor__name']
# The list display lets us control what is shown in the default talk table at Home > Website > Grants
# See: https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_display
@@ -21,6 +24,7 @@ class GrantAdmin(ArtifactAdmin):
autocomplete_fields = ['sponsor']
ordering = ('-date',) # sort by date, most recent first
+ date_hierarchy = 'date' # Year/month/day drill-down by grant start date
fieldsets = [
(None, {'fields': ['title', 'authors']}),
diff --git a/website/admin/keyword_admin.py b/website/admin/keyword_admin.py
index bb1b748d..a2484ceb 100644
--- a/website/admin/keyword_admin.py
+++ b/website/admin/keyword_admin.py
@@ -7,6 +7,11 @@
class KeywordAdmin(admin.ModelAdmin):
list_display = ['keyword', 'project_count', 'publication_count']
+ # The keyword table had no search box; alphabetical ordering also groups
+ # near-duplicate tags (e.g. "Speech" / "speech") adjacently for cleanup.
+ search_fields = ['keyword']
+ ordering = ['keyword']
+
def change_view(self, request, object_id, form_url='', extra_context=None):
"""Add projects and publications to the context. We then use this extra data in
the change_form.html template to display the projects and publications that use this keyword.
diff --git a/website/admin/news_admin.py b/website/admin/news_admin.py
index 786b8034..0cc7f9be 100644
--- a/website/admin/news_admin.py
+++ b/website/admin/news_admin.py
@@ -12,8 +12,11 @@
from django.utils.html import format_html # for formatting thumbnails
from easy_thumbnails.files import get_thumbnailer # for generating thumbnails
import os # for checking if thumbnail file exists
+import logging
from website.admin.admin_site import ml_admin_site
+_logger = logging.getLogger(__name__)
+
class YearListFilter(admin.SimpleListFilter):
title = 'year' # a label for our filter
parameter_name = 'year' # you can put anything here
@@ -42,6 +45,12 @@ class NewsAdmin(ImageCroppingMixin, admin.ModelAdmin):
# Add a filter to the right sidebar that allows us to filter by year
list_filter = (YearListFilter, 'project')
+ # Search by headline or author name (News previously had no search box).
+ search_fields = ['title', 'author__first_name', 'author__last_name']
+
+ # Year/month/day drill-down at the top of the changelist (News is date-driven).
+ date_hierarchy = 'date'
+
# Define 'author' as an auto-complete field. We must then also define "search_fields"
# in PersonAdmin or we'll receive a Django error
autocomplete_fields = ['author']
@@ -51,12 +60,18 @@ class NewsAdmin(ImageCroppingMixin, admin.ModelAdmin):
def get_display_thumbnail(self, obj):
if obj.image and os.path.isfile(obj.image.path):
- # Use easy_thumbnails to generate a thumbnail
- thumbnailer = get_thumbnailer(obj.image)
- thumbnail_options = {'size': (NEWS_THUMBNAIL_SIZE[0], NEWS_THUMBNAIL_SIZE[1]), 'crop': True}
- thumbnail_url = thumbnailer.get_thumbnail(thumbnail_options).url
-
- return format_html('', thumbnail_url)
+ try:
+ # Use easy_thumbnails to generate a thumbnail
+ thumbnailer = get_thumbnailer(obj.image)
+ thumbnail_options = {'size': (NEWS_THUMBNAIL_SIZE[0], NEWS_THUMBNAIL_SIZE[1]), 'crop': True}
+ thumbnail_url = thumbnailer.get_thumbnail(thumbnail_options).url
+
+ return format_html('
', thumbnail_url)
+ except Exception:
+ # A single corrupt/unreadable image must not 500 the entire News
+ # changelist (the column is rendered for every row).
+ _logger.warning("Could not generate admin thumbnail for News id=%s", obj.pk, exc_info=True)
+ return 'No Thumbnail'
return 'No Thumbnail'
get_display_thumbnail.short_description = 'Thumbnail'
diff --git a/website/admin/photo_admin.py b/website/admin/photo_admin.py
index 25510a26..f1695cbc 100644
--- a/website/admin/photo_admin.py
+++ b/website/admin/photo_admin.py
@@ -7,5 +7,8 @@
class PhotoAdmin(ImageCroppingMixin, admin.ModelAdmin):
list_display = ('admin_thumbnail', 'caption', 'alt_text', 'get_resolution_as_str',
'cropping', 'picture')
-
+
+ # Photos had no search box; search caption/alt text and the owning project.
+ search_fields = ['caption', 'alt_text', 'project__name']
+
list_per_page = 20 # changes how many images to show on a single admin page
\ No newline at end of file
diff --git a/website/admin/position_admin.py b/website/admin/position_admin.py
index 26e8ec30..83dcc320 100644
--- a/website/admin/position_admin.py
+++ b/website/admin/position_admin.py
@@ -8,6 +8,10 @@
class PositionAdmin(admin.ModelAdmin):
"""Note: We do not want users to edit positions directly. Instead, we want them to edit people and projects.
See PositionInline in PersonAdmin"""
+ # Needed for autocomplete/search to filter rather than return everything;
+ # the get_search_results override below builds on this base.
+ search_fields = ['person__first_name', 'person__last_name', 'title']
+
def get_model_perms(self, request):
"""
Return empty perms dict thus hiding the model from admin index.
diff --git a/website/admin/poster_admin.py b/website/admin/poster_admin.py
index 4a193e77..fb88cf28 100644
--- a/website/admin/poster_admin.py
+++ b/website/admin/poster_admin.py
@@ -13,7 +13,12 @@ class PosterAdmin(ArtifactAdmin):
# search_fields are used for auto-complete, see:
# https://docs.djangoproject.com/en/3.0/ref/contrib/admin/#django.contrib.admin.ModelAdmin.autocomplete_fields
- search_fields = ['title', 'date']
+ # Was ['title', 'date'] — string-searching a DateField is unhelpful; search
+ # venue and author instead (matches the other artifact admins).
+ search_fields = ['title', 'forum_name', 'authors__first_name', 'authors__last_name']
+
+ ordering = ('-date',) # Poster had no default sort; newest first like its siblings
+ date_hierarchy = 'date' # Year/month/day drill-down
def get_changeform_initial_data(self, request):
"""
diff --git a/website/admin/project_admin.py b/website/admin/project_admin.py
index fa15e5f3..a9459984 100644
--- a/website/admin/project_admin.py
+++ b/website/admin/project_admin.py
@@ -48,7 +48,9 @@ class GrantInline(admin.TabularInline):
@admin.register(Project, site=ml_admin_site)
class ProjectAdmin(ImageCroppingMixin, admin.ModelAdmin):
- search_fields = ['name'] # allows you to search by the name of the project
+ # Search by name plus the research-area facets editors think in (umbrella, keyword).
+ search_fields = ['name', 'short_name', 'project_umbrellas__name', 'keywords__name']
+ ordering = ('name',) # deterministic alphabetical sort (matched the autocomplete already)
inlines = [GrantInline, BannerInline, PhotoInline, ProjectRoleInline]
# The list display lets us control what is shown in the Project table at Home > Website > Project
diff --git a/website/admin/project_umbrella_admin.py b/website/admin/project_umbrella_admin.py
index 63e65e98..ba14d34b 100644
--- a/website/admin/project_umbrella_admin.py
+++ b/website/admin/project_umbrella_admin.py
@@ -11,6 +11,8 @@ class ProjectInline(admin.TabularInline): # or admin.StackedInline
@admin.register(ProjectUmbrella, site=ml_admin_site)
class ProjectUmbrellaAdmin(admin.ModelAdmin):
list_display = ('name', 'short_name', 'project_count')
+ search_fields = ['name', 'short_name']
+ ordering = ('name',)
inlines = [ProjectInline]
def formfield_for_manytomany(self, db_field, request=None, **kwargs):
diff --git a/website/admin/publication_admin.py b/website/admin/publication_admin.py
index 98285fd2..69c00508 100644
--- a/website/admin/publication_admin.py
+++ b/website/admin/publication_admin.py
@@ -47,6 +47,13 @@ class Media:
# default the sort order in table to descending order by date
ordering = ('-date',)
+ # Extend the ArtifactAdmin default (title/forum/authors) with book title and DOI.
+ search_fields = ['title', 'forum_name', 'book_title',
+ 'authors__first_name', 'authors__last_name', 'doi']
+
+ # Year/month/day drill-down for this large, date-ordered table.
+ date_hierarchy = 'date'
+
list_filter = (PubVenueTypeListFilter, PubVenueListFilter)
# add in auto-complete fields
diff --git a/website/admin/sponsor_admin.py b/website/admin/sponsor_admin.py
index 4c692acf..91c643b1 100644
--- a/website/admin/sponsor_admin.py
+++ b/website/admin/sponsor_admin.py
@@ -22,6 +22,8 @@ class SponsorAdmin(ImageCroppingMixin, admin.ModelAdmin):
# In this case, the admin will search the 'name' and 'short_name' fields of the Sponsor model.
search_fields = ['name', 'short_name']
+ ordering = ('name',) # deterministic alphabetical sort (none was defined)
+
def total_funding(self, obj):
return obj.grant_set.aggregate(total_funding=Sum('funding_amount'))['total_funding']
total_funding.short_description = 'Total Funding'
diff --git a/website/admin/talk_admin.py b/website/admin/talk_admin.py
index 438f6b98..fb774842 100644
--- a/website/admin/talk_admin.py
+++ b/website/admin/talk_admin.py
@@ -22,7 +22,8 @@ class TalkAdmin(ArtifactAdmin):
# search_fields are used for auto-complete, see:
# https://docs.djangoproject.com/en/3.0/ref/contrib/admin/#django.contrib.admin.ModelAdmin.autocomplete_fields
- search_fields = ['title', 'forum_name']
+ # Includes speaker (author) name so talks are findable by who gave them.
+ search_fields = ['title', 'forum_name', 'authors__first_name', 'authors__last_name']
# This auto-complete field is not working
# See: https://github.com/makeabilitylab/makeabilitylabwebsite/issues/1093#issuecomment-2423843958
@@ -42,6 +43,7 @@ class TalkAdmin(ArtifactAdmin):
ordering = ('-date',) # Sort talks by date in descending order
list_filter = ('talk_type',) # Add a filter for the talk type
+ date_hierarchy = 'date' # Year/month/day drill-down at the top of the list
def get_changeform_initial_data(self, request):
"""
diff --git a/website/admin/video_admin.py b/website/admin/video_admin.py
index fab91fb3..c93d6eb0 100644
--- a/website/admin/video_admin.py
+++ b/website/admin/video_admin.py
@@ -16,11 +16,12 @@ class VideoAdmin(admin.ModelAdmin):
# search_fields are used for auto-complete, see:
# https://docs.djangoproject.com/en/3.0/ref/contrib/admin/#django.contrib.admin.ModelAdmin.autocomplete_fields
# Jon's note (Dec 16, 2025): We cannot use 'get_video_host_str' here because search_fields requires actual model fields.
- search_fields = ['title', 'video_url', 'date', 'caption']
- # search_fields = ['title']
+ # Dropped 'date' (string-searching a DateField is unhelpful) and added project name.
+ search_fields = ['title', 'video_url', 'caption', 'projects__name']
# default the sort order in table to descending order by date
ordering = ('-date',)
+ date_hierarchy = 'date' # Year/month/day drill-down
def display_projects(self, obj):
return ", ".join([project.name for project in obj.projects.all()])
diff --git a/website/templates/website/project.html b/website/templates/website/project.html
index 1f2872e5..2b96e736 100644
--- a/website/templates/website/project.html
+++ b/website/templates/website/project.html
@@ -445,8 +445,8 @@