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

MushroomObserver / mushroom-observer / 17026364907

17 Aug 2025 10:00PM UTC coverage: 94.957% (+0.05%) from 94.903%
17026364907

push

github

web-flow
Merge pull request #3185 from MushroomObserver/njw-list-write-in-ui

Separate Out Write-In UI from Observation List Create Workflow

242 of 250 new or added lines in 11 files covered. (96.8%)

6 existing lines in 3 files now uncovered.

35097 of 36961 relevant lines covered (94.96%)

616.09 hits per line

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

94.35
/app/controllers/application_controller/queries.rb
1
# frozen_string_literal: true
2

3
# see application_controller.rb
4
module ApplicationController::Queries
1✔
5
  def self.included(base)
1✔
6
    base.helper_method(
24,573✔
7
      :query_from_session, :passed_query, :query_params, :add_query_param,
8
      :get_query_param, :query_params_set
9
    )
10
  end
11

12
  ##############################################################################
13
  #
14
  #  :section: Queries
15
  #
16
  #  Call these public methods to create a query for results shown on an index
17
  #  action.
18
  #
19
  ##############################################################################
20

21
  # Create a new Query of the given model.
22
  # Prefer this instead of directly calling Query.lookup or PatternSearch#query.
23
  # Takes the same query_params as Query#new or Query#lookup, but this is the
24
  # only place where user content filters are applied.
25
  # (Related query links will preserve content filters in the subquery.)
26
  def create_query(model_symbol, query_params = {})
1✔
27
    add_user_content_filter_parameters(query_params, model_symbol)
771✔
28
    # NOTE: This param is used by the controller to distinguish between params
29
    # that have been filtered by User.current.content_filter vs advanced search,
30
    # because they use the same params.
31
    query_params[:preference_filter] = true if @preference_filters_applied
771✔
32
    Query.lookup(model_symbol, query_params)
771✔
33
  end
34

35
  # Lookup an appropriate Query or create a default one if necessary.  If you
36
  # pass in arguments, it modifies the query as necessary to ensure they are
37
  # correct.  (Useful for specifying sort conditions, for example.)
38
  def find_or_create_query(model_symbol, args = {})
1✔
39
    map_past_bys(args)
303✔
40
    model = model_symbol.to_s
303✔
41
    found_query = existing_updated_or_default_query(model, args)
303✔
42
    save_query_record_unless_bot(found_query)
303✔
43
    found_query
303✔
44
  end
45

46
  # Lookup the given kind of Query, returning nil if it no longer exists.
47
  def find_query(model = nil, update: !browser.bot?)
1✔
48
    model = model.to_s if model
464✔
49
    q = dealphabetize_q_param
464✔
50

51
    return nil unless (query = query_record_exists(q))
464✔
52

53
    found_query = find_new_query_for_model(model, query)
129✔
54
    save_updated_query_record(found_query) if update && found_query
129✔
55
    found_query
129✔
56
  end
57

58
  BY_MAP = { "modified" => :updated_at, "created" => :created_at }.freeze
1✔
59

60
  private ##########
1✔
61

62
  # Lookup the query and,
63
  # If it exists, return it or - if its arguments need modification -
64
  # a new query based on the existing one but with modified arguments.
65
  # If it does not exist, resturn default query.
66
  def existing_updated_or_default_query(model, args)
1✔
67
    query = find_query(model, update: false)
303✔
68
    if query
303✔
69
      # If existing query needs updates, we need to create a new query,
70
      # otherwise the modifications won't persist.
71
      # Use the existing query as the template, though.
72
      if query_needs_update?(args, query)
83✔
73
        query = create_query(model, query.params.merge(args))
12✔
74
      end
75
    # If no query found, just create a default one.
76
    else
77
      query = create_query(model, args)
220✔
78
    end
79
    query
303✔
80
  end
81

82
  def query_needs_update?(new_args, query)
1✔
83
    new_args.any? { |_arg, val| query.params[:arg] != val }
128✔
84
  end
85

86
  def query_record_exists(params)
1✔
87
    return unless params && (query = Query.safe_find(params))
499✔
88

89
    query
164✔
90
  end
91

92
  # Turn old query into a new query for given model,
93
  # (re-using the old query if it's still correct),
94
  # and returning nil if no new query can be found.
95
  def find_new_query_for_model(model, old_query)
1✔
96
    old_query_correct_for_model(model, old_query) || nil
129✔
97
  end
98

99
  def old_query_correct_for_model(model, old_query)
1✔
100
    old_query if !old_query || (old_query.model.to_s == model)
129✔
101
  end
102

103
  def save_query_record_unless_bot(query)
1✔
104
    return unless query && !browser.bot?
303✔
105

106
    save_updated_query_record(query)
303✔
107
  end
108

109
  def save_updated_query_record(query)
1✔
110
    query.increment_access_count
347✔
111
    query.save
347✔
112
  end
113

114
  def map_past_bys(args)
1✔
115
    return unless args.member?(:order_by)
303✔
116

117
    args[:order_by] = (BY_MAP[args[:order_by].to_s] || args[:order_by])
202✔
118
  end
119

120
  def add_user_content_filter_parameters(query_params, model)
1✔
121
    filters = current_user_preference_filters || {}
771✔
122
    return if filters.blank?
771✔
123

124
    # disable cop because Query::Filter is not an ActiveRecord model
125
    Query::Filter.all.each do |fltr| # rubocop:disable Rails/FindEach
16✔
126
      apply_one_content_filter(fltr, query_params, model, filters[fltr.sym])
80✔
127
    end
128
  end
129

130
  def apply_one_content_filter(fltr, query_params, model, user_filter)
1✔
131
    query_class = "Query::#{model.to_s.pluralize}".constantize
80✔
132
    key = fltr.sym
80✔
133
    return unless query_class.has_attribute?(key)
80✔
134
    return if query_params.key?(key)
80✔
135
    return unless fltr.on?(user_filter)
70✔
136

137
    query_params[key] = user_filter.to_s
13✔
138
    @preference_filters_applied = true
13✔
139
  end
140

141
  def current_user_preference_filters
1✔
142
    @user ? @user.content_filter : MO.default_content_filter
771✔
143
  end
144

145
  public ##########
1✔
146

147
  ##############################################################################
148
  #
149
  #  :section: Query parameters and session query (session[:checklist_source])
150
  #
151
  #  The general idea is that the user executes a search or requests an index,
152
  #  then clicks on a result.  This takes the user to a show_object page.  This
153
  #  page "knows" about the search or index via a special universal URL
154
  #  parameter (via +query_params+).  When the user then clicks on "prev" or
155
  #  "next", it can then step through the query results.
156
  #
157
  #  While browsing like this, the user may want to divert temporarily to add a
158
  #  comment or propose a name or something.  These actions are responsible for
159
  #  keeping track of these search parameters, and eventually passing them back
160
  #  to the show_object page.  Usually they just pass the query parameter
161
  #  through via +pass_query_params+.
162
  #
163
  #  See Query and AbstractQuery for more detail.
164
  #
165
  ##############################################################################
166

167
  # This clears the search/index saved in the session.
168
  def clear_query_in_session
1✔
169
    session[:checklist_source] = nil
255✔
170
  end
171

172
  # This stores the latest search/index used for use by create_species_list.
173
  # (Stores the Query id in <tt>session[:checklist_source]</tt>.)
174
  def store_query_in_session(query)
1✔
175
    query.save unless query.id
333✔
176
    session[:checklist_source] = query.id
333✔
177
  end
178

179
  # Get Query last stored on the "clipboard" (session).
180
  def query_from_session
1✔
UNCOV
181
    return unless (id = session[:checklist_source])
×
182

UNCOV
183
    Query.safe_find(id)
×
184
  end
185
  # helper_method :query_from_session
186

187
  # Get instance of Query which is being passed to subsequent pages.
188
  def passed_query
1✔
189
    Query.safe_find(query_params[:q].to_s.dealphabetize)
15✔
190
  end
191
  # helper_method :passed_query
192

193
  # NOTE: If we're going to cache user stuff that depends on their present q,
194
  # we'll need a helper to make the current QueryRecord (not just the id)
195
  # available to templates as an ApplicationController ivar. Something like:
196
  #
197
  # def current_query_record
198
  #   current_query = passed_query || query_from_session # could both be nil!
199
  #   current_query_record = current_query&.record || "no_query"
200
  # end
201

202
  # Return query parameter(s) necessary to pass query information along to
203
  # the next request. *NOTE*: This method is available to views.
204
  def query_params(query = nil)
1✔
205
    if browser.bot?
3,362✔
206
      {}
×
207
    elsif query
3,362✔
208
      query.save unless query.id
49✔
209
      { q: query.id.alphabetize }
49✔
210
    else
211
      @query_params || {}
3,313✔
212
    end
213
  end
214
  # helper_method :query_params
215

216
  def add_query_param(params, query = nil)
1✔
217
    return params if browser.bot?
20,144✔
218

219
    query_param = get_query_param(query)
20,074✔
220
    if params.is_a?(String) # i.e., if params is a path
20,074✔
221
      append_query_param_to_path(params, query_param)
6,773✔
222
    else
223
      params[:q] = query_param if query_param
13,301✔
224
      params
13,301✔
225
    end
226
  end
227
  # helper_method :add_query_param
228

229
  def append_query_param_to_path(path, query_param)
1✔
230
    return path unless query_param
6,773✔
231

232
    if path.include?("?") # Does path already have a query string?
2,686✔
233
      "#{path}&q=#{query_param}" # add query_param to existing query string
665✔
234
    else
235
      "#{path}?q=#{query_param}" # create a query string comprising query_param
2,021✔
236
    end
237
  end
238

239
  # Allows us to add query to a path helper:
240
  #   object_path(@object, q: get_query_param)
241
  def get_query_param(query = nil)
1✔
242
    return nil if browser.bot?
22,548✔
243

244
    if query
22,548✔
245
      query.save unless query.id
2,196✔
246
      query.id.alphabetize
2,196✔
247
    elsif @query_params
20,352✔
248
      @query_params[:q]
19,853✔
249
    end
250
  end
251
  # helper_method :get_query_param
252

253
  # NOTE: these two methods add q: param to urls built from controllers/actions.
254
  def redirect_with_query(args, query = nil)
1✔
255
    redirect_to(add_query_param(args, query))
402✔
256
  end
257

258
  def url_with_query(args, query = nil)
1✔
259
    url_for(add_query_param(args, query))
3✔
260
  end
261

262
  # Pass the in-coming query parameter(s) through to the next request.
263
  def pass_query_params
1✔
264
    @query_params = {}
1,512✔
265
    @query_params[:q] = params[:q] if params[:q].present?
1,512✔
266
    @query_params
1,512✔
267
  end
268

269
  # Change the query that +query_params+ passes along to the next request.
270
  # *NOTE*: This method is available to views.
271
  def query_params_set(query = nil)
1✔
272
    @query_params = {}
611✔
273
    if browser.bot?
611✔
274
      # do nothing
275
    elsif query
611✔
276
      query.save unless query.id
611✔
277
      @query_params[:q] = query.id.alphabetize
611✔
278
    end
279
    @query_params
611✔
280
  end
281
  # helper_method :query_params_set
282

283
  # Handle advanced_search actions with an invalid q param,
284
  # so that they get just one flash msg if the query has expired.
285
  # This method avoids a call to find_safe, which would add
286
  # "undefined method `id' for nil:NilClass" if there's no QueryRecord for q
287
  def handle_advanced_search_invalid_q_param?
1✔
288
    return false unless invalid_q_param?
14✔
289

290
    flash_error(:advanced_search_bad_q_error.t)
1✔
291
    redirect_to(search_advanced_path)
1✔
292
  end
293

294
  def dealphabetize_q_param
1✔
295
    params[:q].dealphabetize
499✔
296
  rescue StandardError
297
    nil
335✔
298
  end
299

300
  def invalid_q_param?
1✔
301
    params && params[:q] &&
14✔
302
      !QueryRecord.exists?(id: params[:q].dealphabetize)
303
  end
304

305
  # Need to pass list of tags used in this action to next page if redirecting.
306
  def redirect_to(*args)
1✔
307
    flash[:tags_on_last_page] = Language.save_tags if Language.tracking_usage?
1,645✔
308
    if args.member?(:back)
1,645✔
309
      redirect_back(fallback_location: "/")
3✔
310
    else
311
      super
1,642✔
312
    end
313
  end
314

315
  # Objects that belong to a single observation:
316
  def redirect_to_back_object_or_object(back_obj, obj)
1✔
317
    if back_obj
30✔
318
      redirect_with_query(back_obj.show_link_args)
27✔
319
    elsif obj
3✔
320
      redirect_with_query(obj.index_link_args)
3✔
321
    else
322
      redirect_with_query("/")
×
323
    end
324
  end
325

326
  # This is the common code for all the 'prev/next_object' actions.  Pass in
327
  # the current object and direction (:prev or :next), and it looks up the
328
  # query, grabs the next object, and redirects to the appropriate
329
  # 'show_object' action.
330
  #
331
  #   def next_image
332
  #     redirect_to_next_object(:next, Image, params[:id].to_s)
333
  #   end
334
  #
335
  def redirect_to_next_object(method, model, id)
1✔
336
    return unless (object = find_or_goto_index(model, id))
48✔
337

338
    next_params = find_query_and_next_object(object, method, id)
47✔
339
    object = next_params[:object]
47✔
340
    id =     next_params[:id]
47✔
341
    query =  next_params[:query]
47✔
342

343
    # Redirect to the show_object page appropriate for the new object.
344
    redirect_to(add_query_param({ controller: object.show_controller,
47✔
345
                                  action: object.show_action,
346
                                  id: id }, query))
347
  end
348

349
  def find_query_and_next_object(object, method, id)
1✔
350
    # prev/next in RssLog query
351
    query_and_next_object_rss_log_increment(object, method) ||
49✔
352
      # other cases (normal case or no next object)
353
      query_and_next_object_normal(object, method, id)
354
  end
355

356
  private ##########
1✔
357

358
  def query_and_next_object_rss_log_increment(object, method)
1✔
359
    # Special exception for prev/next in RssLog query: If go to "next" in
360
    # observations/show, for example, inside an RssLog query, go to the next
361
    # object, even if it's not an observation. If...
362
    #             ... q param is an RssLog query
363
    return unless (query = current_query_is_rss_log) &&
49✔
364
                  # ... and current rss_log exists, it's in query results,
365
                  #     and can set current index of query results from rss_log
366
                  (rss_log = results_index_settable_from_rss_log(query,
1✔
367
                                                                 object)) &&
368
                  # ... and next/prev doesn't return nil (at end)
369
                  (new_query = query.send(method)) &&
×
370
                  # ... and can get new rss_log object
371
                  (rss_log = new_query.current)
×
372

373
    { object: rss_log.target || rss_log, id: object.id, query: new_query }
×
374
  end
375

376
  # q parameter exists, a query exists for that param, and it's an rss query
377
  def current_query_is_rss_log
1✔
378
    return unless params[:q] &&
49✔
379
                  (query = query_record_exists(dealphabetize_q_param))
35✔
380

381
    query if query.model == RssLog
35✔
382
  end
383

384
  # Can we can set current index in query results based on rss_log query?
385
  def results_index_settable_from_rss_log(query, object)
1✔
386
    return unless (rss_log = rss_log_exists) &&
1✔
387
                  in_query_results(rss_log, query) &&
388
                  # ... and can set current index in query results
389
                  (query.current = object.rss_log)
×
390

391
    rss_log
×
392
  end
393

394
  def rss_log_exists
1✔
395
    object.rss_log
1✔
396
  rescue StandardError
397
    nil
1✔
398
  end
399

400
  def in_query_results(rss_log, query)
1✔
401
    query.index(rss_log)
×
402
  end
403

404
  # Normal case: attempt to coerce the current query into an appropriate
405
  # type, and go from there.  This handles all the exceptional cases:
406
  # 1) query not coercable (creates a new default one)
407
  # 2) current object missing from results of the current query
408
  # 3) no more objects being left in the query in the given direction
409
  def query_and_next_object_normal(object, method, id)
1✔
410
    query = find_or_create_query(object.class.to_s.to_sym)
49✔
411
    query.current = object
49✔
412

413
    if !query.index(object)
49✔
414
      current_object_missing_from_current_query_results(object, id, query)
2✔
415
    elsif (new_query = query.send(method))
47✔
416
      { object: object, id: new_query.current_id, query: new_query }
41✔
417
    else
418
      no_more_objects_in_given_direction(object, id, query)
6✔
419
    end
420
  end
421

422
  def current_object_missing_from_current_query_results(object, id, query)
1✔
423
    flash_error(:runtime_object_not_in_index.t(id: object.id,
2✔
424
                                               type: object.type_tag))
425
    { object: object, id: id, query: query }
2✔
426
  end
427

428
  def no_more_objects_in_given_direction(object, id, query)
1✔
429
    flash_error(:runtime_no_more_search_objects.t(type: object.type_tag))
6✔
430
    { object: object, id: id, query: query }
6✔
431
  end
432
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