Skip to content

Commit cd00879

Browse files
author
Stanley Stuart
committed
improve relationship support for Canvas::APISerializer
Adds better support for relationships with regards to ActiveModel::Serializers through Canvas::APISerializer: - Serializing a url is now the default if your relationship is `embed: :ids` (implied embed_in_root: false) - Serializing IDs is the default if your relationship is `embed: :ids, embed_in_root: true` (relationship is sideloaded with the main response) - Stringifying ids is now the default. You can opt out by defining a `stringify_ids?` method on your serializer. - Introduce Canvas::APIArraySerializer for our own needs since ActiveModel::ArraySerializer calls `serializable_object` instead of `as_json`, bypassing stringification. Also fixes an issue where subclassing Canvas::APISerializer would not get stringified if you overrode `serializable_object`. test plan: - make sure the jsonapi version of the quizzes api still works. Change-Id: I301648d3e21b887eb7c2502cc00dec023fbbf79d Reviewed-on: https://gerrit.instructure.com/28116 QA-Review: Myller de Araujo <myller@instructure.com> Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Jacob Fugal <jacob@instructure.com> Product-Review: Stanley Stuart <stanley@instructure.com>
1 parent 4a724a0 commit cd00879

9 files changed

Lines changed: 243 additions & 84 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module Canvas
2+
class APIArraySerializer < ActiveModel::ArraySerializer
3+
include Canvas::APISerialization
4+
def serializable_object
5+
super.map! { |hash| stringify!(hash) }
6+
end
7+
end
8+
end
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
module Canvas
2+
module APISerialization
3+
def stringify!(hash)
4+
return hash unless stringify_ids?
5+
Api.stringify_json_ids(hash)
6+
if (links = hash['links']).present?
7+
links.each do |key, value|
8+
links[key] = value.is_a?(Array) ? value.map(&:to_s) : value.to_s
9+
end
10+
end
11+
hash
12+
end
13+
14+
def stringify_ids?
15+
true
16+
end
17+
end
18+
end
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
module Canvas
2+
class APISerializer < ActiveModel::Serializer
3+
extend Forwardable
4+
include Canvas::APISerialization
5+
6+
attr_reader :controller, :session
7+
alias_method :user, :scope
8+
alias_method :current_user, :user
9+
def_delegators :@controller, :stringify_json_ids?, :polymorphic_url,
10+
:accepts_jsonapi?, :session, :context
11+
12+
# See ActiveModel::Serializer's documentation for options.
13+
#
14+
# object - thing to serialize, e.g. quiz, assignment
15+
# options - see AMS documentation, however, you must pass a :controller
16+
# key with a controller.
17+
#
18+
# methods available on your instance:
19+
#
20+
# session - controller's session
21+
# controller - controller
22+
# accepts_jsonapi? - if jsonapi header is present
23+
# polymorphic_url - for good ole' rails URL helpers
24+
# context - if your controller has a @context, it'll be that. Otherwise,
25+
# nil.
26+
# stringify_json_ids? - whether the stringify_json_ids? header is present
27+
# user - whatever you passed as options[:scope]
28+
# current_user - alias for user
29+
def initialize(object, options={})
30+
super(object, options)
31+
@controller = options[:controller]
32+
unless controller
33+
raise ArgumentError.new("You must pass a controller to APISerializer!")
34+
end
35+
end
36+
37+
# Overriding to allow for "links" hash.
38+
# You should probably NOT override this method in your own serializer.
39+
# This will be going away once ActiveModel::Serializer has support for
40+
# the "links" style.
41+
def associations
42+
associations = self.class._associations
43+
included_associations = filter(associations.keys)
44+
associations.each_with_object({}) do |(name, association), hash|
45+
if included_associations.include? name
46+
if association.embed_ids?
47+
hash['links'] ||= {}
48+
hash['links'][association.name] = serialize_ids association
49+
elsif association.embed_objects?
50+
hash['links'] ||= {}
51+
hash['links'][association.embedded_key] = serialize association
52+
end
53+
end
54+
end
55+
end
56+
57+
# Overriding *ONLY* to add our own stringify logic in here.
58+
# You should not override as_json in your own subclass.
59+
# Override `ActiveModel::Serializer`s serializable_object to stringify_ids
60+
# and ids in relationships if necessary.
61+
#
62+
# You can override when to stringify by implementing the "stringify_ids?"
63+
# method.
64+
def as_json(options={})
65+
root = options[:root]
66+
hash = super(options)
67+
response = root ? (hash[root] || hash) : hash
68+
response = response[self.root] || response
69+
stringify!(response)
70+
hash
71+
end
72+
# Creates a method alias for the "object" method based on the name of your
73+
# serializer. For example, if your class is `QuizSerializer`, you will
74+
# have a method named "quiz" available to your class, so you don't have to
75+
# use object if you don't want to.
76+
def self.inherited(klass)
77+
super(klass)
78+
resource_name = klass.name.underscore.downcase.split('_serializer').first
79+
klass.send(:alias_method, resource_name.to_sym, :object)
80+
end
81+
82+
private
83+
84+
# Overwrite AMS's serialize_id's function until it has support
85+
# for serializing a url for "links".
86+
# You can opt into this behavior by using `embed: :ids`
87+
#
88+
# ```ruby
89+
# has_one :assignment_group, embed: :ids
90+
# ```
91+
#
92+
# Will give you a response like:
93+
#
94+
# ```json
95+
# {
96+
# "quizzes": [
97+
# {
98+
# "id": 1,
99+
# "links": {
100+
# "assignment_group": "http://canvas.example.com/api/v1/path/to/assignment_group"
101+
# }
102+
# }
103+
# ]
104+
# }
105+
# ```
106+
#
107+
# Note that using `embed_in_root: true` will default to serializing ids, e.g.:
108+
#
109+
# ```ruby
110+
# has_one :assignment_group, embed: ids, embed_in_root: true
111+
# ```
112+
#
113+
# ```json
114+
# {
115+
# "quizzes": [
116+
# {
117+
# "id": "1",
118+
# "links": {
119+
# "assignment_group": "1"
120+
# }
121+
# }
122+
# ],
123+
# "assignment_groups": [
124+
# {
125+
# "id": "1"
126+
# }
127+
# ]
128+
# }
129+
# ```
130+
def serialize_ids(association)
131+
if association.embed_ids? && !association.embed_in_root
132+
name = association.name
133+
send("#{name}_url".to_sym) if send(name).present?
134+
else
135+
super
136+
end
137+
end
138+
end
139+
end

app/serializers/canvas_api_serializer.rb

Lines changed: 0 additions & 66 deletions
This file was deleted.

app/serializers/quiz_serializer.rb

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
require File.expand_path(File.dirname(__FILE__) + '/canvas_api_serializer')
2-
31
class QuizSerializer < Canvas::APISerializer
42
include LockedSerializer
53

@@ -14,7 +12,9 @@ class QuizSerializer < Canvas::APISerializer
1412
:lock_explanation, :hide_results, :show_correct_answers_at,
1513
:hide_correct_answers_at, :all_dates, :can_unpublish, :can_update
1614

17-
has_one :assignment_group, embed: :ids
15+
def_delegators :@controller, :api_v1_course_assignment_group_url
16+
17+
has_one :assignment_group, embed: :ids, key: :assignment_group
1818

1919
def html_url
2020
polymorphic_url([context, quiz])
@@ -30,12 +30,27 @@ def all_dates
3030

3131
def locked_for_json_type; 'quiz' end
3232

33+
def include_all_dates?
34+
quiz.grants_right?(current_user, session, :update)
35+
end
36+
37+
def include_access_code?
38+
quiz.grants_right?(current_user, session, :grade)
39+
end
40+
41+
def include_unpublishable?
42+
quiz.grants_right?(current_user, session, :manage)
43+
end
44+
3345
def filter(keys)
34-
rejected = []
35-
rejected << :all_dates unless quiz.grants_right?(current_user, session, :update)
36-
rejected << :access_code unless quiz.grants_right?(current_user, session, :grade)
37-
rejected << :unpublishable unless quiz.grants_right?(current_user, session, :manage)
38-
super(keys) - rejected
46+
super(keys).select do |key|
47+
case key
48+
when :all_dates then include_all_dates?
49+
when :access_code then include_access_code?
50+
when :unpublishable then include_unpublishable?
51+
else true
52+
end
53+
end
3954
end
4055

4156
def can_unpublish
@@ -52,10 +67,21 @@ def question_count
5267

5368
def serializable_object(options={})
5469
hash = super(options)
55-
if (accepts_jsonapi? && id = hash.delete('assignment_group_id'))
56-
hash[:links] ||= {}
57-
hash[:links][:assignment_group] = controller.send(:api_v1_course_assignment_group_url, quiz.context, id)
70+
# legacy v1 api
71+
unless accepts_jsonapi?
72+
links = hash.delete('links')
73+
id = hash['assignment_group']
74+
hash['assignment_group_id'] = quiz.assignment_group.try(:id)
5875
end
5976
hash
6077
end
78+
79+
def assignment_group_url
80+
api_v1_course_assignment_group_url(quiz.context, quiz.assignment_group.id)
81+
end
82+
83+
def stringify_ids?
84+
!!(accepts_jsonapi? || stringify_json_ids?)
85+
end
86+
6187
end

lib/api/v1/quiz.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
# You should have received a copy of the GNU Affero General Public License along
1616
# with this program. If not, see <http://www.gnu.org/licenses/>.
1717
#
18-
1918
module Api::V1::Quiz
2019
include Api::V1::Json
2120
include Api::V1::AssignmentOverride
@@ -64,7 +63,7 @@ def jsonapi_quizzes_json(options)
6463
api_route = options.fetch(:api_route)
6564
@quizzes, meta = Api.jsonapi_paginate(scope, self, api_route)
6665
meta[:primaryCollection] = 'quizzes'
67-
ActiveModel::ArraySerializer.new(@quizzes,
66+
Canvas::APIArraySerializer.new(@quizzes,
6867
scope: @current_user,
6968
controller: self,
7069
root: :quizzes,

spec/ams_spec_helper.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ def stringify_json_ids?
4444
end
4545
end
4646

47+
require File.expand_path(File.dirname(__FILE__)) + '/../app/serializers/canvas/api_serialization.rb'
48+
require File.expand_path(File.dirname(__FILE__)) + '/../app/serializers/canvas/api_serializer.rb'
49+
require File.expand_path(File.dirname(__FILE__)) + '/../app/serializers/canvas/api_array_serializer.rb'
50+
4751
Dir[File.expand_path(File.dirname(__FILE__) + '/../app/serializers/*.rb')].each do |file|
4852
require file
4953
end

0 commit comments

Comments
 (0)