jumbotron 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/adapters/jumbotron/adapters/espn/nfl.rb +15 -0
- data/app/adapters/jumbotron/adapters/espn/nfl_helpers/scoreboard.rb +15 -2
- data/app/adapters/jumbotron/adapters/espn/nfl_helpers/team_nickname.rb +19 -0
- data/app/adapters/jumbotron/adapters/espn/nfl_helpers/teams.rb +3 -1
- data/app/deserializers/jumbotron/deserializers/clients/espn/competitor.rb +14 -1
- data/app/deserializers/jumbotron/deserializers/clients/espn/competitor_record.rb +27 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/team.rb +2 -0
- data/app/deserializers/jumbotron/deserializers/clients/espn/venue.rb +10 -1
- data/app/deserializers/jumbotron/deserializers/clients/espn/venue_address.rb +27 -0
- data/app/models/jumbotron/canonical.rb +28 -4
- data/app/models/jumbotron/public.rb +15 -3
- data/app/services/jumbotron/services/adapters/resolve_game_update_policy.rb +1 -0
- data/app/services/jumbotron/services/adapters/select_eligible_games.rb +1 -1
- data/app/services/jumbotron/services/canonical/upsert_game_participant.rb +92 -3
- data/app/services/jumbotron/services/canonical/upsert_team.rb +48 -0
- data/app/services/jumbotron/services/canonical/upsert_venue.rb +33 -1
- data/app/services/jumbotron/services/public/assemble_public_game.rb +21 -2
- data/db/migrate/20260828180001_add_nickname_to_jumbotron_teams.rb +7 -0
- data/db/migrate/20260828220001_add_game_presentation_contract_fields.rb +19 -0
- data/lib/jumbotron/version.rb +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 59e55efb1ece4d237011869ce7416fd4cda99a2e95feb914c21e93a3f23aecaa
|
|
4
|
+
data.tar.gz: 2199925a6f4a46d2db581c63e2a5eb55b81a26483195a396a4b7c111ae41bee1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ae7c4b2c06eed8291bafcb279940f0ca45ad5b33a62cc5970a5c1d353eae0c4d8bde4173e62811d13dade3038107940a828f37b2b3e1c1d6a7f2c118f62d5447
|
|
7
|
+
data.tar.gz: 8ecd32a04d755e5eb0422fccbb98b10beead9f3b88de8f8e885f2b8e4f458af6a672d1bdf9a63040b88e352186cea328565263dff18d001298b1a0cf97389748
|
|
@@ -66,6 +66,21 @@ module Jumbotron
|
|
|
66
66
|
INTERRUPTED_LIFECYCLES.include?(game.lifecycle)
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
# Bounded post-final scoreboard re-observation when post-game record is still
|
|
70
|
+
# missing (Strategy B). Stops after window or once post_game is persisted.
|
|
71
|
+
policy :post_final_record,
|
|
72
|
+
type: :post_final_game_update,
|
|
73
|
+
cadence: Cadence.new(every: 15, unit: :minute),
|
|
74
|
+
eligible: lambda { |game, now:|
|
|
75
|
+
return false unless game.lifecycle == "completed"
|
|
76
|
+
return false if game.changed_at.nil? || game.changed_at < now - 6.hours
|
|
77
|
+
|
|
78
|
+
participants = game.game_participants
|
|
79
|
+
return false if participants.empty?
|
|
80
|
+
|
|
81
|
+
participants.any? { |participant| participant.record_summary_post_game.blank? }
|
|
82
|
+
}
|
|
83
|
+
|
|
69
84
|
policy :far_future_lines,
|
|
70
85
|
type: :future_line_update,
|
|
71
86
|
cadence: Cadence.new(every: 1, unit: :week),
|
|
@@ -80,11 +80,14 @@ module Jumbotron
|
|
|
80
80
|
raise TransformError, "venue id required" if venue.id.blank?
|
|
81
81
|
raise TransformError, "venue name required" if venue.full_name.blank?
|
|
82
82
|
|
|
83
|
+
address = venue.respond_to?(:address) ? venue.address : nil
|
|
83
84
|
Canonical::VenueInput.new(
|
|
84
85
|
provider_identities: [
|
|
85
86
|
Canonical::ProviderIdentityRef.new(provider: "espn", namespace: "venue", id: venue.id.to_s)
|
|
86
87
|
],
|
|
87
|
-
name: venue.full_name.to_s
|
|
88
|
+
name: venue.full_name.to_s,
|
|
89
|
+
city: address&.city.presence,
|
|
90
|
+
region: address&.state.presence
|
|
88
91
|
)
|
|
89
92
|
end
|
|
90
93
|
private_class_method :transform_venue
|
|
@@ -103,13 +106,23 @@ module Jumbotron
|
|
|
103
106
|
Canonical::ProviderIdentityRef.new(provider: "espn", namespace: "team", id: team.id.to_s)
|
|
104
107
|
],
|
|
105
108
|
team_name: name.to_s,
|
|
109
|
+
team_nickname: TeamNickname.resolve(team),
|
|
110
|
+
team_abbreviation: team.abbreviation.presence,
|
|
106
111
|
role: competitor.home_away.to_s,
|
|
107
112
|
score: coerce_score(competitor.score),
|
|
108
|
-
result: coerce_result(competitor.winner, lifecycle)
|
|
113
|
+
result: coerce_result(competitor.winner, lifecycle),
|
|
114
|
+
record_summary: total_record_summary(competitor)
|
|
109
115
|
)
|
|
110
116
|
end
|
|
111
117
|
private_class_method :transform_participant
|
|
112
118
|
|
|
119
|
+
def self.total_record_summary(competitor)
|
|
120
|
+
records = competitor.respond_to?(:records) ? Array(competitor.records) : []
|
|
121
|
+
total = records.find { |record| record.type.to_s == "total" }
|
|
122
|
+
total&.summary.presence
|
|
123
|
+
end
|
|
124
|
+
private_class_method :total_record_summary
|
|
125
|
+
|
|
113
126
|
def self.coerce_score(raw)
|
|
114
127
|
return nil if raw.nil? || raw == ""
|
|
115
128
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Adapters
|
|
5
|
+
module Espn
|
|
6
|
+
module NflHelpers
|
|
7
|
+
# Maps ESPN team identity fields into canonical nickname without parsing display_name.
|
|
8
|
+
# Precedence: nickname → name → short_display_name (provider fields only).
|
|
9
|
+
module TeamNickname
|
|
10
|
+
def self.resolve(team)
|
|
11
|
+
return nil if team.nil?
|
|
12
|
+
|
|
13
|
+
team.nickname.presence || team.name.presence || team.short_display_name.presence
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -24,7 +24,9 @@ module Jumbotron
|
|
|
24
24
|
provider_identities: [
|
|
25
25
|
Canonical::ProviderIdentityRef.new(provider: "espn", namespace: "team", id: team.id.to_s)
|
|
26
26
|
],
|
|
27
|
-
name: name.to_s
|
|
27
|
+
name: name.to_s,
|
|
28
|
+
nickname: TeamNickname.resolve(team),
|
|
29
|
+
abbreviation: team.abbreviation.presence
|
|
28
30
|
)
|
|
29
31
|
end
|
|
30
32
|
private_class_method :transform_team
|
|
@@ -13,6 +13,7 @@ module Jumbotron
|
|
|
13
13
|
field :winner, type: Support.types.boolean, nullable: true
|
|
14
14
|
field :order, type: Support.types.integer, nullable: true
|
|
15
15
|
field :team, type: Team::Result, required: true
|
|
16
|
+
field :records, type: Support.types.array(CompetitorRecord::Result), nullable: true
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def self.call(payload)
|
|
@@ -25,10 +26,22 @@ module Jumbotron
|
|
|
25
26
|
score: coerce_score(Support.payload.fetch(payload, "score")),
|
|
26
27
|
winner: Support.payload.fetch(payload, "winner"),
|
|
27
28
|
order: Support.payload.fetch(payload, "order"),
|
|
28
|
-
team: Support.nest("team", Support.payload.fetch(payload, "team")) { |raw| Team.call(raw) }
|
|
29
|
+
team: Support.nest("team", Support.payload.fetch(payload, "team")) { |raw| Team.call(raw) },
|
|
30
|
+
records: map_records(Support.payload.fetch(payload, "records"))
|
|
29
31
|
)
|
|
30
32
|
end
|
|
31
33
|
|
|
34
|
+
def self.map_records(raw)
|
|
35
|
+
return nil if raw.equal?(Support.missing) || raw.nil?
|
|
36
|
+
|
|
37
|
+
Array(raw).each_with_index.map do |item, index|
|
|
38
|
+
CompetitorRecord.call(item)
|
|
39
|
+
rescue CommandTower::Clients::Errors::DeserializationError => e
|
|
40
|
+
raise CommandTower::Deserializers::Clients::Errors.prefix(e, "records[#{index}]")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
private_class_method :map_records
|
|
44
|
+
|
|
32
45
|
def self.coerce_id(raw)
|
|
33
46
|
return raw if raw.equal?(Support.missing) || raw.nil? || raw.is_a?(String)
|
|
34
47
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Deserializers
|
|
5
|
+
module Clients
|
|
6
|
+
module Espn
|
|
7
|
+
class CompetitorRecord
|
|
8
|
+
class Result < CommandTower::Deserializers::Clients::Result
|
|
9
|
+
field :type, type: Support.types.string, required: true
|
|
10
|
+
field :summary, type: Support.types.string, required: true
|
|
11
|
+
field :name, type: Support.types.string, nullable: true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.call(payload)
|
|
15
|
+
Support.ensure_hash!(payload, label: "competitor.record")
|
|
16
|
+
|
|
17
|
+
Result.build!(
|
|
18
|
+
type: Support.payload.fetch(payload, "type"),
|
|
19
|
+
summary: Support.payload.fetch(payload, "summary"),
|
|
20
|
+
name: Support.payload.fetch(payload, "name")
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -12,6 +12,7 @@ module Jumbotron
|
|
|
12
12
|
field :display_name, type: Support.types.string, required: true
|
|
13
13
|
field :short_display_name, type: Support.types.string, nullable: true
|
|
14
14
|
field :name, type: Support.types.string, nullable: true
|
|
15
|
+
field :nickname, type: Support.types.string, nullable: true
|
|
15
16
|
field :location, type: Support.types.string, nullable: true
|
|
16
17
|
end
|
|
17
18
|
|
|
@@ -25,6 +26,7 @@ module Jumbotron
|
|
|
25
26
|
display_name: Support.payload.fetch(payload, "displayName"),
|
|
26
27
|
short_display_name: Support.payload.fetch(payload, "shortDisplayName"),
|
|
27
28
|
name: Support.payload.fetch(payload, "name"),
|
|
29
|
+
nickname: Support.payload.fetch(payload, "nickname"),
|
|
28
30
|
location: Support.payload.fetch(payload, "location")
|
|
29
31
|
)
|
|
30
32
|
end
|
|
@@ -9,6 +9,7 @@ module Jumbotron
|
|
|
9
9
|
field :id, type: Support.types.string, required: true
|
|
10
10
|
field :full_name, type: Support.types.string, required: true
|
|
11
11
|
field :indoor, type: Support.types.boolean, nullable: true
|
|
12
|
+
field :address, type: VenueAddress::Result, nullable: true
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def self.call(payload)
|
|
@@ -17,10 +18,18 @@ module Jumbotron
|
|
|
17
18
|
Result.build!(
|
|
18
19
|
id: coerce_id(Support.payload.fetch(payload, "id")),
|
|
19
20
|
full_name: Support.payload.fetch(payload, "fullName"),
|
|
20
|
-
indoor: Support.payload.fetch(payload, "indoor")
|
|
21
|
+
indoor: Support.payload.fetch(payload, "indoor"),
|
|
22
|
+
address: map_address(Support.payload.fetch(payload, "address"))
|
|
21
23
|
)
|
|
22
24
|
end
|
|
23
25
|
|
|
26
|
+
def self.map_address(raw)
|
|
27
|
+
return nil if raw.equal?(Support.missing) || raw.nil?
|
|
28
|
+
|
|
29
|
+
VenueAddress.call(raw)
|
|
30
|
+
end
|
|
31
|
+
private_class_method :map_address
|
|
32
|
+
|
|
24
33
|
def self.coerce_id(raw)
|
|
25
34
|
return raw if raw.equal?(Support.missing) || raw.nil? || raw.is_a?(String)
|
|
26
35
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jumbotron
|
|
4
|
+
module Deserializers
|
|
5
|
+
module Clients
|
|
6
|
+
module Espn
|
|
7
|
+
class VenueAddress
|
|
8
|
+
class Result < CommandTower::Deserializers::Clients::Result
|
|
9
|
+
field :city, type: Support.types.string, nullable: true
|
|
10
|
+
field :state, type: Support.types.string, nullable: true
|
|
11
|
+
field :country, type: Support.types.string, nullable: true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.call(payload)
|
|
15
|
+
Support.ensure_hash!(payload, label: "venue.address")
|
|
16
|
+
|
|
17
|
+
Result.build!(
|
|
18
|
+
city: Support.payload.fetch(payload, "city"),
|
|
19
|
+
state: Support.payload.fetch(payload, "state"),
|
|
20
|
+
country: Support.payload.fetch(payload, "country")
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -4,17 +4,41 @@ module Jumbotron
|
|
|
4
4
|
module Canonical
|
|
5
5
|
ProviderIdentityRef = Data.define(:provider, :namespace, :id)
|
|
6
6
|
|
|
7
|
-
TeamInput = Data.define(:provider_identities, :name)
|
|
7
|
+
TeamInput = Data.define(:provider_identities, :name, :nickname, :abbreviation) do
|
|
8
|
+
def initialize(provider_identities:, name:, nickname: nil, abbreviation: nil)
|
|
9
|
+
super
|
|
10
|
+
end
|
|
11
|
+
end
|
|
8
12
|
|
|
9
|
-
VenueInput = Data.define(:provider_identities, :name)
|
|
13
|
+
VenueInput = Data.define(:provider_identities, :name, :city, :region) do
|
|
14
|
+
def initialize(provider_identities:, name:, city: nil, region: nil)
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
end
|
|
10
18
|
|
|
11
19
|
ParticipantInput = Data.define(
|
|
12
20
|
:provider_identities,
|
|
13
21
|
:team_name,
|
|
14
22
|
:role,
|
|
15
23
|
:score,
|
|
16
|
-
:result
|
|
17
|
-
|
|
24
|
+
:result,
|
|
25
|
+
:team_nickname,
|
|
26
|
+
:team_abbreviation,
|
|
27
|
+
:record_summary
|
|
28
|
+
) do
|
|
29
|
+
def initialize(
|
|
30
|
+
provider_identities:,
|
|
31
|
+
team_name:,
|
|
32
|
+
role:,
|
|
33
|
+
score:,
|
|
34
|
+
result:,
|
|
35
|
+
team_nickname: nil,
|
|
36
|
+
team_abbreviation: nil,
|
|
37
|
+
record_summary: nil
|
|
38
|
+
)
|
|
39
|
+
super
|
|
40
|
+
end
|
|
41
|
+
end
|
|
18
42
|
|
|
19
43
|
ScheduleGroupInput = Data.define(:kind, :number, :name)
|
|
20
44
|
|
|
@@ -19,9 +19,21 @@ module Jumbotron
|
|
|
19
19
|
:season_phase,
|
|
20
20
|
:kind
|
|
21
21
|
)
|
|
22
|
-
Team = Data.define(:id, :name, :key)
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
Team = Data.define(:id, :name, :nickname, :key, :abbreviation) do
|
|
23
|
+
def initialize(id:, name:, key:, nickname: nil, abbreviation: nil)
|
|
24
|
+
super(id:, name:, nickname:, key:, abbreviation:)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
Venue = Data.define(:id, :name, :city, :region) do
|
|
28
|
+
def initialize(id:, name:, city: nil, region: nil)
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
Participant = Data.define(:role, :score, :result, :team, :record) do
|
|
33
|
+
def initialize(role:, score:, result:, team:, record: nil)
|
|
34
|
+
super
|
|
35
|
+
end
|
|
36
|
+
end
|
|
25
37
|
GameSegment = Data.define(:kind, :number)
|
|
26
38
|
# Clock label text. Name is canonical/public contract, not Kernel#display.
|
|
27
39
|
GameClock = Data.define(:mode, :seconds, :display) # rubocop:disable Lint/DataDefineOverride
|
|
@@ -20,7 +20,7 @@ module Jumbotron
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
games = Game.where(league_id: league.id)
|
|
23
|
-
.includes(:season, :season_phase, :schedule_group)
|
|
23
|
+
.includes(:season, :season_phase, :schedule_group, :game_participants)
|
|
24
24
|
.select { |game| policy.eligible?(game, now: now) }
|
|
25
25
|
|
|
26
26
|
context.league = league
|
|
@@ -13,7 +13,9 @@ module Jumbotron
|
|
|
13
13
|
team_result = UpsertTeam.call(
|
|
14
14
|
team_input: Jumbotron::Canonical::TeamInput.new(
|
|
15
15
|
provider_identities: participant_input.provider_identities,
|
|
16
|
-
name: participant_input.team_name
|
|
16
|
+
name: participant_input.team_name,
|
|
17
|
+
nickname: participant_input.team_nickname,
|
|
18
|
+
abbreviation: participant_input.team_abbreviation
|
|
17
19
|
),
|
|
18
20
|
observed_at: observed_at,
|
|
19
21
|
change_set: change_set
|
|
@@ -45,16 +47,18 @@ module Jumbotron
|
|
|
45
47
|
end
|
|
46
48
|
|
|
47
49
|
def create_new!(team)
|
|
48
|
-
|
|
50
|
+
attrs = {
|
|
49
51
|
team: team,
|
|
50
52
|
role: participant_input.role,
|
|
51
53
|
score: participant_input.score,
|
|
52
54
|
result: participant_input.result,
|
|
53
55
|
observed_at: observed_at,
|
|
54
56
|
changed_at: observed_at
|
|
55
|
-
)
|
|
57
|
+
}.merge(initial_record_attrs)
|
|
58
|
+
gp = game.game_participants.create!(attrs)
|
|
56
59
|
change_set.record(subject: gp, attribute: "role", previous: nil, new_value: gp.role)
|
|
57
60
|
change_set.record(subject: gp, attribute: "score", previous: nil, new_value: gp.score)
|
|
61
|
+
record_create_history!(gp)
|
|
58
62
|
gp
|
|
59
63
|
end
|
|
60
64
|
|
|
@@ -77,11 +81,96 @@ module Jumbotron
|
|
|
77
81
|
participant.public_send("#{key}=", value)
|
|
78
82
|
material = true
|
|
79
83
|
end
|
|
84
|
+
material |= apply_record_snapshots!(participant)
|
|
80
85
|
participant.observed_at = observed_at
|
|
81
86
|
participant.changed_at = observed_at if material
|
|
82
87
|
participant.save!
|
|
83
88
|
participant
|
|
84
89
|
end
|
|
90
|
+
|
|
91
|
+
def initial_record_attrs
|
|
92
|
+
summary = participant_input.record_summary
|
|
93
|
+
return {} if summary.blank?
|
|
94
|
+
|
|
95
|
+
attrs = {
|
|
96
|
+
record_summary_current: summary,
|
|
97
|
+
record_observed_at: observed_at
|
|
98
|
+
}
|
|
99
|
+
if game.lifecycle == "completed"
|
|
100
|
+
attrs[:record_summary_post_game] = summary
|
|
101
|
+
elsif game.lifecycle != "cancelled"
|
|
102
|
+
attrs[:record_summary_entering] = summary
|
|
103
|
+
end
|
|
104
|
+
attrs
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def record_create_history!(participant)
|
|
108
|
+
%i[
|
|
109
|
+
record_summary_entering
|
|
110
|
+
record_summary_current
|
|
111
|
+
record_summary_post_game
|
|
112
|
+
].each do |attr|
|
|
113
|
+
value = participant.public_send(attr)
|
|
114
|
+
next if value.blank?
|
|
115
|
+
|
|
116
|
+
change_set.record(subject: participant, attribute: attr.to_s, previous: nil, new_value: value)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def apply_record_snapshots!(participant)
|
|
121
|
+
summary = participant_input.record_summary
|
|
122
|
+
return false if summary.blank?
|
|
123
|
+
|
|
124
|
+
material = false
|
|
125
|
+
if participant.record_summary_current != summary
|
|
126
|
+
change_set.record(
|
|
127
|
+
subject: participant,
|
|
128
|
+
attribute: "record_summary_current",
|
|
129
|
+
previous: participant.record_summary_current,
|
|
130
|
+
new_value: summary
|
|
131
|
+
)
|
|
132
|
+
participant.record_summary_current = summary
|
|
133
|
+
material = true
|
|
134
|
+
end
|
|
135
|
+
participant.record_observed_at = observed_at
|
|
136
|
+
|
|
137
|
+
if game.lifecycle == "completed"
|
|
138
|
+
material |= apply_post_game_record!(participant, summary)
|
|
139
|
+
elsif game.lifecycle != "cancelled"
|
|
140
|
+
material |= assign_entering_record!(participant, summary)
|
|
141
|
+
end
|
|
142
|
+
material
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def assign_entering_record!(participant, summary) # rubocop:disable Naming/PredicateMethod
|
|
146
|
+
return false if participant.record_summary_entering == summary
|
|
147
|
+
|
|
148
|
+
change_set.record(
|
|
149
|
+
subject: participant,
|
|
150
|
+
attribute: "record_summary_entering",
|
|
151
|
+
previous: participant.record_summary_entering,
|
|
152
|
+
new_value: summary
|
|
153
|
+
)
|
|
154
|
+
participant.record_summary_entering = summary
|
|
155
|
+
true
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Authoritative post-game only: never invent W–L; if total still equals
|
|
159
|
+
# entering on a completed observation, leave post_game nil (Strategy B).
|
|
160
|
+
def apply_post_game_record!(participant, summary) # rubocop:disable Naming/PredicateMethod
|
|
161
|
+
entering = participant.record_summary_entering
|
|
162
|
+
return false if entering.present? && summary == entering
|
|
163
|
+
return false if participant.record_summary_post_game == summary
|
|
164
|
+
|
|
165
|
+
change_set.record(
|
|
166
|
+
subject: participant,
|
|
167
|
+
attribute: "record_summary_post_game",
|
|
168
|
+
previous: participant.record_summary_post_game,
|
|
169
|
+
new_value: summary
|
|
170
|
+
)
|
|
171
|
+
participant.record_summary_post_game = summary
|
|
172
|
+
true
|
|
173
|
+
end
|
|
85
174
|
end
|
|
86
175
|
end
|
|
87
176
|
end
|
|
@@ -57,6 +57,8 @@ module Jumbotron
|
|
|
57
57
|
team.name = team_input.name
|
|
58
58
|
team.changed_at = observed_at
|
|
59
59
|
end
|
|
60
|
+
apply_nickname!(team)
|
|
61
|
+
apply_abbreviation!(team)
|
|
60
62
|
# key is immutable after assignment — never regenerate from name or provider id
|
|
61
63
|
team.observed_at = observed_at
|
|
62
64
|
team.save!
|
|
@@ -66,15 +68,61 @@ module Jumbotron
|
|
|
66
68
|
def create_new!
|
|
67
69
|
team = Team.create!(
|
|
68
70
|
name: team_input.name,
|
|
71
|
+
nickname: team_input.nickname,
|
|
72
|
+
abbreviation: team_input.abbreviation,
|
|
69
73
|
key: allocate_key!(team_input.name),
|
|
70
74
|
observed_at: observed_at,
|
|
71
75
|
changed_at: observed_at
|
|
72
76
|
)
|
|
73
77
|
change_set.record(subject: team, attribute: "name", previous: nil, new_value: team.name)
|
|
74
78
|
change_set.record(subject: team, attribute: "key", previous: nil, new_value: team.key)
|
|
79
|
+
if team.nickname.present?
|
|
80
|
+
change_set.record(
|
|
81
|
+
subject: team,
|
|
82
|
+
attribute: "nickname",
|
|
83
|
+
previous: nil,
|
|
84
|
+
new_value: team.nickname
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
if team.abbreviation.present?
|
|
88
|
+
change_set.record(
|
|
89
|
+
subject: team,
|
|
90
|
+
attribute: "abbreviation",
|
|
91
|
+
previous: nil,
|
|
92
|
+
new_value: team.abbreviation
|
|
93
|
+
)
|
|
94
|
+
end
|
|
75
95
|
team
|
|
76
96
|
end
|
|
77
97
|
|
|
98
|
+
def apply_nickname!(team)
|
|
99
|
+
return if team_input.nickname.nil?
|
|
100
|
+
return if team.nickname == team_input.nickname
|
|
101
|
+
|
|
102
|
+
change_set.record(
|
|
103
|
+
subject: team,
|
|
104
|
+
attribute: "nickname",
|
|
105
|
+
previous: team.nickname,
|
|
106
|
+
new_value: team_input.nickname
|
|
107
|
+
)
|
|
108
|
+
team.nickname = team_input.nickname
|
|
109
|
+
team.changed_at = observed_at
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def apply_abbreviation!(team)
|
|
113
|
+
return if team_input.abbreviation.nil?
|
|
114
|
+
return if team.abbreviation == team_input.abbreviation
|
|
115
|
+
|
|
116
|
+
change_set.record(
|
|
117
|
+
subject: team,
|
|
118
|
+
attribute: "abbreviation",
|
|
119
|
+
previous: team.abbreviation,
|
|
120
|
+
new_value: team_input.abbreviation
|
|
121
|
+
)
|
|
122
|
+
team.abbreviation = team_input.abbreviation
|
|
123
|
+
team.changed_at = observed_at
|
|
124
|
+
end
|
|
125
|
+
|
|
78
126
|
def allocate_key!(name)
|
|
79
127
|
base = Jumbotron::TeamKey.normalize(name)
|
|
80
128
|
return base unless Team.exists?(key: base)
|
|
@@ -52,6 +52,7 @@ module Jumbotron
|
|
|
52
52
|
|
|
53
53
|
def update_existing!(venue_id)
|
|
54
54
|
venue = Venue.lock.find(venue_id)
|
|
55
|
+
material = false
|
|
55
56
|
if venue.name != venue_input.name
|
|
56
57
|
change_set.record(
|
|
57
58
|
subject: venue,
|
|
@@ -60,8 +61,10 @@ module Jumbotron
|
|
|
60
61
|
new_value: venue_input.name
|
|
61
62
|
)
|
|
62
63
|
venue.name = venue_input.name
|
|
63
|
-
|
|
64
|
+
material = true
|
|
64
65
|
end
|
|
66
|
+
material |= apply_location!(venue)
|
|
67
|
+
venue.changed_at = observed_at if material
|
|
65
68
|
venue.observed_at = observed_at
|
|
66
69
|
venue.save!
|
|
67
70
|
venue
|
|
@@ -70,13 +73,42 @@ module Jumbotron
|
|
|
70
73
|
def create_new!
|
|
71
74
|
venue = Venue.create!(
|
|
72
75
|
name: venue_input.name,
|
|
76
|
+
city: venue_input.city,
|
|
77
|
+
region: venue_input.region,
|
|
73
78
|
observed_at: observed_at,
|
|
74
79
|
changed_at: observed_at
|
|
75
80
|
)
|
|
76
81
|
change_set.record(subject: venue, attribute: "name", previous: nil, new_value: venue.name)
|
|
82
|
+
if venue.city.present?
|
|
83
|
+
change_set.record(subject: venue, attribute: "city", previous: nil, new_value: venue.city)
|
|
84
|
+
end
|
|
85
|
+
if venue.region.present?
|
|
86
|
+
change_set.record(subject: venue, attribute: "region", previous: nil, new_value: venue.region)
|
|
87
|
+
end
|
|
77
88
|
venue
|
|
78
89
|
end
|
|
79
90
|
|
|
91
|
+
def apply_location!(venue)
|
|
92
|
+
material = false
|
|
93
|
+
{
|
|
94
|
+
city: venue_input.city,
|
|
95
|
+
region: venue_input.region
|
|
96
|
+
}.each do |key, value|
|
|
97
|
+
next if value.nil?
|
|
98
|
+
next if venue.public_send(key) == value
|
|
99
|
+
|
|
100
|
+
change_set.record(
|
|
101
|
+
subject: venue,
|
|
102
|
+
attribute: key.to_s,
|
|
103
|
+
previous: venue.public_send(key),
|
|
104
|
+
new_value: value
|
|
105
|
+
)
|
|
106
|
+
venue.public_send("#{key}=", value)
|
|
107
|
+
material = true
|
|
108
|
+
end
|
|
109
|
+
material
|
|
110
|
+
end
|
|
111
|
+
|
|
80
112
|
def recover_after_race!
|
|
81
113
|
ref = venue_input.provider_identities.first
|
|
82
114
|
identity = ProviderIdentity.find_by!(
|
|
@@ -61,7 +61,12 @@ module Jumbotron
|
|
|
61
61
|
def build_venue(venue)
|
|
62
62
|
return if venue.nil?
|
|
63
63
|
|
|
64
|
-
Jumbotron::Public::Venue.new(
|
|
64
|
+
Jumbotron::Public::Venue.new(
|
|
65
|
+
id: venue.id,
|
|
66
|
+
name: venue.name,
|
|
67
|
+
city: venue.city,
|
|
68
|
+
region: venue.region
|
|
69
|
+
)
|
|
65
70
|
end
|
|
66
71
|
|
|
67
72
|
def build_participant(participant)
|
|
@@ -69,14 +74,28 @@ module Jumbotron
|
|
|
69
74
|
role: participant.role,
|
|
70
75
|
score: participant.score,
|
|
71
76
|
result: participant.result,
|
|
77
|
+
record: resolve_presentation_record(participant),
|
|
72
78
|
team: Jumbotron::Public::Team.new(
|
|
73
79
|
id: participant.team.id,
|
|
74
80
|
name: participant.team.name,
|
|
75
|
-
|
|
81
|
+
nickname: participant.team.nickname,
|
|
82
|
+
key: participant.team.key,
|
|
83
|
+
abbreviation: participant.team.abbreviation
|
|
76
84
|
)
|
|
77
85
|
)
|
|
78
86
|
end
|
|
79
87
|
|
|
88
|
+
# Lifecycle-resolved game-time record for presentation. Never falls back to
|
|
89
|
+
# entering when completed post-game is missing.
|
|
90
|
+
def resolve_presentation_record(participant)
|
|
91
|
+
case game.lifecycle
|
|
92
|
+
when "scheduled", "in_progress", "postponed", "suspended"
|
|
93
|
+
participant.record_summary_entering
|
|
94
|
+
when "completed"
|
|
95
|
+
participant.record_summary_post_game
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
80
99
|
def map_currents(lines)
|
|
81
100
|
return [] unless Array(includes).include?(:current_lines)
|
|
82
101
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddGamePresentationContractFields < ActiveRecord::Migration[8.0]
|
|
4
|
+
def change
|
|
5
|
+
change_table :jumbotron_game_participants, bulk: true do |t|
|
|
6
|
+
t.string :record_summary_entering
|
|
7
|
+
t.string :record_summary_current
|
|
8
|
+
t.string :record_summary_post_game
|
|
9
|
+
t.datetime :record_observed_at
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
change_table :jumbotron_venues, bulk: true do |t|
|
|
13
|
+
t.string :city
|
|
14
|
+
t.string :region
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
add_column :jumbotron_teams, :abbreviation, :string
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/jumbotron/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jumbotron
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- matt-taylor
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: command_tower
|
|
@@ -65,6 +65,7 @@ files:
|
|
|
65
65
|
- app/adapters/jumbotron/adapters/espn/nfl_helpers/progress.rb
|
|
66
66
|
- app/adapters/jumbotron/adapters/espn/nfl_helpers/scoreboard.rb
|
|
67
67
|
- app/adapters/jumbotron/adapters/espn/nfl_helpers/season_phase.rb
|
|
68
|
+
- app/adapters/jumbotron/adapters/espn/nfl_helpers/team_nickname.rb
|
|
68
69
|
- app/adapters/jumbotron/adapters/espn/nfl_helpers/teams.rb
|
|
69
70
|
- app/adapters/jumbotron/adapters/line_acquisition.rb
|
|
70
71
|
- app/adapters/jumbotron/adapters/operation.rb
|
|
@@ -90,6 +91,7 @@ files:
|
|
|
90
91
|
- app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/side_quote.rb
|
|
91
92
|
- app/deserializers/jumbotron/deserializers/clients/espn/competition_odds/team_odds.rb
|
|
92
93
|
- app/deserializers/jumbotron/deserializers/clients/espn/competitor.rb
|
|
94
|
+
- app/deserializers/jumbotron/deserializers/clients/espn/competitor_record.rb
|
|
93
95
|
- app/deserializers/jumbotron/deserializers/clients/espn/event.rb
|
|
94
96
|
- app/deserializers/jumbotron/deserializers/clients/espn/scoreboard/get.rb
|
|
95
97
|
- app/deserializers/jumbotron/deserializers/clients/espn/season.rb
|
|
@@ -98,6 +100,7 @@ files:
|
|
|
98
100
|
- app/deserializers/jumbotron/deserializers/clients/espn/team.rb
|
|
99
101
|
- app/deserializers/jumbotron/deserializers/clients/espn/teams/get.rb
|
|
100
102
|
- app/deserializers/jumbotron/deserializers/clients/espn/venue.rb
|
|
103
|
+
- app/deserializers/jumbotron/deserializers/clients/espn/venue_address.rb
|
|
101
104
|
- app/deserializers/jumbotron/deserializers/clients/espn/week.rb
|
|
102
105
|
- app/deserializers/jumbotron/deserializers/public/game.rb
|
|
103
106
|
- app/deserializers/jumbotron/deserializers/public/game_ids.rb
|
|
@@ -206,6 +209,8 @@ files:
|
|
|
206
209
|
- db/migrate/20260814000001_create_jumbotron_line_observations.rb
|
|
207
210
|
- db/migrate/20260820000001_add_progress_to_jumbotron_games.rb
|
|
208
211
|
- db/migrate/20260827150001_add_key_to_jumbotron_teams.rb
|
|
212
|
+
- db/migrate/20260828180001_add_nickname_to_jumbotron_teams.rb
|
|
213
|
+
- db/migrate/20260828220001_add_game_presentation_contract_fields.rb
|
|
209
214
|
- docs/testing.md
|
|
210
215
|
- docs/upgrades/0.1.2.md
|
|
211
216
|
- docs/upgrades/0.1.3.md
|