Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7

- name: Set up Ruby
uses: ruby/setup-ruby@v1
Expand Down Expand Up @@ -47,7 +47,7 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7

- name: Set up Ruby
uses: ruby/setup-ruby@v1
Expand Down
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ This codebase (Rails 8.1)
| `app/models/` | ActiveRecord models | ~78 files |
| `app/services/` | Service objects and POROs (e.g. `MoneyFormatter` for currency display) | ~29 files |
| `app/jobs/` | SolidQueue background jobs | 3 files |
| `app/models/concerns/` | Shared model modules | 15 concerns |
| `app/models/concerns/` | Shared model modules | 16 concerns |

### Presentation

Expand Down Expand Up @@ -104,6 +104,8 @@ This codebase (Rails 8.1)
| `Organization` | Groups with affiliations, addresses, logos via ActiveStorage |
| `Grant` | Donated funds (polymorphic `donor`: Organization or Person) with eligibility criteria, tasks, deadlines; parent of `Scholarship`. Scholarship totals cannot exceed the grant amount |
| `Scholarship` | Award to a `Person`; optionally drawn from a `Grant`, syncs to event registration `Allocation` |
| `ProfessionalLicense` | A license a `Person` holds (`number`, `kind`, `issuing_state`, `expires_on`); a null `number` is a placeholder. `find_or_create_for` keeps one license per (person, number) |
| `ContinuingEducationRegistration` | A registrant's CE for one event against one `ProfessionalLicense`; billable `allocatable` (`Registerable`) with stored `hours` + `cost_cents` (default from the event). Payment is computed (no stored status); the certificate is delivered via `certificate_sent_at` and gated by its own `certificate_available?` |
| `Report` | STI base class for MonthlyReport |
| `WorkshopLog` | Standalone model for workshop log submissions (attendance, form fields) |

Expand Down Expand Up @@ -133,6 +135,7 @@ This codebase (Rails 8.1)
| `NameFilterable` | Name-based filtering |
| `Publishable` | `published`, `publicly_visible` scopes |
| `PunctuationStrippable` | Strips punctuation from strings |
| `Registerable` | Shared payment (`allocations_sum`/`paid?`/`remaining_cost`/…) + certificate (`certificate_sent?`, `mark_certificate_sent!`) interface for `EventRegistration` and `ContinuingEducationRegistration`; includers supply `cost_cents` + their own `certificate_available?` |
| `RemoteSearchable` | AJAX remote search by column |
| `RichTextSearchable` | Full-text search on ActionText rich_text fields |
| `SectorsTaggable` | Enforces a single primary sector for sector-tagged owners |
Expand Down
11 changes: 0 additions & 11 deletions app/models/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,6 @@ def self.mentionable_rich_text_fields
PUBLISHED_KINDS = [ "Handout", "Template", "Toolkit", "Form" ]
KINDS = PUBLISHED_KINDS + [ "Resource", "Story", "LeaderSpotlight", "SectorImpact", "Theme", "Scholarship" ]

# Titles whose downloadable PDF is a single page. Every other resource PDF is
# multi-page, so its first-page preview needs a "download for all pages" note.
# Hardcoded for now β€” replace with real PDF introspection later.
SINGLE_PAGE_PDF_TITLES = [ "Letter to Supervisors", "W-9" ].freeze

has_rich_text :rhino_body

belongs_to :created_by, class_name: "User"
Expand Down Expand Up @@ -124,12 +119,6 @@ def story?
[ "Story", "LeaderSpotlight" ].include? self.kind
end

# A multi-page PDF only previews its first page, so the viewer needs a prompt
# to download for the rest. See SINGLE_PAGE_PDF_TITLES.
def single_page_pdf?
SINGLE_PAGE_PDF_TITLES.include?(title)
end

def custom_label_list
"#{self.title} (#{self.kind.upcase})" unless self.kind.nil?
end
Expand Down
17 changes: 17 additions & 0 deletions app/views/assets/_resource_display.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<%# Renders a resource's main display. A PDF opens in the browser's built-in
inline viewer (scrollable, all pages) β€” every major browser ships one β€” with
the first-page/image preview as the fallback child for the rare client that
can't. Anything else (images, etc.) renders through the shared asset
pipeline. `resource` is a decorated Resource. %>
<% display = resource.display_image %>
<% if display.respond_to?(:attached?) && display.attached? && display.content_type == "application/pdf" %>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ€– From Claude: The PDF check keys off display_image (the resolved hero: primary_asset β†’ downloadable_asset β†’ …), not the downloadable file directly β€” so a resource with a primary image and a downloadable PDF still shows its image here, and only resources whose hero actually is a PDF get the inline viewer. respond_to?(:attached?) guards against the symbol/string defaults display_image can return.

Comment on lines +6 to +7
<object data="<%= rails_blob_path(display, disposition: :inline) %>" type="application/pdf"
class="w-full h-[80vh] rounded-lg border border-gray-200"
aria-label="<%= resource.title %> (PDF preview)">
<%= render "assets/display_assets",
resource: resource, file: display, variant: :hero, link: true %>
</object>
<% else %>
<%= render "assets/display_assets",
resource: resource, file: display, variant: :hero, link: true %>
<% end %>
16 changes: 6 additions & 10 deletions app/views/events/callouts/_resource_body.html.erb
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
<%# Renders a resource inside a callout page: a top-right download button (when a
downloadable file is attached) plus the resource display (PDF first-page preview
etc., via the same pipeline as resources/show). `resource` is a decorated
Resource. Shared by the registrant resource viewer and admin callouts that link
a resource. %>
downloadable file is attached) plus the resource display. PDFs show in the
browser's inline viewer; everything else renders via the shared asset
pipeline (see assets/resource_display). `resource` is a decorated Resource.
Shared by the registrant resource viewer and admin callouts that link a
resource. %>
<% if resource.downloadable_asset&.file&.attached? %>
<div class="flex flex-wrap items-center justify-end gap-x-3 gap-y-1 mb-4">
<% unless resource.single_page_pdf? %>
<span class="text-xs text-gray-500">The preview shows the first page only β€” download to get all pages.</span>
<% end %>
<%= link_to resource_download_path(resource), download: true, class: "btn btn-utility" do %>
Download <i class="fa-solid fa-download"></i>
<% end %>
Expand All @@ -16,6 +14,4 @@

<p class="text-xs text-gray-400 mb-3">The preview below may take a moment to load.</p>

<%= render "assets/display_assets",
resource: resource, file: resource.display_image,
variant: :hero, link: true %>
<%= render "assets/resource_display", resource: resource %>
2 changes: 1 addition & 1 deletion app/views/resources/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
</div>
<!-- Assets -->
<div id="hero-image">
<%= render "assets/display_assets", resource: @resource, file: @resource.display_image, variant: :hero, link: true %>
<%= render "assets/resource_display", resource: @resource %>
</div>
</div>
<!-- Mentions -->
Expand Down
11 changes: 0 additions & 11 deletions spec/models/resource_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,4 @@
end
end
end

describe "#single_page_pdf?" do
it "is true for the known single-page titles" do
expect(build(:resource, title: "W-9").single_page_pdf?).to be(true)
expect(build(:resource, title: "Letter to Supervisors").single_page_pdf?).to be(true)
end

it "is false for any other title" do
expect(build(:resource, title: "AHA Moments").single_page_pdf?).to be(false)
end
end
end
29 changes: 29 additions & 0 deletions spec/requests/events/registration_ticket_callouts_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,35 @@
end
end

context "when linked to a PDF resource" do
let(:resource) { create(:resource) }
let(:callout) { create(:registration_ticket_callout, event:, resource:, description: "") }

before { create(:downloadable_asset, owner: resource) }

it "shows the PDF in the browser's inline viewer" do
get event_registration_ticket_callout_path(event, callout)

expect(response).to have_http_status(:ok)
expect(response.body).to include("type=\"application/pdf\"")
expect(response.body).to include(rails_blob_path(resource.downloadable_asset.file, disposition: :inline))
end
end

context "when linked to a non-PDF resource" do
let(:resource) { create(:resource) }
let(:callout) { create(:registration_ticket_callout, event:, resource:, description: "") }

before { create(:downloadable_asset, :with_image, owner: resource) }

it "renders the preview instead of an inline PDF viewer" do
get event_registration_ticket_callout_path(event, callout)

expect(response).to have_http_status(:ok)
expect(response.body).not_to include("type=\"application/pdf\"")
end
end

context "when linked to a resource without a downloadable file" do
let(:resource) { create(:resource) }
let(:callout) do
Expand Down
18 changes: 0 additions & 18 deletions spec/requests/events/registrations_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -292,24 +292,6 @@
expect(response.body).to include(resource_download_path(resource))
end

it "prompts to download for all pages on a multi-page resource" do
resource = create(:resource, title: "AHA Moments", kind: "Handout")
create(:downloadable_asset, owner: resource)

get registration_resource_path(registration.slug, resource)

expect(response.body).to include("download to get all pages")
end

it "omits the all-pages prompt on a known single-page resource" do
resource = create(:resource, title: "W-9", kind: "Form")
create(:downloadable_asset, owner: resource)

get registration_resource_path(registration.slug, resource)

expect(response.body).not_to include("download to get all pages")
end

it "is reachable by slug without logging in" do
resource = create(:resource, title: "AHA Moments", kind: "Handout")

Expand Down