Skip to content

Commit da2094c

Browse files
committed
Merge branch 'master' into 1380-artifact-thumbnail-preview
# Conflicts: # website/admin/artifact_admin.py
2 parents b35fa0b + 92a6a35 commit da2094c

5 files changed

Lines changed: 685 additions & 1 deletion

File tree

CLAUDE.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,19 @@ News items use `django-ckeditor`. Uploaded files via CKEditor land under `media/
150150
- UI changes require before/after screenshots or mockups in the PR (see issue #287 as a reference).
151151
- Run the Pa11y a11y service before submitting any UI change.
152152
- PRs target `master`.
153+
154+
## Issue conventions (labeling)
155+
156+
**Always apply labels when filing a GitHub issue via Claude Code** (`gh issue create --label ...`). Pick from the existing taxonomy below — most issues warrant 2–4 labels (typically one *kind* + one or more *areas*). Run `gh label list` to see the current set before creating new ones; only add a new label when nothing existing fits, and keep names/casing consistent with what's there.
157+
158+
The taxonomy (as of June 2026):
159+
160+
- **Kind:** `Bug`, `New Feature`, `Maintenance`, `Code Cleanup`, `Data Entry`, `Discuss`, `Won't Fix`, `Dependencies`, `Security`
161+
- **Priority** (optional, maintainer's call — don't guess): `Priority: Very High` / `High` / `Medium` / `Low`
162+
- **Effort** (optional): `Easy Fix`, `Time Consuming`
163+
- **Backend / infra:** `Backend`, `Docker`, `Logging`, `Server Start Scripts`, `Django Upgrade`, `Testing / Test Harness`, `Rest API`, `Requires Updating Model Database`, `Needs UW CSE IT`
164+
- **Frontend / UI:** `UI Design`, `CSS`, `Mobile`, `Accessibility`, `Navbar`, `Menu Bar`, `Footer`, `Banner`
165+
- **Content areas / pages:** `Publications`, `Talks`, `Posters`, `Videos`, `Projects`, `Project Gallery Page`, `People`, `Member Page`, `News`, `Awards`, `Landing Page`, `FAQ Page`, `Admin`, `Sponsors`, `Grants & Funding`, `SEO`
166+
- **Status:** `FixedNeedsToBeTestedOnTestServer`
167+
168+
Notes: `Awards` is for external recognitions and award content/data work; paper-level awards live on `Publication.award` (see Key model relationships) but issues about them still get `Awards` + `Publications`. Use `Grants & Funding` for proposals/funding-source tracking; `Sponsors` for sponsor logos/listings. Add `Requires Updating Model Database` whenever the work implies a schema/model change.

website/admin/artifact_admin.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,34 @@
33
from django.contrib.admin import widgets
44
from django.utils.html import format_html
55
from sortedm2m_filter_horizontal_widget.forms import SortedFilteredSelectMultiple
6+
from website.utils.upload_validators import PDF_EXTENSIONS, RAW_FILE_EXTENSIONS
67
from easy_thumbnails.files import get_thumbnailer
78
import os
89
import logging
910

1011
# This retrieves a Python logging instance (or creates it)
1112
_logger = logging.getLogger(__name__)
1213

14+
15+
def _accept_attr(extensions):
16+
"""Build an HTML ``accept`` value (e.g. ``.pdf,.pptx``) from an extension
17+
allowlist. Used to seed the file inputs so both the OS file picker and the
18+
client-side check in ``admin_artifact_form.js`` read the allowed types
19+
straight from the markup — keeping them in sync with the server validators
20+
(issue #248)."""
21+
return ",".join(f".{ext}" for ext in extensions)
22+
23+
1324
class ArtifactAdmin(admin.ModelAdmin):
1425

26+
# Loaded on every artifact add/change form (Talk/Poster/Publication, which
27+
# all subclass this). Guards against losing selected files when the form is
28+
# submitted with a missing required field, plus drag-and-drop (issue #248).
29+
# PublicationAdmin defines its own Media; Django merges this base in.
30+
class Media:
31+
css = {"all": ("website/css/admin_artifact_form.css",)}
32+
js = ("website/js/admin_artifact_form.js",)
33+
1534
# The list display lets us control what is shown in the default talk table at Home > Website > Talk
1635
# See: https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_display
1736
list_display = ('title', 'date', 'get_first_author_last_name', 'forum_name', 'location')
@@ -99,9 +118,28 @@ def get_fieldsets(self, request, obj=None):
99118
updated.append((name, opts))
100119
return updated
101120

121+
def get_form(self, request, obj=None, **kwargs):
122+
"""
123+
Seed the ``accept`` attribute on the file inputs from the same extension
124+
allowlists the server validators enforce (``PDF_EXTENSIONS`` /
125+
``RAW_FILE_EXTENSIONS`` in ``website.utils.upload_validators``). This gives
126+
the OS file picker a native type filter and is the single source of truth
127+
the client-side check in ``admin_artifact_form.js`` reads back from the
128+
DOM, so the JS can't drift from the Python rules (issue #248).
129+
130+
Subclasses (Talk/Publication) call ``super().get_form()`` first and then
131+
layer their own widget tweaks, so this runs for all artifact admins.
132+
"""
133+
form = super().get_form(request, obj, **kwargs)
134+
if "pdf_file" in form.base_fields:
135+
form.base_fields["pdf_file"].widget.attrs["accept"] = _accept_attr(PDF_EXTENSIONS)
136+
if "raw_file" in form.base_fields:
137+
form.base_fields["raw_file"].widget.attrs["accept"] = _accept_attr(RAW_FILE_EXTENSIONS)
138+
return form
139+
102140
def formfield_for_manytomany(self, db_field, request, **kwargs):
103141
"""
104-
Overrides the formfield_for_manytomany method of the parent ModelAdmin class to customize the widgets
142+
Overrides the formfield_for_manytomany method of the parent ModelAdmin class to customize the widgets
105143
used for ManyToMany fields in the admin interface.
106144
107145
Parameters:
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/*
2+
* Styles for the artifact (Talk / Poster / Publication) admin drag-and-drop
3+
* upload zone and the submit guard. See
4+
* website/static/website/js/admin_artifact_form.js (issue #248).
5+
*
6+
* Accessibility: error state is never signaled by color alone — it always pairs
7+
* with an icon-color change plus text (the inline message and the top-of-form
8+
* summary), so it stays perceivable for low-vision and color-blind users
9+
* (WCAG 2.0 AA). The native file input is visually hidden but kept focusable;
10+
* its focus is mirrored onto the zone (`.is-focused`) as a visible ring.
11+
*/
12+
13+
/* Visually hide the raw file input while keeping it focusable + submittable. */
14+
.artifact-file-input-hidden {
15+
position: absolute !important;
16+
width: 1px;
17+
height: 1px;
18+
padding: 0;
19+
margin: -1px;
20+
overflow: hidden;
21+
clip: rect(0 0 0 0);
22+
white-space: nowrap;
23+
border: 0;
24+
}
25+
26+
/* --- The drop zone (primary control) --- */
27+
.artifact-dropzone {
28+
display: flex;
29+
align-items: center;
30+
gap: 12px;
31+
margin-top: 6px;
32+
padding: 16px 18px;
33+
max-width: 520px;
34+
border: 2px dashed #b8c4cc;
35+
border-radius: 8px;
36+
background: #f8fafb;
37+
color: #2b3a42;
38+
cursor: pointer;
39+
transition: border-color 0.12s ease, background-color 0.12s ease, box-shadow 0.12s ease;
40+
}
41+
42+
.artifact-dropzone:hover {
43+
border-color: #79aec8; /* Django admin accent */
44+
background: #f0f6fa;
45+
}
46+
47+
/* Keyboard focus (mirrored from the hidden native input). */
48+
.artifact-dropzone.is-focused {
49+
outline: 2px solid #417690;
50+
outline-offset: 2px;
51+
}
52+
53+
/* Dragging a file over the zone. */
54+
.artifact-dropzone-active {
55+
border-style: solid;
56+
border-color: #417690;
57+
background: #e2f0f8;
58+
box-shadow: inset 0 0 0 3px rgba(65, 118, 144, 0.12);
59+
}
60+
61+
/* Error state (paired with text + icon color below — not color alone). */
62+
.artifact-dropzone.has-error {
63+
border-color: #ba2121;
64+
background: #fdf4f4;
65+
}
66+
67+
.artifact-dropzone-icon {
68+
flex: 0 0 auto;
69+
width: 26px;
70+
height: 26px;
71+
color: #5a7a8a;
72+
}
73+
74+
.artifact-dropzone:hover .artifact-dropzone-icon {
75+
color: #417690;
76+
}
77+
78+
.artifact-dropzone.has-error .artifact-dropzone-icon {
79+
color: #ba2121;
80+
}
81+
82+
.artifact-dropzone-copy {
83+
display: flex;
84+
flex-direction: column;
85+
min-width: 0; /* allow filename to ellipsize */
86+
line-height: 1.3;
87+
}
88+
89+
.artifact-dropzone-title,
90+
.artifact-dropzone-filename {
91+
font-weight: 600;
92+
font-size: 13px;
93+
}
94+
95+
.artifact-dropzone-filename {
96+
overflow: hidden;
97+
text-overflow: ellipsis;
98+
white-space: nowrap;
99+
}
100+
101+
.artifact-dropzone-sub {
102+
margin-top: 2px;
103+
font-size: 11px;
104+
color: #6a7b85;
105+
}
106+
107+
.artifact-dropzone.has-error .artifact-dropzone-sub {
108+
color: #ba2121;
109+
font-weight: 600;
110+
}
111+
112+
.artifact-dropzone-link {
113+
color: #417690;
114+
text-decoration: underline;
115+
}
116+
117+
/* Remove / replace button in the filled state. */
118+
.artifact-file-remove {
119+
flex: 0 0 auto;
120+
margin-left: auto;
121+
padding: 3px 10px;
122+
background: none;
123+
border: 1px solid #b8c4cc;
124+
border-radius: 4px;
125+
color: #444;
126+
font-size: 11px;
127+
cursor: pointer;
128+
}
129+
130+
.artifact-file-remove:hover {
131+
border-color: #ba2121;
132+
color: #ba2121;
133+
}
134+
135+
/* --- Top-of-form error summary --- */
136+
.artifact-error-summary {
137+
margin: 0 0 16px 0;
138+
padding: 12px 16px;
139+
border: 1px solid #ba2121;
140+
border-left-width: 6px;
141+
border-radius: 4px;
142+
background: #fff4f4;
143+
}
144+
145+
.artifact-error-summary:focus {
146+
outline: 2px solid #417690;
147+
outline-offset: 2px;
148+
}
149+
150+
.artifact-error-summary-heading {
151+
margin: 0 0 8px 0;
152+
font-weight: 700;
153+
color: #6b1414;
154+
}
155+
156+
.artifact-error-summary ul {
157+
margin: 0;
158+
padding-left: 20px;
159+
}
160+
161+
.artifact-error-summary li {
162+
margin: 2px 0;
163+
}
164+
165+
.artifact-error-summary a {
166+
color: #ba2121;
167+
text-decoration: underline;
168+
}

0 commit comments

Comments
 (0)