Add JSON activities API endpoint (#2783)
* Add JSON events API endpoint * Add regression test for event particulars defaults * Move JSON events API to a dedicated ActivitiesController The events endpoint served both the HTML day timeline and the JSON API feed, but the two paths shared no data or behavior — the HTML side uses DayTimelinesScoped while the JSON side built its own query. Splitting into ActivitiesController gives the API its own home at GET /:account/activities.json without dragging in the timeline before_actions. Also preloads comment creator in Event.preloaded to avoid an N+1 when rendering comment eventables in the JSON feed.
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
class ActivitiesController < ApplicationController
|
||||
ACTIONS = %w[
|
||||
card_assigned
|
||||
card_auto_postponed
|
||||
card_board_changed
|
||||
card_closed
|
||||
card_postponed
|
||||
card_published
|
||||
card_reopened
|
||||
card_sent_back_to_triage
|
||||
card_title_changed
|
||||
card_triaged
|
||||
card_unassigned
|
||||
comment_created
|
||||
].freeze
|
||||
|
||||
def index
|
||||
set_page_and_extract_portion_from(activities)
|
||||
end
|
||||
|
||||
private
|
||||
def activities
|
||||
Current.user.accessible_events
|
||||
.preloaded
|
||||
.where(action: ACTIONS)
|
||||
.for_creators(params[:creator_ids])
|
||||
.for_boards(params[:board_ids])
|
||||
.reverse_chronologically
|
||||
end
|
||||
end
|
||||
+4
-1
@@ -9,10 +9,13 @@ class Event < ApplicationRecord
|
||||
has_many :webhook_deliveries, class_name: "Webhook::Delivery", dependent: :delete_all
|
||||
|
||||
scope :chronologically, -> { order created_at: :asc, id: :desc }
|
||||
scope :reverse_chronologically, -> { order created_at: :desc, id: :desc }
|
||||
scope :for_creators, ->(ids) { where(creator_id: ids) if ids.present? }
|
||||
scope :for_boards, ->(ids) { where(board_id: ids) if ids.present? }
|
||||
scope :preloaded, -> {
|
||||
includes(:creator, :board, {
|
||||
eventable: [
|
||||
:goldness, :closure, :image_attachment,
|
||||
:creator, :goldness, :closure, :image_attachment,
|
||||
{ rich_text_body: :embeds_attachments },
|
||||
{ rich_text_description: :embeds_attachments },
|
||||
{ card: [ :goldness, :closure, :image_attachment ] }
|
||||
|
||||
@@ -8,4 +8,21 @@ module Event::Particulars
|
||||
def assignees
|
||||
@assignees ||= User.where id: assignee_ids
|
||||
end
|
||||
|
||||
def api_particulars
|
||||
nested = particulars.dig("particulars") || {}
|
||||
|
||||
case action.to_s
|
||||
when "card_assigned", "card_unassigned"
|
||||
{ "assignee_ids" => Array(assignee_ids) }
|
||||
when "card_board_changed"
|
||||
{ "old_board" => nested["old_board"].to_s, "new_board" => nested["new_board"].to_s }
|
||||
when "card_title_changed"
|
||||
{ "old_title" => nested["old_title"].to_s, "new_title" => nested["new_title"].to_s }
|
||||
when "card_triaged"
|
||||
{ "column" => nested["column"].to_s }
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
json.(event, :id, :action)
|
||||
json.created_at event.created_at.utc
|
||||
json.description event.description_for(Current.user).to_plain_text
|
||||
json.particulars event.api_particulars
|
||||
json.url(
|
||||
case event.eventable
|
||||
when Comment then card_url(event.eventable.card, anchor: dom_id(event.eventable))
|
||||
else polymorphic_url(event.eventable)
|
||||
end
|
||||
)
|
||||
json.eventable_type event.eventable_type
|
||||
|
||||
json.eventable do
|
||||
case event.eventable
|
||||
when Card then json.partial! "cards/card", card: event.eventable
|
||||
when Comment then json.partial! "cards/comments/comment", comment: event.eventable
|
||||
end
|
||||
end
|
||||
|
||||
json.board event.board, partial: "boards/board", as: :board
|
||||
json.creator event.creator, partial: "users/user", as: :user
|
||||
@@ -0,0 +1 @@
|
||||
json.array! @page.records, partial: "activities/activity", as: :event
|
||||
@@ -133,6 +133,7 @@ Rails.application.routes.draw do
|
||||
end
|
||||
end
|
||||
|
||||
resources :activities, only: :index
|
||||
resources :events, only: :index
|
||||
namespace :events do
|
||||
resources :days
|
||||
|
||||
@@ -17,6 +17,7 @@ a bot to perform various actions for you.
|
||||
- [Reactions](sections/reactions.md)
|
||||
- [Tags](sections/tags.md)
|
||||
- [Users](sections/users.md)
|
||||
- [Activities](sections/activities.md)
|
||||
- [Notifications](sections/notifications.md)
|
||||
- [Rich Text](sections/rich_text.md)
|
||||
- [Exports](sections/exports.md)
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
# Activities
|
||||
|
||||
Activities are the activity stream for an account — a record of significant actions like cards being published, assigned, closed, and commented on.
|
||||
|
||||
## `GET /:account_slug/activities`
|
||||
|
||||
Returns a paginated flat list of activities the current user can access, sorted newest first.
|
||||
|
||||
__Query Parameters:__
|
||||
|
||||
| Parameter | Description |
|
||||
|-----------|-------------|
|
||||
| `creator_ids[]` | Filter to activities created by specific user ID(s). Multiple values are ORed. |
|
||||
| `board_ids[]` | Filter to activities on specific board ID(s). Multiple values are ORed. |
|
||||
|
||||
Different filter params are ANDed together: `creator_ids[]=A&board_ids[]=X` means activities created by A on board X.
|
||||
|
||||
To fetch activity for a specific user, use `creator_ids[]=USER_ID`.
|
||||
|
||||
__Supported actions:__
|
||||
|
||||
| `action` | `eventable_type` | `particulars` shape |
|
||||
|----------|-----------------|---------------------|
|
||||
| `card_assigned` | `Card` | `{ "assignee_ids": [USER_ID, ...] }` |
|
||||
| `card_auto_postponed` | `Card` | `{}` |
|
||||
| `card_board_changed` | `Card` | `{ "old_board": STRING, "new_board": STRING }` |
|
||||
| `card_closed` | `Card` | `{}` |
|
||||
| `card_postponed` | `Card` | `{}` |
|
||||
| `card_published` | `Card` | `{}` |
|
||||
| `card_reopened` | `Card` | `{}` |
|
||||
| `card_sent_back_to_triage` | `Card` | `{}` |
|
||||
| `card_title_changed` | `Card` | `{ "old_title": STRING, "new_title": STRING }` |
|
||||
| `card_triaged` | `Card` | `{ "column": STRING }` |
|
||||
| `card_unassigned` | `Card` | `{ "assignee_ids": [USER_ID, ...] }` |
|
||||
| `comment_created` | `Comment` | `{}` |
|
||||
|
||||
`particulars` is always an object. It contains action-specific metadata in a normalized format intended for API clients. It does not necessarily mirror the internal event JSON stored by Fizzy. Unknown keys may appear in the future and should be ignored. For `card_assigned` and `card_unassigned`, `assignee_ids` is currently a single-element array in practice.
|
||||
|
||||
__`particulars` examples:__
|
||||
|
||||
```json
|
||||
{ "action": "card_assigned", "particulars": { "assignee_ids": ["03f5user123"] } }
|
||||
{ "action": "card_unassigned", "particulars": { "assignee_ids": ["03f5user123"] } }
|
||||
{ "action": "card_board_changed", "particulars": { "old_board": "Backlog", "new_board": "Mobile" } }
|
||||
{ "action": "card_title_changed", "particulars": { "old_title": "Fix login", "new_title": "Fix mobile login" } }
|
||||
{ "action": "card_triaged", "particulars": { "column": "In Progress" } }
|
||||
{ "action": "card_closed", "particulars": {} }
|
||||
```
|
||||
|
||||
The practical `eventable_type` values are `Card` and `Comment`. Clients should handle unknown future values conservatively.
|
||||
|
||||
Activities whose underlying `Card` or `Comment` has been deleted or is inaccessible to the current user are omitted from the feed. The endpoint never returns `eventable: null`.
|
||||
|
||||
The top-level `board` field reflects the activity's current board association. If a card moves boards, all its activities move with it for the purposes of this feed and `board_ids[]` filtering.
|
||||
|
||||
This endpoint is a paginated activity feed, not an immutable audit-log. `description`, `eventable`, `board`, and `creator` may reflect current resource state. Re-fetch recent pages to get fresh activity data.
|
||||
|
||||
__Response:__
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "03faevt004",
|
||||
"action": "card_closed",
|
||||
"created_at": "2026-03-25T15:11:04.000Z",
|
||||
"description": "David Heinemeier Hansson moved \"Fix mobile login\" to \"Done\"",
|
||||
"particulars": {},
|
||||
"url": "http://fizzy.localhost:3006/897362094/cards/42",
|
||||
"eventable_type": "Card",
|
||||
"eventable": {
|
||||
"id": "03f6card042",
|
||||
"number": 42,
|
||||
"title": "Fix mobile login",
|
||||
"status": "closed",
|
||||
"description": "Users cannot complete login on iOS.",
|
||||
"description_html": "<div>Users cannot complete login on iOS.</div>",
|
||||
"image_url": null,
|
||||
"has_attachments": false,
|
||||
"tags": ["ios", "auth"],
|
||||
"closed": true,
|
||||
"postponed": false,
|
||||
"golden": false,
|
||||
"last_active_at": "2026-03-25T15:11:04.000Z",
|
||||
"created_at": "2026-03-25T09:00:00.000Z",
|
||||
"url": "http://fizzy.localhost:3006/897362094/cards/42"
|
||||
},
|
||||
"board": {
|
||||
"id": "03f6abc123",
|
||||
"name": "Mobile",
|
||||
"all_access": true,
|
||||
"created_at": "2026-03-01T10:00:00.000Z",
|
||||
"auto_postpone_period_in_days": 14,
|
||||
"url": "http://fizzy.localhost:3006/897362094/boards/03f6abc123"
|
||||
},
|
||||
"creator": {
|
||||
"id": "03f5user123",
|
||||
"name": "David Heinemeier Hansson",
|
||||
"role": "owner",
|
||||
"active": true,
|
||||
"email_address": "david@example.com",
|
||||
"created_at": "2026-03-01T09:00:00.000Z",
|
||||
"url": "http://fizzy.localhost:3006/897362094/users/03f5user123",
|
||||
"avatar_url": "http://fizzy.localhost:3006/897362094/users/03f5user123/avatar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "03faevt003",
|
||||
"action": "comment_created",
|
||||
"created_at": "2026-03-25T14:17:22.000Z",
|
||||
"description": "David Heinemeier Hansson commented on \"Fix mobile login\"",
|
||||
"particulars": {},
|
||||
"url": "http://fizzy.localhost:3006/897362094/cards/42#comment_03facomment9",
|
||||
"eventable_type": "Comment",
|
||||
"eventable": {
|
||||
"id": "03facomment9",
|
||||
"created_at": "2026-03-25T14:17:22.000Z",
|
||||
"updated_at": "2026-03-25T14:17:22.000Z",
|
||||
"body": {
|
||||
"plain_text": "I found the regression in the callback flow.",
|
||||
"html": "<div>I found the regression in the callback flow.</div>"
|
||||
},
|
||||
"creator": {
|
||||
"id": "03f5user123",
|
||||
"name": "David Heinemeier Hansson",
|
||||
"role": "owner",
|
||||
"active": true,
|
||||
"email_address": "david@example.com",
|
||||
"created_at": "2026-03-01T09:00:00.000Z",
|
||||
"url": "http://fizzy.localhost:3006/897362094/users/03f5user123",
|
||||
"avatar_url": "http://fizzy.localhost:3006/897362094/users/03f5user123/avatar"
|
||||
},
|
||||
"card": {
|
||||
"id": "03f6card042",
|
||||
"url": "http://fizzy.localhost:3006/897362094/cards/42"
|
||||
},
|
||||
"reactions_url": "http://fizzy.localhost:3006/897362094/cards/42/comments/03facomment9/reactions",
|
||||
"url": "http://fizzy.localhost:3006/897362094/cards/42/comments/03facomment9"
|
||||
},
|
||||
"board": {
|
||||
"id": "03f6abc123",
|
||||
"name": "Mobile",
|
||||
"all_access": true,
|
||||
"created_at": "2026-03-01T10:00:00.000Z",
|
||||
"auto_postpone_period_in_days": 14,
|
||||
"url": "http://fizzy.localhost:3006/897362094/boards/03f6abc123"
|
||||
},
|
||||
"creator": {
|
||||
"id": "03f5user123",
|
||||
"name": "David Heinemeier Hansson",
|
||||
"role": "owner",
|
||||
"active": true,
|
||||
"email_address": "david@example.com",
|
||||
"created_at": "2026-03-01T09:00:00.000Z",
|
||||
"url": "http://fizzy.localhost:3006/897362094/users/03f5user123",
|
||||
"avatar_url": "http://fizzy.localhost:3006/897362094/users/03f5user123/avatar"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
All `url` fields are opaque absolute URLs for the current Fizzy instance. Clients should not construct them.
|
||||
@@ -0,0 +1,272 @@
|
||||
require "test_helper"
|
||||
|
||||
class ActivitiesControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
sign_in_as :kevin
|
||||
end
|
||||
|
||||
test "index returns flat array of events with required envelope fields" do
|
||||
get activities_path, as: :json
|
||||
assert_response :success
|
||||
|
||||
body = @response.parsed_body
|
||||
assert_kind_of Array, body
|
||||
assert body.any?
|
||||
|
||||
event = body.first
|
||||
assert_includes event.keys, "id"
|
||||
assert_includes event.keys, "action"
|
||||
assert_includes event.keys, "created_at"
|
||||
assert_includes event.keys, "description"
|
||||
assert_includes event.keys, "particulars"
|
||||
assert_includes event.keys, "url"
|
||||
assert_includes event.keys, "eventable_type"
|
||||
assert_includes event.keys, "eventable"
|
||||
assert_includes event.keys, "board"
|
||||
assert_includes event.keys, "creator"
|
||||
end
|
||||
|
||||
test "index returns events in reverse-chronological order" do
|
||||
get activities_path, as: :json
|
||||
assert_response :success
|
||||
|
||||
times = @response.parsed_body.map { |e| Time.parse(e["created_at"]) }
|
||||
assert_equal times, times.sort.reverse
|
||||
end
|
||||
|
||||
test "index includes card eventable" do
|
||||
event = events(:logo_published)
|
||||
get activities_path, as: :json
|
||||
assert_response :success
|
||||
|
||||
item = @response.parsed_body.find { |e| e["id"] == event.id }
|
||||
assert_not_nil item
|
||||
assert_equal "Card", item["eventable_type"]
|
||||
assert_equal cards(:logo).title, item["eventable"]["title"]
|
||||
assert_equal "card_published", item["action"]
|
||||
assert item["url"].end_with?("/cards/#{cards(:logo).number}")
|
||||
end
|
||||
|
||||
test "index includes comment eventable" do
|
||||
event = events(:layout_commented)
|
||||
get activities_path, as: :json
|
||||
assert_response :success
|
||||
|
||||
item = @response.parsed_body.find { |e| e["id"] == event.id }
|
||||
assert_not_nil item
|
||||
assert_equal "Comment", item["eventable_type"]
|
||||
assert_equal card_url(event.eventable.card, anchor: ActionView::RecordIdentifier.dom_id(event.eventable)), item["url"]
|
||||
assert item["eventable"]["body"].present?
|
||||
end
|
||||
|
||||
test "index includes board and creator at top level" do
|
||||
get activities_path, as: :json
|
||||
assert_response :success
|
||||
|
||||
item = @response.parsed_body.first
|
||||
assert item["board"]["id"].present?
|
||||
assert item["board"]["name"].present?
|
||||
assert item["creator"]["id"].present?
|
||||
assert item["creator"]["name"].present?
|
||||
end
|
||||
|
||||
test "index normalizes particulars for card_assigned" do
|
||||
event = events(:logo_assignment_jz)
|
||||
get activities_path, as: :json
|
||||
assert_response :success
|
||||
|
||||
item = @response.parsed_body.find { |e| e["id"] == event.id }
|
||||
assert_not_nil item
|
||||
assert_equal "card_assigned", item["action"]
|
||||
assert_equal [ users(:jz).id ], item["particulars"]["assignee_ids"]
|
||||
end
|
||||
|
||||
test "index normalizes particulars for card_unassigned" do
|
||||
card = cards(:logo)
|
||||
event = card.board.events.create!(
|
||||
action: "card_unassigned",
|
||||
creator: users(:david),
|
||||
eventable: card,
|
||||
account: accounts("37s"),
|
||||
particulars: { assignee_ids: [ users(:jz).id ] }
|
||||
)
|
||||
|
||||
get activities_path, as: :json
|
||||
assert_response :success
|
||||
|
||||
item = @response.parsed_body.find { |e| e["id"] == event.id }
|
||||
assert_not_nil item
|
||||
assert_equal [ users(:jz).id ], item["particulars"]["assignee_ids"]
|
||||
end
|
||||
|
||||
test "index returns empty particulars for actions with no payload" do
|
||||
event = events(:logo_published)
|
||||
get activities_path, as: :json
|
||||
assert_response :success
|
||||
|
||||
item = @response.parsed_body.find { |e| e["id"] == event.id }
|
||||
assert_not_nil item
|
||||
assert_equal({}, item["particulars"])
|
||||
end
|
||||
|
||||
test "index normalizes particulars for card_board_changed" do
|
||||
card = cards(:logo)
|
||||
event = card.board.events.create!(
|
||||
action: "card_board_changed",
|
||||
creator: users(:david),
|
||||
eventable: card,
|
||||
account: accounts("37s"),
|
||||
particulars: { particulars: { old_board: "Backlog", new_board: "Mobile" } }
|
||||
)
|
||||
|
||||
get activities_path, as: :json
|
||||
assert_response :success
|
||||
|
||||
item = @response.parsed_body.find { |e| e["id"] == event.id }
|
||||
assert_not_nil item
|
||||
assert_equal "Backlog", item["particulars"]["old_board"]
|
||||
assert_equal "Mobile", item["particulars"]["new_board"]
|
||||
end
|
||||
|
||||
test "index normalizes particulars for card_title_changed" do
|
||||
card = cards(:logo)
|
||||
event = card.board.events.create!(
|
||||
action: "card_title_changed",
|
||||
creator: users(:david),
|
||||
eventable: card,
|
||||
account: accounts("37s"),
|
||||
particulars: { particulars: { old_title: "Old title", new_title: "New title" } }
|
||||
)
|
||||
|
||||
get activities_path, as: :json
|
||||
assert_response :success
|
||||
|
||||
item = @response.parsed_body.find { |e| e["id"] == event.id }
|
||||
assert_not_nil item
|
||||
assert_equal "Old title", item["particulars"]["old_title"]
|
||||
assert_equal "New title", item["particulars"]["new_title"]
|
||||
end
|
||||
|
||||
test "index normalizes particulars for card_triaged" do
|
||||
card = cards(:logo)
|
||||
event = card.board.events.create!(
|
||||
action: "card_triaged",
|
||||
creator: users(:david),
|
||||
eventable: card,
|
||||
account: accounts("37s"),
|
||||
particulars: { particulars: { column: "In Progress" } }
|
||||
)
|
||||
|
||||
get activities_path, as: :json
|
||||
assert_response :success
|
||||
|
||||
item = @response.parsed_body.find { |e| e["id"] == event.id }
|
||||
assert_not_nil item
|
||||
assert_equal "In Progress", item["particulars"]["column"]
|
||||
end
|
||||
|
||||
test "index filters by creator_ids" do
|
||||
get activities_path(creator_ids: [ users(:kevin).id ]), as: :json
|
||||
assert_response :success
|
||||
|
||||
body = @response.parsed_body
|
||||
assert body.any?
|
||||
assert body.all? { |e| e["creator"]["id"] == users(:kevin).id }
|
||||
end
|
||||
|
||||
test "index filters by multiple creator_ids with OR semantics" do
|
||||
get activities_path(creator_ids: [ users(:david).id, users(:kevin).id ]), as: :json
|
||||
assert_response :success
|
||||
|
||||
body = @response.parsed_body
|
||||
creator_ids = body.map { |e| e["creator"]["id"] }.uniq.sort
|
||||
assert_includes creator_ids, users(:david).id
|
||||
assert_includes creator_ids, users(:kevin).id
|
||||
end
|
||||
|
||||
test "index filters by board_ids" do
|
||||
get activities_path(board_ids: [ boards(:writebook).id ]), as: :json
|
||||
assert_response :success
|
||||
|
||||
body = @response.parsed_body
|
||||
assert body.any?
|
||||
assert body.all? { |e| e["board"]["id"] == boards(:writebook).id }
|
||||
end
|
||||
|
||||
test "index ANDs creator_ids and board_ids filters" do
|
||||
get activities_path(creator_ids: [ users(:david).id ], board_ids: [ boards(:writebook).id ]), as: :json
|
||||
assert_response :success
|
||||
|
||||
body = @response.parsed_body
|
||||
assert body.any?
|
||||
assert body.all? { |e| e["creator"]["id"] == users(:david).id }
|
||||
assert body.all? { |e| e["board"]["id"] == boards(:writebook).id }
|
||||
end
|
||||
|
||||
test "index ignores empty creator_ids and board_ids filters" do
|
||||
get activities_path(creator_ids: [], board_ids: []), as: :json
|
||||
assert_response :success
|
||||
|
||||
assert_predicate @response.parsed_body, :any?
|
||||
end
|
||||
|
||||
test "index only returns events from accessible boards" do
|
||||
get activities_path, as: :json
|
||||
assert_response :success
|
||||
|
||||
accessible_board_ids = users(:kevin).boards.pluck(:id)
|
||||
@response.parsed_body.each do |item|
|
||||
assert_includes accessible_board_ids, item["board"]["id"]
|
||||
end
|
||||
end
|
||||
|
||||
test "index paginates and returns Link header when more results exist" do
|
||||
board = boards(:writebook)
|
||||
30.times do |i|
|
||||
board.events.create!(
|
||||
action: "card_published",
|
||||
creator: users(:kevin),
|
||||
eventable: cards(:logo),
|
||||
account: accounts("37s")
|
||||
)
|
||||
end
|
||||
|
||||
get activities_path, as: :json
|
||||
assert_response :success
|
||||
|
||||
link_header = @response.headers["Link"]
|
||||
assert link_header.present?, "Expected a Link header for paginated results"
|
||||
assert_match(/rel="next"/, link_header)
|
||||
end
|
||||
|
||||
test "index follows pagination to retrieve all events" do
|
||||
board = boards(:writebook)
|
||||
30.times do
|
||||
board.events.create!(
|
||||
action: "card_published",
|
||||
creator: users(:kevin),
|
||||
eventable: cards(:logo),
|
||||
account: accounts("37s")
|
||||
)
|
||||
end
|
||||
|
||||
all_ids = []
|
||||
next_path = activities_path(format: :json)
|
||||
|
||||
while next_path
|
||||
get next_path
|
||||
assert_response :success
|
||||
all_ids.concat(@response.parsed_body.map { |e| e["id"] })
|
||||
next_path = next_page_from_link_header(@response.headers["Link"])
|
||||
end
|
||||
|
||||
total = Event.where(board: users(:kevin).boards).where(action: ActivitiesController::ACTIONS).count
|
||||
assert_equal total, all_ids.uniq.count
|
||||
end
|
||||
|
||||
private
|
||||
def next_page_from_link_header(link_header)
|
||||
url = link_header&.match(/<([^>]+)>;\s*rel="next"/)&.captures&.first
|
||||
URI.parse(url).request_uri if url
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,53 @@
|
||||
require "test_helper"
|
||||
|
||||
class EventTest < ActiveSupport::TestCase
|
||||
test "blank creator and board filters return the current relation unchanged" do
|
||||
relation = Event.where(action: "card_published")
|
||||
|
||||
assert_equal relation.to_sql, relation.for_creators(nil).to_sql
|
||||
assert_equal relation.to_sql, relation.for_creators([]).to_sql
|
||||
assert_equal relation.to_sql, relation.for_boards(nil).to_sql
|
||||
assert_equal relation.to_sql, relation.for_boards([]).to_sql
|
||||
end
|
||||
|
||||
test "blank creator and board filters remain chainable" do
|
||||
relation = Event.where(action: "card_published")
|
||||
|
||||
assert_nothing_raised do
|
||||
relation.for_creators([]).for_boards([]).load
|
||||
end
|
||||
end
|
||||
|
||||
test "api_particulars returns empty strings for missing nested board change values" do
|
||||
event = boards(:writebook).events.create!(
|
||||
action: "card_board_changed",
|
||||
creator: users(:david),
|
||||
eventable: cards(:logo),
|
||||
account: accounts("37s"),
|
||||
particulars: {}
|
||||
)
|
||||
|
||||
assert_equal({ "old_board" => "", "new_board" => "" }, event.api_particulars)
|
||||
end
|
||||
|
||||
test "api_particulars returns empty strings for missing nested title and column values" do
|
||||
title_event = boards(:writebook).events.create!(
|
||||
action: "card_title_changed",
|
||||
creator: users(:david),
|
||||
eventable: cards(:logo),
|
||||
account: accounts("37s"),
|
||||
particulars: {}
|
||||
)
|
||||
|
||||
triage_event = boards(:writebook).events.create!(
|
||||
action: "card_triaged",
|
||||
creator: users(:david),
|
||||
eventable: cards(:logo),
|
||||
account: accounts("37s"),
|
||||
particulars: {}
|
||||
)
|
||||
|
||||
assert_equal({ "old_title" => "", "new_title" => "" }, title_event.api_particulars)
|
||||
assert_equal({ "column" => "" }, triage_event.api_particulars)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user