Skip to content

Commit f56868d

Browse files
committed
Merge pull request #74 from openstax/uid_api_changes
Draft API changes
2 parents 4d415ab + bf970d2 commit f56868d

File tree

9 files changed

+237
-69
lines changed

9 files changed

+237
-69
lines changed

app/controllers/api/v1/exercises_controller.rb

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
module Api::V1
22
class ExercisesController < OpenStax::Api::V1::ApiController
33

4-
before_filter :get_exercise, only: [:show, :update, :destroy]
4+
before_filter :get_exercise_or_create_draft, only: [:show, :update]
5+
before_filter :get_exercise, only: [:destroy]
56

67
resource_description do
78
api_versions "v1"
@@ -21,7 +22,7 @@ class ExercisesController < OpenStax::Api::V1::ApiController
2122
of the matching Exercises. Only Exercises visible to the caller will be
2223
returned. The schema for the returned JSON result is shown below.
2324
24-
#{json_schema(Api::V1::ExerciseSearchRepresenter, include: :readable)}
25+
#{json_schema(Api::V1::ExerciseSearchRepresenter, include: :readable)}
2526
EOS
2627
# Using route helpers doesn't work in test or production, probably has to do with initialization order
2728
example "#{api_example(url_base: 'https://exercises.openstax.org/api/exercises',
@@ -44,7 +45,7 @@ class ExercisesController < OpenStax::Api::V1::ApiController
4445
(uses wildcard matching)
4546
* `number` &ndash; Matches the exercise number exactly.
4647
* `version` &ndash; Matches the exercise version exactly.
47-
* `id` &ndash; Matches the exercise ID exactly.
48+
* `id` &ndash; Matches the exercise ID or UID exactly.
4849
* `published_before` &ndash; Matches exercises published before the given date.
4950
Enclose date in quotes to avoid parsing errors.
5051
@@ -67,7 +68,7 @@ class ExercisesController < OpenStax::Api::V1::ApiController
6768
The fields can be one of #{
6869
SearchExercises::SORTABLE_FIELDS.keys.collect{|sf| "`"+sf+"`"}.join(', ')
6970
}.
70-
Sort directions can either be `ASC` for
71+
Sort directions can either be `ASC` for
7172
an ascending sort, or `DESC` for a
7273
descending sort. If not provided, an ascending sort is assumed. Sort
7374
directions should be separated from the fields by a space.
@@ -78,7 +79,8 @@ class ExercisesController < OpenStax::Api::V1::ApiController
7879
`number, version DESC` &ndash; sorts by number ascending, then by version descending
7980
EOS
8081
def index
81-
standard_search(Exercise, SearchExercises, ExerciseSearchRepresenter, user: current_api_user)
82+
standard_search(Exercise, SearchExercises, ExerciseSearchRepresenter,
83+
user: current_api_user)
8284
end
8385

8486
##########
@@ -89,7 +91,7 @@ def index
8991
description <<-EOS
9092
Creates an Exercise with the given attributes.
9193
92-
#{json_schema(Api::V1::ExerciseRepresenter, include: :writeable)}
94+
#{json_schema(Api::V1::ExerciseRepresenter, include: :writeable)}
9395
EOS
9496
def create
9597
user = current_human_user
@@ -108,11 +110,11 @@ def create
108110
# show #
109111
########
110112

111-
api :GET, '/exercises/:id', 'Gets the specified Exercise'
113+
api :GET, '/exercises/:uid', 'Gets the specified Exercise'
112114
description <<-EOS
113-
Gets the Exercise that matches the provided ID.
115+
Gets the Exercise that matches the provided UID.
114116
115-
#{json_schema(Api::V1::ExerciseRepresenter, include: :readable)}
117+
#{json_schema(Api::V1::ExerciseRepresenter, include: :readable)}
116118
EOS
117119
def show
118120
standard_read(@exercise)
@@ -122,11 +124,11 @@ def show
122124
# update #
123125
##########
124126

125-
api :PUT, '/exercises/:id', 'Updates the specified Exercise'
127+
api :PUT, '/exercises/:uid', 'Updates the specified Exercise'
126128
description <<-EOS
127-
Updates the Exercise that matches the provided ID with the given attributes.
129+
Updates the Exercise that matches the provided UID with the given attributes.
128130
129-
#{json_schema(Api::V1::ExerciseRepresenter, include: :writeable)}
131+
#{json_schema(Api::V1::ExerciseRepresenter, include: :writeable)}
130132
EOS
131133
def update
132134
standard_update(@exercise)
@@ -136,9 +138,9 @@ def update
136138
# destroy #
137139
###########
138140

139-
api :DELETE, '/exercises/:id', 'Deletes the specified Exercise'
141+
api :DELETE, '/exercises/:uid', 'Deletes the specified Exercise'
140142
description <<-EOS
141-
Deletes the Exercise that matches the provided ID.
143+
Deletes the Exercise that matches the provided UID.
142144
EOS
143145
def destroy
144146
standard_destroy(@exercise)
@@ -150,6 +152,21 @@ def get_exercise
150152
@exercise = Exercise.visible_for(current_api_user).with_uid(params[:id]).first || \
151153
raise(ActiveRecord::RecordNotFound, "Couldn't find Exercise with 'uid'=#{params[:id]}")
152154
end
153-
155+
156+
def get_exercise_or_create_draft
157+
@exercise = Exercise.visible_for(current_api_user).with_uid(params[:id]).first
158+
return unless @exercise.nil?
159+
160+
@number, @version = params[:id].split('@')
161+
draft_requested = @version == 'draft' || @version == 'd'
162+
raise(ActiveRecord::RecordNotFound, "Couldn't find Exercise with 'uid'=#{params[:id]}") \
163+
unless draft_requested
164+
165+
published_exercise = Exercise.visible_for(current_api_user).with_uid(@number).first || \
166+
raise(ActiveRecord::RecordNotFound, "Couldn't find Exercise with 'uid'=#{params[:id]}")
167+
@exercise = published_exercise.new_version
168+
@exercise.save!
169+
end
170+
154171
end
155172
end

app/controllers/api/v1/publications_controller.rb

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ class PublicationsController < OpenStax::Api::V1::ApiController
1616
# publish #
1717
###########
1818

19-
api :PUT, '/object/:object_id/publish',
19+
api :PUT, '/object/:object_uid/publish',
2020
'Publishes the specified object'
2121
description <<-EOS
22-
Publishes the specified object.
22+
Publishes the specified object.
2323
EOS
2424
def publish
2525
OSU::AccessPolicy.require_action_allowed!(
@@ -32,10 +32,20 @@ def publish
3232

3333
protected
3434

35+
def get_exercise
36+
Exercise.visible_for(current_api_user).with_uid(params[:exercise_id]).first || \
37+
raise(ActiveRecord::RecordNotFound,
38+
"Couldn't find Exercise with 'uid'=#{params[:exercise_id]}")
39+
end
40+
41+
def get_solution
42+
Solution.visible_for(current_api_user).with_uid(params[:solution_id]).first || \
43+
raise(ActiveRecord::RecordNotFound,
44+
"Couldn't find Solution with 'uid'=#{params[:solution_id]}")
45+
end
46+
3547
def get_publishable
36-
@publishable = params[:solution_id].nil? ? \
37-
Exercise.find(params[:exercise_id]) : \
38-
Solution.find(params[:solution_id])
48+
@publishable = params[:solution_id].nil? ? get_exercise : get_solution
3949
end
4050

4151
end

app/controllers/api/v1/solutions_controller.rb

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
module Api::V1
22
class SolutionsController < OpenStax::Api::V1::ApiController
33

4+
before_filter :get_solution, only: [:show, :update, :destroy]
5+
46
resource_description do
57
api_versions "v1"
68
short_description 'A solution for an Exercise.'
@@ -32,7 +34,7 @@ def index
3234
# show #
3335
########
3436

35-
api :GET, '/solutions/:id', 'Gets the specified Solution'
37+
api :GET, '/solutions/:uid', 'Gets the specified Solution'
3638
description <<-EOS
3739
Shows the specified Solution, including high-level explanation and detailed explanation.
3840
@@ -41,14 +43,15 @@ def index
4143
#{json_schema(Api::V1::SolutionRepresenter, include: :readable)}
4244
EOS
4345
def show
44-
standard_read(Solution, params[:id])
46+
standard_read(@solution)
4547
end
4648

4749
##########
4850
# create #
4951
##########
5052

51-
api :POST, '/exercises/:exercise_id/solutions', 'Creates a new Solution for the given exercise'
53+
api :POST, '/exercises/:exercise_uid/solutions',
54+
'Creates a new Solution for the given exercise'
5255
description <<-EOS
5356
Creates a new Solution for the given exercise.
5457
The user is set as the author and copyright holder.
@@ -67,7 +70,7 @@ def create
6770
# update #
6871
##########
6972

70-
api :PUT, '/solutions/:id', 'Updates the properties of a Solution'
73+
api :PUT, '/solutions/:uid', 'Updates the properties of a Solution'
7174
description <<-EOS
7275
Updates the properties of the specified Solution.
7376
@@ -76,21 +79,29 @@ def create
7679
#{json_schema(Api::V1::SolutionRepresenter, include: :writeable)}
7780
EOS
7881
def update
79-
standard_update(Solution, params[:id])
82+
standard_update(@solution)
8083
end
8184

8285
###########
8386
# destroy #
8487
###########
8588

86-
api :DELETE, '/solutions/:id', 'Deletes the specified Solution'
89+
api :DELETE, '/solutions/:uid', 'Deletes the specified Solution'
8790
description <<-EOS
8891
Deletes the specified Solution.
8992
9093
The user must have permission to edit the solution.
9194
EOS
9295
def destroy
93-
standard_destroy(Solution, params[:id])
96+
standard_destroy(@solution)
97+
end
98+
99+
protected
100+
101+
def get_solution
102+
@exercise = Solution.visible_for(current_api_user).with_uid(params[:id]).first || \
103+
raise(ActiveRecord::RecordNotFound,
104+
"Couldn't find Solution with 'uid'=#{params[:id]}")
94105
end
95106

96107
end

app/models/exercise.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,35 @@
11
class Exercise < ActiveRecord::Base
22

3+
# deep_clone does not iterate through hashes, so each hash must have only 1 key
4+
NEW_VERSION_DUPED_ASSOCIATIONS = [
5+
:attachments,
6+
:logic,
7+
:tags,
8+
{
9+
publication: [
10+
:derivations,
11+
:authors,
12+
:copyright_holders,
13+
:editors
14+
]
15+
},
16+
{
17+
questions: [
18+
:hints,
19+
:answers,
20+
{
21+
stems: [
22+
:stylings,
23+
:combo_choices,
24+
{
25+
stem_answers: :answer
26+
}
27+
]
28+
}
29+
]
30+
}
31+
]
32+
333
acts_as_votable
434
parsable :stimulus
535
publishable
@@ -27,6 +57,16 @@ class Exercise < ActiveRecord::Base
2757
])
2858
}
2959

60+
def new_version
61+
nv = deep_clone include: NEW_VERSION_DUPED_ASSOCIATIONS, use_dictionary: true
62+
nv.publication.version = version + 1
63+
nv.publication.published_at = nil
64+
nv.publication.yanked_at = nil
65+
nv.publication.embargoed_until = nil
66+
nv.publication.major_change = false
67+
nv
68+
end
69+
3070
protected
3171

3272
def has_questions

app/models/publication.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def has_collaborator?(user)
5555

5656
def publish
5757
self.published_at = Time.now
58+
self
5859
end
5960

6061
protected

lib/publishable.rb

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module Base
44
def self.included(base)
55
base.extend(ClassMethods)
66
end
7-
7+
88
module ClassMethods
99
def publishable(options = {})
1010
class_exec do
@@ -37,10 +37,17 @@ def publishable(options = {})
3737

3838
scope :with_uid, ->(uid) {
3939
number, version = uid.to_s.split('@')
40-
publication_conditions = { number: number }
41-
publication_conditions[:version] = version unless version.nil?
42-
joins(:publication).where(publication: publication_conditions)
43-
.order{[publication.number.asc, publication.version.desc]}
40+
relation = joins(:publication)
41+
pub_conditions = { number: number }
42+
if version.nil?
43+
relation = relation.where{ publication.published_at != nil }
44+
elsif version == 'draft' || version == 'd'
45+
pub_conditions[:published_at] = nil
46+
else
47+
pub_conditions[:version] = version
48+
end
49+
relation.where(publication: pub_conditions)
50+
.order{[publication.number.asc, publication.version.desc]}
4451
}
4552

4653
# http://stackoverflow.com/a/7745635

0 commit comments

Comments
 (0)