• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MushroomObserver / mushroom-observer / 20596684700

30 Dec 2025 12:36PM UTC coverage: 95.261% (+0.008%) from 95.253%
20596684700

Pull #3659

github

web-flow
Merge pull request #3661 from MushroomObserver/nimmo-lightbox-caption-sync

Consolidate lightbox caption sync into lightgallery controller
Pull Request #3659: Refactor reviewed state logic and add test coverage

17 of 18 new or added lines in 4 files covered. (94.44%)

7 existing lines in 3 files now uncovered.

31440 of 33004 relevant lines covered (95.26%)

713.34 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.37
/app/components/lightbox_caption.rb
1
# frozen_string_literal: true
2

3
# Lightbox caption component for building the HTML caption shown in the
4
# lightbox.
5
#
6
# Handles two types of captions:
7
# 1. Observation captions - full details with when/where/who, notes, and naming
8
# 2. Image captions - just the image notes
9
#
10
# Both types include image links (original, EXIF) at the bottom.
11
#
12
# @example With observation
13
#   render LightboxCaption.new(
14
#     user: @user,
15
#     image: @image,
16
#     image_id: @image.id,
17
#     obs: @observation,
18
#     identify: true
19
#   )
20
#
21
# @example With image only
22
#   render LightboxCaption.new(
23
#     user: @user,
24
#     image: @image,
25
#     image_id: @image.id
26
#   )
27
class Components::LightboxCaption < Components::Base
1✔
28
  include Phlex::Rails::Helpers::LinkTo
1✔
29
  include ObservationReviewedState
1✔
30

31
  prop :user, _Nilable(User)
1✔
32
  prop :image, _Nilable(::Image), default: nil
1✔
33
  prop :image_id, _Nilable(Integer), default: nil
1✔
34
  prop :obs, _Union(Observation, Hash), default: -> { {} }
2✔
35
  prop :identify, _Boolean, default: false
1✔
36

37
  def view_template
1✔
38
    if @obs.is_a?(Observation)
1,347✔
39
      render_obs_caption_parts
541✔
40
    elsif @image&.notes.present?
806✔
41
      render_image_caption
758✔
42
    end
43

44
    render_image_links
1,347✔
45
  end
46

47
  private
1✔
48

49
  def render_obs_caption_parts
1✔
50
    render_identify_ui if @identify
541✔
51
    render_obs_title
541✔
52
    render_obs_when_where_who
541✔
53
    render_truncated_notes
541✔
54
  end
55

56
  def render_identify_ui
1✔
57
    div(class: "obs-identify mb-3", id: "observation_identify_#{@obs.id}") do
32✔
58
      propose_naming_link(
32✔
59
        @obs.id,
60
        context: "lightgallery",
61
        btn_class: "btn btn-primary d-inline-block"
62
      )
63
      span(class: "mx-2") { whitespace }
64✔
64
      render(Components::MarkAsReviewedToggle.new(
32✔
65
               obs_id: @obs.id,
66
               reviewed: observation_reviewed_state(@obs, @user)
67
             ))
68
    end
69
  end
70

71
  def render_obs_title
1✔
72
    fragment("obs_title") do
541✔
73
      LightboxObservationTitle(
541✔
74
        obs: @obs,
75
        user: @user,
76
        identify: @identify
77
      )
78
    end
79
  end
80

81
  def render_obs_when_where_who
1✔
82
    render_obs_when
541✔
83
    render_obs_where
541✔
84
    render_obs_where_gps
541✔
85
    render_obs_who
541✔
86
  end
87

88
  def render_obs_when
1✔
89
    p(class: "obs-when", id: "observation_when") do
541✔
90
      plain("#{:WHEN.t}: ")
541✔
91
      b { @obs.when.web_date }
1,082✔
92
    end
93
  end
94

95
  def render_obs_where
1✔
96
    p(class: "obs-where", id: "observation_where") do
541✔
97
      plain("#{obs_where_label}: ")
541✔
98
      render_obs_location
541✔
99
      render_vague_notice_if_needed
541✔
100
    end
101
  end
102

103
  def obs_where_label
1✔
104
    if @obs.is_collection_location
541✔
105
      :show_observation_collection_location.t
494✔
106
    else
107
      :show_observation_seen_at.t
47✔
108
    end
109
  end
110

111
  def render_obs_location
1✔
112
    if @user
541✔
113
      location_link(@obs.where, @obs.location, nil, true)
540✔
114
    else
115
      plain(@obs.where)
1✔
116
    end
117
  end
118

119
  def render_vague_notice_if_needed
1✔
120
    return unless @obs.location&.vague?
541✔
121

UNCOV
122
    title = :show_observation_vague_location.l
×
UNCOV
123
    title += " #{:show_observation_improve_location.l}" if @user == @obs.user
×
124

UNCOV
125
    whitespace
×
UNCOV
126
    p(class: "ml-3") do
×
UNCOV
127
      em { title }
×
128
    end
129
  end
130

131
  def render_obs_where_gps
1✔
132
    return unless @obs.lat && @user
541✔
133

134
    p(class: "obs-where-gps", id: "observation_where_gps") do
1✔
135
      render_gps_link if @obs.reveal_location?(@user)
1✔
136
      i { "(#{:show_observation_gps_hidden.t})" } if @obs.gps_hidden
1✔
137
    end
138
  end
139

140
  def render_gps_link
1✔
141
    link_text = [
142
      @obs.display_lat_lng.t,
1✔
143
      @obs.display_alt.t,
144
      "[#{:click_for_map.t}]"
145
    ].join(" ")
146
    a(href: map_observation_path(id: @obs.id)) { link_text }
2✔
147
  end
148

149
  def render_obs_who
1✔
150
    obs_user = @obs.user
541✔
151

152
    p(class: "obs-who", id: "observation_who") do
541✔
153
      plain("#{:WHO.t}: ")
541✔
154
      render_obs_user(obs_user)
541✔
155
      render_contact_link(obs_user) if show_contact_link?(obs_user)
541✔
156
    end
157
  end
158

159
  def render_obs_user(obs_user)
1✔
160
    if @user
541✔
161
      user_link(obs_user)
540✔
162
    else
163
      plain(obs_user.unique_text_name)
1✔
164
    end
165
  end
166

167
  def show_contact_link?(obs_user)
1✔
168
    @user && obs_user != @user && !obs_user&.no_emails &&
541✔
169
      obs_user&.email_general_question
170
  end
171

172
  def render_contact_link(_obs_user)
1✔
173
    plain(" [")
342✔
174
    modal_link_to(
342✔
175
      "observation_email",
176
      *send_observer_question_tab(@obs)
177
    )
178
    plain("]")
342✔
179
  end
180

181
  def render_truncated_notes
1✔
182
    return unless @obs.notes?
541✔
183

184
    prepare_textile_cache
245✔
185
    div(class: "obs-notes", id: "observation_#{@obs.id}_notes") do
245✔
186
      formatted_truncated_notes
245✔
187
    end
188
  end
189

190
  def prepare_textile_cache
1✔
191
    Textile.clear_textile_cache
245✔
192
    Textile.register_name(@obs.name)
245✔
193
  end
194

195
  def formatted_truncated_notes
1✔
196
    @obs.notes_show_formatted.truncate(150, separator: " ").
245✔
197
      sub(/\A/, "#{:NOTES.t}: ").wring_out_textile.tpl
198
  end
199

200
  def render_image_caption
1✔
201
    div(class: "image-notes") { @image.notes.tl.truncate_html(300) }
1,516✔
202
  end
203

204
  def render_image_links
1✔
205
    image_for_links = @image || @image_id
1,347✔
206

207
    p(class: "caption-image-links my-3") do
1,347✔
208
      render_original_image_link(image_for_links)
1,347✔
209
      plain(" | ")
1,347✔
210
      render_image_exif_link(image_for_links)
1,347✔
211
    end
212
  end
213

214
  def render_original_image_link(image_or_image_id)
1✔
215
    if image_or_image_id.is_a?(::Image)
1,347✔
216
      ImageOriginalLink(
1,343✔
217
        image: image_or_image_id,
218
        link_class: "lightbox_link"
219
      )
220
    else
221
      ImageOriginalLink(
4✔
222
        image_id: image_or_image_id,
223
        link_class: "lightbox_link"
224
      )
225
    end
226
  end
227

228
  def render_image_exif_link(image_or_image_id)
1✔
229
    image_id = if image_or_image_id.is_a?(::Image)
1,347✔
230
                 image_or_image_id.id
1,343✔
231
               else
232
                 image_or_image_id
4✔
233
               end
234

235
    ImageEXIFLink(
1,347✔
236
      image_id: image_id,
237
      link_class: "lightbox_link"
238
    )
239
  end
240
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc