Skip to content

Commit e14d986

Browse files
committed
Merge pull request #65 from Dantemss/created_before
Added published_before to the exercise search API
2 parents 6327c33 + ddac9dc commit e14d986

File tree

5 files changed

+103
-22
lines changed

5 files changed

+103
-22
lines changed

app/controllers/api/v1/exercises_controller.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ class ExercisesController < OpenStax::Api::V1::ApiController
4545
* `number` &ndash; Matches the exercise number exactly.
4646
* `version` &ndash; Matches the exercise version exactly.
4747
* `id` &ndash; Matches the exercise ID exactly.
48+
* `published_before` &ndash; Matches exercises published before the given date.
49+
Enclose date in quotes to avoid parsing errors.
4850
4951
You can also add search terms without prefixes, separated by spaces.
5052
These terms will be searched for in all of the prefix categories.
@@ -56,7 +58,7 @@ class ExercisesController < OpenStax::Api::V1::ApiController
5658
5759
`content:DTFT` &ndash; returns exercises containing the DTFT word.
5860
59-
`number:1 version:2` &ndash; returns e1v2.
61+
`number:1 version:2` &ndash; returns exercise 1@2.
6062
EOS
6163
param :order_by, String, desc: <<-EOS
6264
A string that indicates how to sort the results of the query. The string

app/models/publication.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ class Publication < ActiveRecord::Base
2525

2626
default_scope { order{[number.asc, version.desc]} }
2727

28+
scope :published, -> { where{published_at != nil} }
29+
2830
def uid
2931
"#{number}@#{version}"
3032
end

app/routines/search_exercises.rb

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,31 @@ class SearchExercises
1010
'number' => Publication.arel_table[:number],
1111
'version' => Publication.arel_table[:version],
1212
'title' => :title,
13-
'created_at' => :created_at
13+
'created_at' => :created_at,
14+
'updated_at' => :updated_at,
15+
'published_at' => Publication.arel_table[:published_at]
1416
}
1517

1618
protected
1719

1820
def exec(params = {}, options = {})
1921
params[:ob] ||= [{number: :asc}, {version: :desc}]
22+
relation = Exercise.visible_for(options[:user])
2023

21-
# By default, only return the latest exercises.
24+
# By default, only return the latest exercises visible to the user.
2225
# If either versions or uids are specified, this "latest" condition is disabled.
23-
latest_only = true
24-
run(:search, relation: Exercise.visible_for(options[:user]).preloaded,
26+
latest_scope = relation
27+
28+
run(:search, relation: relation.preloaded,
2529
sortable_fields: SORTABLE_FIELDS,
2630
params: params) do |with|
2731
with.default_keyword :content
2832

2933
with.keyword :id, :uid do |ids|
30-
latest_only = false
31-
3234
ids.each do |id|
3335
sanitized_ids = to_string_array(id).collect{|id| id.split('@')}
3436
next @items = @items.none if sanitized_ids.empty?
37+
3538
sanitized_numbers = sanitized_ids.collect{|sid| sid.first}.compact
3639
sanitized_versions = sanitized_ids.collect{|sid| sid.second}.compact
3740
if sanitized_numbers.empty?
@@ -42,31 +45,38 @@ def exec(params = {}, options = {})
4245
@items = @items.where(publication: {number: sanitized_numbers,
4346
version: sanitized_versions})
4447
end
48+
49+
# Since we are returning specific uids, disable "latest"
50+
latest_scope = nil
4551
end
4652
end
4753

4854
with.keyword :number do |numbers|
4955
numbers.each do |number|
5056
sanitized_numbers = to_string_array(numbers)
5157
next @items = @items.none if sanitized_numbers.empty?
58+
5259
@items = @items.where(publication: {number: sanitized_versions})
5360
end
5461
end
5562

5663
with.keyword :version do |versions|
57-
latest_only = false
58-
5964
versions.each do |version|
6065
sanitized_versions = to_string_array(version)
6166
next @items = @items.none if sanitized_versions.empty?
67+
6268
@items = @items.where(publication: {version: sanitized_versions})
69+
70+
# Since we are returning specific versions, disable "latest"
71+
latest_scope = nil
6372
end
6473
end
6574

6675
with.keyword :tag do |tags|
6776
tags.each do |tag|
6877
sanitized_tags = to_string_array(tag).collect{|t| t.downcase}
6978
next @items = @items.none if sanitized_tags.empty?
79+
7080
@items = @items.joins(:tags)
7181
.where(tags: {name: sanitized_tags})
7282
end
@@ -77,6 +87,7 @@ def exec(params = {}, options = {})
7787
sanitized_titles = to_string_array(title, append_wildcard: true,
7888
prepend_wildcard: true)
7989
next @items = @items.none if sanitized_titles.empty?
90+
8091
@items = @items.where{title.like_any sanitized_titles}
8192
end
8293
end
@@ -86,6 +97,7 @@ def exec(params = {}, options = {})
8697
sanitized_contents = to_string_array(content, append_wildcard: true,
8798
prepend_wildcard: true)
8899
next @items = @items.none if sanitized_contents.empty?
100+
89101
@items = @items.joins{[questions.outer.stems.outer, questions.outer.answers.outer]}
90102
.where{
91103
(title.like_any sanitized_contents) |\
@@ -102,6 +114,7 @@ def exec(params = {}, options = {})
102114
sanitized_solutions = to_string_array(solution, append_wildcard: true,
103115
prepend_wildcard: true)
104116
next @items = @items.none if sanitized_solutions.empty?
117+
105118
@items = @items.joins(:solutions)
106119
.where{(solutions.summary.like_any sanitized_solutions) |\
107120
(solutions.details.like_any sanitized_solutions)}
@@ -112,6 +125,7 @@ def exec(params = {}, options = {})
112125
names.each do |name|
113126
sn = to_string_array(name, append_wildcard: true)
114127
next @items = @items.none if sn.empty?
128+
115129
@items = @items.joins(publication: {authors: {user: :account}})
116130
.where{
117131
(publication.authors.user.account.username.like_any sn) |\
@@ -126,6 +140,7 @@ def exec(params = {}, options = {})
126140
names.each do |name|
127141
sn = to_string_array(name, append_wildcard: true)
128142
next @items = @items.none if sn.empty?
143+
129144
@items = @items.joins(publication: {copyright_holders: {user: :account}})
130145
.where{
131146
(publication.copyright_holders.user.account.username.like_any sn) |\
@@ -140,6 +155,7 @@ def exec(params = {}, options = {})
140155
names.each do |name|
141156
sn = to_string_array(name, append_wildcard: true)
142157
next @items = @items.none if sn.empty?
158+
143159
@items = @items.joins(publication: {editors: {user: :account}})
144160
.where{
145161
(publication.editors.user.account.username.like_any sn) |\
@@ -154,6 +170,7 @@ def exec(params = {}, options = {})
154170
names.each do |name|
155171
sn = to_string_array(name, append_wildcard: true)
156172
next @items = @items.none if sn.empty?
173+
157174
@items = @items.joins{publication.outer.authors.outer.user.outer.account.outer}
158175
.joins{publication.outer.copyright_holders.outer.user.outer.account.outer}
159176
.joins{publication.outer.editors.outer.user.outer.account.outer}
@@ -173,10 +190,24 @@ def exec(params = {}, options = {})
173190
}
174191
end
175192
end
193+
194+
with.keyword :published_before do |published_befores|
195+
min_published_before = published_befores.flatten.collect do |str|
196+
DateTime.parse(str) rescue nil
197+
end.compact.min
198+
next @items = @items.none if min_published_before.nil?
199+
200+
@items = @items.where{publication.published_at < min_published_before}
201+
202+
# Latest now refers to results that happened before min_published_before
203+
latest_scope = latest_scope.where{publication.published_at < min_published_before} \
204+
unless latest_scope.nil?
205+
end
176206
end
177207

178-
return unless latest_only
179-
outputs[:items] = outputs[:items].latest
208+
return if latest_scope.nil?
209+
210+
outputs[:items] = outputs[:items].latest(latest_scope)
180211
outputs[:total_count] = outputs[:items].limit(nil).offset(nil).reorder(nil).count
181212
end
182213
end

lib/publishable.rb

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,19 @@ def publishable(options = {})
4444
}
4545

4646
# http://stackoverflow.com/a/7745635
47-
scope :latest, -> {
48-
class_name = name
49-
47+
scope :latest, ->(publishable_scope = nil,
48+
publication_scope = Publication.unscoped.published) {
5049
joins(:publication).joins{
51-
Publication.unscoped
52-
.as(:same_number)
53-
.on{ (same_number.publishable_type == my{class_name}) & \
54-
(same_number.number == ~publication.number) & \
55-
(same_number.version > ~publication.version) & \
56-
(same_number.published_at != nil) }
57-
.outer
58-
}.where{same_number.id == nil}
50+
pub_rel = publication_scope.where(publishable_type: my{name})
51+
pub_rel = pub_rel.where(
52+
publishable_id: publishable_scope.limit(nil).reorder(nil).pluck(:id)
53+
) unless publishable_scope.nil?
54+
55+
pub_rel.as(:newer_publication)
56+
.on{ (newer_publication.number == ~publication.number) & \
57+
(newer_publication.version > ~publication.version) }
58+
.outer
59+
}.where{newer_publication.id == nil}
5960
}
6061

6162
after_initialize :build_publication, unless: [:persisted?, :publication]

spec/routines/search_exercises_spec.rb

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,51 @@
142142
expect(outputs.total_count).to eq 1
143143
expect(outputs.items).to eq [new_exercise_2]
144144
end
145+
146+
it 'changes the definition of "latest" if published_before is specified' do
147+
new_exercise = Exercise.new
148+
Api::V1::ExerciseRepresenter.new(new_exercise).from_json({
149+
tags: ['tag2', 'tag3'],
150+
title: "Lorem ipsum",
151+
stimulus: "Dolor",
152+
questions: [{
153+
stimulus: "Sit amet",
154+
stem_html: "Consectetur adipiscing elit",
155+
answers: [{
156+
content_html: "Sed do eiusmod tempor"
157+
}]
158+
}]
159+
}.to_json)
160+
new_exercise.publication.number = @exercise_1.publication.number
161+
new_exercise.publication.version = @exercise_1.publication.version + 1
162+
new_exercise.save!
163+
164+
result = SearchExercises.call(q: 'tag:tAg1')
165+
expect(result.errors).to be_empty
166+
167+
outputs = result.outputs
168+
expect(outputs.total_count).to eq 1
169+
expect(outputs.items).to eq [@exercise_1]
170+
171+
new_exercise.publication.publish
172+
new_exercise.publication.save!
173+
174+
result = SearchExercises.call(q: 'tag:tAg1')
175+
expect(result.errors).to be_empty
176+
177+
outputs = result.outputs
178+
expect(outputs.total_count).to eq 0
179+
expect(outputs.items).to eq []
180+
181+
result = SearchExercises.call(
182+
q: "tag:tAg1 published_before:\"#{new_exercise.published_at.as_json}\""
183+
)
184+
expect(result.errors).to be_empty
185+
186+
outputs = result.outputs
187+
expect(outputs.total_count).to eq 1
188+
expect(outputs.items).to eq [@exercise_1]
189+
end
145190
end
146191

147192
context "multiple matches" do

0 commit comments

Comments
 (0)