openstax_accounts 4.1.1 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -1
  3. data/app/controllers/openstax/accounts/application_controller.rb +1 -1
  4. data/app/models/openstax/accounts/account.rb +11 -6
  5. data/app/models/openstax/accounts/anonymous_account.rb +1 -1
  6. data/app/models/openstax/accounts/application_account.rb +2 -1
  7. data/app/models/openstax/accounts/group.rb +14 -12
  8. data/app/models/openstax/accounts/group_member.rb +10 -10
  9. data/app/models/openstax/accounts/group_nesting.rb +7 -13
  10. data/app/models/openstax/accounts/group_owner.rb +10 -10
  11. data/app/representers/openstax/accounts/api/v1/account_representer.rb +4 -3
  12. data/app/representers/openstax/accounts/api/v1/application_account_representer.rb +7 -2
  13. data/app/representers/openstax/accounts/api/v1/application_accounts_representer.rb +0 -2
  14. data/app/representers/openstax/accounts/api/v1/application_group_representer.rb +7 -2
  15. data/app/representers/openstax/accounts/api/v1/application_groups_representer.rb +0 -2
  16. data/app/representers/openstax/accounts/api/v1/group_nesting_representer.rb +1 -1
  17. data/app/representers/openstax/accounts/api/v1/group_representer.rb +1 -1
  18. data/app/representers/openstax/accounts/api/v1/group_user_representer.rb +1 -1
  19. data/app/routines/openstax/accounts/search_accounts.rb +1 -1
  20. data/app/routines/openstax/accounts/sync_accounts.rb +24 -27
  21. data/app/routines/openstax/accounts/sync_groups.rb +22 -26
  22. data/lib/openstax/accounts/action_controller/base.rb +61 -0
  23. data/lib/openstax/accounts/api.rb +270 -0
  24. data/lib/openstax/accounts/configuration.rb +77 -0
  25. data/lib/openstax/accounts/engine.rb +13 -11
  26. data/lib/openstax/accounts/has_many_through_groups/active_record/base.rb +51 -0
  27. data/lib/openstax/accounts/version.rb +1 -1
  28. data/lib/openstax_accounts.rb +8 -331
  29. data/spec/dummy/app/controllers/api/users_controller.rb +4 -0
  30. data/spec/dummy/config/routes.rb +3 -1
  31. data/spec/dummy/log/test.log +152307 -0
  32. data/spec/lib/openstax/accounts/api_spec.rb +232 -0
  33. data/spec/lib/openstax/accounts/has_many_through_groups/active_record/base_spec.rb +57 -0
  34. data/spec/routines/openstax/accounts/sync_accounts_spec.rb +5 -9
  35. data/spec/routines/openstax/accounts/sync_groups_spec.rb +38 -39
  36. metadata +27 -16
  37. data/lib/openstax/accounts/extend_builtins.rb +0 -50
  38. data/lib/openstax/accounts/has_many_through_groups.rb +0 -47
  39. data/spec/lib/openstax/accounts/has_many_through_groups_spec.rb +0 -53
  40. data/spec/lib/openstax_accounts_spec.rb +0 -210
@@ -17,42 +17,38 @@ module OpenStax
17
17
 
18
18
  def exec(options={})
19
19
 
20
- begin
21
- OpenStax::Accounts.syncing = true
20
+ return if OpenStax::Accounts.configuration.enable_stubbing?
22
21
 
23
- return if OpenStax::Accounts.configuration.enable_stubbing?
22
+ response = OpenStax::Accounts::Api.get_application_group_updates
24
23
 
25
- response = OpenStax::Accounts.get_application_group_updates
24
+ app_groups = []
25
+ app_groups_rep = OpenStax::Accounts::Api::V1::ApplicationGroupsRepresenter
26
+ .new(app_groups)
27
+ app_groups_rep.from_json(response.body)
26
28
 
27
- app_groups = []
28
- app_groups_rep = OpenStax::Accounts::Api::V1::ApplicationGroupsRepresenter
29
- .new(app_groups)
30
- app_groups_rep.from_json(response.body)
29
+ return if app_groups.empty?
31
30
 
32
- return if app_groups.empty?
31
+ updated_app_groups = []
32
+ app_groups.each do |app_group|
33
+ group = OpenStax::Accounts::Group.where(
34
+ :openstax_uid => app_group.group.openstax_uid
35
+ ).first || app_group.group
36
+ group.syncing = true
33
37
 
34
- updated_app_groups = []
35
- app_groups.each do |app_group|
36
- group = OpenStax::Accounts::Group.where(
37
- :openstax_uid => app_group.group.openstax_uid).first || app_group.group
38
-
39
- if group != app_group.group
40
- SYNC_ATTRIBUTES.each do |attribute|
41
- group.send("#{attribute}=", app_group.group.send(attribute))
42
- end
38
+ if group != app_group.group
39
+ SYNC_ATTRIBUTES.each do |attribute|
40
+ group.send("#{attribute}=", app_group.group.send(attribute))
43
41
  end
44
-
45
- next unless group.save
46
-
47
- updated_app_groups << {group_id: group.openstax_uid,
48
- read_updates: app_group.unread_updates}
49
42
  end
50
43
 
51
- OpenStax::Accounts.mark_group_updates_as_read(updated_app_groups)
52
- ensure
53
- OpenStax::Accounts.syncing = false
44
+ next unless group.save
45
+
46
+ updated_app_groups << {group_id: group.openstax_uid,
47
+ read_updates: app_group.unread_updates}
54
48
  end
55
49
 
50
+ OpenStax::Accounts::Api.mark_group_updates_as_read(updated_app_groups)
51
+
56
52
  end
57
53
 
58
54
  end
@@ -0,0 +1,61 @@
1
+ module OpenStax
2
+ module Accounts
3
+ module ActionController
4
+ module Base
5
+
6
+ def self.included(base)
7
+ base.helper_method :current_user, :signed_in?
8
+ end
9
+
10
+ # Returns the current user
11
+ def current_user
12
+ current_user_manager.current_user
13
+ end
14
+
15
+ # Returns the current account
16
+ def current_account
17
+ current_user_manager.current_account
18
+ end
19
+
20
+ # Returns true iff there is a user signed in
21
+ def signed_in?
22
+ current_user_manager.signed_in?
23
+ end
24
+
25
+ # Signs in the given account or user
26
+ def sign_in(user)
27
+ current_user_manager.sign_in(user)
28
+ end
29
+
30
+ # Signs out the current account and user
31
+ def sign_out!
32
+ current_user_manager.sign_out!
33
+ end
34
+
35
+ protected
36
+
37
+ def current_user_manager
38
+ @current_user_manager ||= \
39
+ OpenStax::Accounts::CurrentUserManager.new(request, session, cookies)
40
+ end
41
+
42
+ def authenticate_user!
43
+ account = current_account
44
+
45
+ return if account && !account.is_anonymous?
46
+
47
+ store_url key: :accounts_return_to, strategies: [:session]
48
+
49
+ respond_to do |format|
50
+ format.html { redirect_to openstax_accounts.login_url }
51
+ format.json { head(:forbidden) }
52
+ end
53
+ end
54
+
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ ::ActionController::Base.send :include,
61
+ OpenStax::Accounts::ActionController::Base
@@ -0,0 +1,270 @@
1
+ module OpenStax
2
+ module Accounts
3
+ module Api
4
+
5
+ DEFAULT_API_VERSION = :v1
6
+
7
+ # Executes an OpenStax Accounts API request,
8
+ # using the given HTTP method, API path and request options.
9
+ # Any options accepted by OAuth2 requests can be used, such as
10
+ # :params, :body, :headers, etc, plus the :access_token option, which can
11
+ # be used to manually specify an OAuth access token.
12
+ # On failure, it can throw Faraday::ConnectionFailed for connection errors
13
+ # or OAuth2::Error if Accounts returns an HTTP 400 error,
14
+ # such as 422 Unprocessable Entity.
15
+ # On success, returns an OAuth2::Response object.
16
+ def self.request(http_method, path, options = {})
17
+ version = options.delete(:api_version) || DEFAULT_API_VERSION
18
+ unless version.blank?
19
+ options[:headers] ||= {}
20
+ options[:headers].merge!({
21
+ 'Accept' => "application/vnd.accounts.openstax.#{version.to_s}"
22
+ })
23
+ end
24
+
25
+ token_string = options.delete(:access_token)
26
+ token = token_string.blank? ? client.client_credentials.get_token :
27
+ OAuth2::AccessToken.new(client, token_string)
28
+
29
+ accounts_url = OpenStax::Accounts.configuration.openstax_accounts_url
30
+ url = URI.join(accounts_url, 'api/', path)
31
+
32
+ token.request(http_method, url, options)
33
+ end
34
+
35
+ # Performs an API request using the given account's access token
36
+ def self.request_for_account(account, http_method, path, options = {})
37
+ request(http_method, path,
38
+ options.merge(access_token: account.access_token))
39
+ end
40
+
41
+ # Performs an account search in the Accounts server.
42
+ # Results are limited to 10 accounts maximum.
43
+ # Takes a query parameter and an options hash.
44
+ # On failure, throws an Exception, just like the request method.
45
+ # On success, returns an OAuth2::Response object.
46
+ def self.search_accounts(query, options = {})
47
+ request(:get, 'users', options.merge(params: {:q => query}))
48
+ end
49
+
50
+ # Updates a user account in the Accounts server.
51
+ # The account is determined by the OAuth access token.
52
+ # Also takes an options hash.
53
+ # On failure, throws an Exception, just like the request method.
54
+ # On success, returns an OAuth2::Response object.
55
+ def self.update_account(account, options = {})
56
+ request_for_account(
57
+ account, :put, 'user', options.merge(
58
+ body: account.attributes.slice('username', 'first_name',
59
+ 'last_name', 'full_name',
60
+ 'title').to_json
61
+ )
62
+ )
63
+ end
64
+
65
+ # Performs an account search in the Accounts server.
66
+ # Results are limited to accounts that have used the current app.
67
+ # Takes a query parameter and an options hash.
68
+ # On failure, throws an Exception, just like the request method.
69
+ # On success, returns an OAuth2::Response object.
70
+ def self.search_application_accounts(query, options = {})
71
+ request(:get, 'application_users', options.merge(params: {:q => query}))
72
+ end
73
+
74
+ # Retrieves information about accounts that have been
75
+ # recently updated.
76
+ # Results are limited to accounts that have used the current app.
77
+ # Takes an options hash.
78
+ # On failure, throws an Exception, just like the request method.
79
+ # On success, returns an OAuth2::Response object.
80
+ def self.get_application_account_updates(options = {})
81
+ request(:get, 'application_users/updates', options)
82
+ end
83
+
84
+ # Marks account updates as "read".
85
+ # The application_users parameter is an array of hashes.
86
+ # Each hash has 2 required fields: 'id', which should contain the
87
+ # application_user's id, and 'read_updates', which should contain
88
+ # the last received value of unread_updates for that application_user.
89
+ # Can only be called for application_users that belong to the current app.
90
+ # Also takes an options hash.
91
+ # On failure, throws an Exception, just like the request method.
92
+ # On success, returns an OAuth2::Response object.
93
+ def self.mark_account_updates_as_read(application_users, options = {})
94
+ request(:put, 'application_users/updated', options.merge(
95
+ body: application_users.to_json
96
+ ))
97
+ end
98
+
99
+ # Retrieves information about groups that have been
100
+ # recently updated.
101
+ # Results are limited to groups that users of the current app
102
+ # have access to.
103
+ # Takes an options hash.
104
+ # On failure, throws an Exception, just like the request method.
105
+ # On success, returns an OAuth2::Response object.
106
+ def self.get_application_group_updates(options = {})
107
+ request(:get, 'application_groups/updates', options)
108
+ end
109
+
110
+ # Marks group updates as "read".
111
+ # The application_groups parameter is an array of hashes.
112
+ # Each hash has 2 required fields: 'id', which should contain the
113
+ # application_group's id, and 'read_updates', which should contain
114
+ # the last received value of unread_updates for that application_group.
115
+ # Can only be called for application_groups that belong to the current app.
116
+ # Also takes an options hash.
117
+ # On failure, throws an Exception, just like the request method.
118
+ # On success, returns an OAuth2::Response object.
119
+ def self.mark_group_updates_as_read(application_groups, options = {})
120
+ request(:put, 'application_groups/updated', options.merge(
121
+ body: application_groups.to_json
122
+ ))
123
+ end
124
+
125
+ # Creates a group in the Accounts server.
126
+ # The given account will be the owner of the group.
127
+ # Also takes an options hash.
128
+ # On failure, throws an Exception, just like the request method.
129
+ # On success, returns a hash containing the group attributes
130
+ def self.create_group(account, group, options = {})
131
+ response = ActiveSupport::JSON.decode(
132
+ request_for_account(account, :post, 'groups', options.merge(
133
+ body: group.attributes.slice('name', 'is_public').to_json
134
+ )).body
135
+ )
136
+ group.openstax_uid = response['id']
137
+ response
138
+ end
139
+
140
+ # Updates a group in the Accounts server.
141
+ # The given account must own the group.
142
+ # Also takes an options hash.
143
+ # On failure, throws an Exception, just like the request method.
144
+ # On success, returns an OAuth2::Response object.
145
+ def self.update_group(account, group, options = {})
146
+ request_for_account(account, :put, "groups/#{group.openstax_uid}",
147
+ options.merge(
148
+ body: group.attributes.slice('name', 'is_public').to_json
149
+ )
150
+ )
151
+ end
152
+
153
+ # Deletes a group from the Accounts server.
154
+ # The given account must own the group.
155
+ # Also takes an options hash.
156
+ # On failure, throws an Exception, just like the request method.
157
+ # On success, returns an OAuth2::Response object.
158
+ def self.destroy_group(account, group, options = {})
159
+ request_for_account(account, :delete,
160
+ "groups/#{group.openstax_uid}", options)
161
+ end
162
+
163
+ # Creates a group_member in the Accounts server.
164
+ # The given account must own the group.
165
+ # Also takes an options hash.
166
+ # On failure, throws an Exception, just like the request method.
167
+ # On success, returns an OAuth2::Response object.
168
+ def self.create_group_member(account, group_member, options = {})
169
+ request_for_account(
170
+ account,
171
+ :post,
172
+ "groups/#{group_member.group_id}/members/#{group_member.user_id}",
173
+ options
174
+ )
175
+ end
176
+
177
+ # Deletes a group_member from the Accounts server.
178
+ # The given account must own the group.
179
+ # Also takes an options hash.
180
+ # On failure, throws an Exception, just like the request method.
181
+ # On success, returns an OAuth2::Response object.
182
+ def self.destroy_group_member(account, group_member, options = {})
183
+ request_for_account(
184
+ account,
185
+ :delete,
186
+ "groups/#{group_member.group_id}/members/#{group_member.user_id}",
187
+ options
188
+ )
189
+ end
190
+
191
+ # Creates a group_owner in the Accounts server.
192
+ # The given account must own the group.
193
+ # Also takes an options hash.
194
+ # On failure, throws an Exception, just like the request method.
195
+ # On success, returns an OAuth2::Response object.
196
+ def self.create_group_owner(account, group_owner, options = {})
197
+ request_for_account(
198
+ account,
199
+ :post,
200
+ "groups/#{group_owner.group_id}/owners/#{group_owner.user_id}",
201
+ options
202
+ )
203
+ end
204
+
205
+ # Deletes a group_owner from the Accounts server.
206
+ # The given account must own the group.
207
+ # Also takes an options hash.
208
+ # On failure, throws an Exception, just like the request method.
209
+ # On success, returns an OAuth2::Response object.
210
+ def self.destroy_group_owner(account, group_owner, options = {})
211
+ request_for_account(
212
+ account,
213
+ :delete,
214
+ "groups/#{group_owner.group_id}/owners/#{group_owner.user_id}",
215
+ options
216
+ )
217
+ end
218
+
219
+ # Creates a group_nesting in the Accounts server.
220
+ # The given account must own both groups.
221
+ # Also takes an an options hash.
222
+ # On failure, throws an Exception, just like the request method.
223
+ # On success, returns an OAuth2::Response object.
224
+ def self.create_group_nesting(account, group_nesting, options = {})
225
+ request_for_account(
226
+ account,
227
+ :post,
228
+ "groups/#{group_nesting.container_group_id}/nestings/#{
229
+ group_nesting.member_group_id}",
230
+ options)
231
+ end
232
+
233
+ # Deletes a group_nesting from the Accounts server.
234
+ # The given account must own either group.
235
+ # Also takes an options hash.
236
+ # On failure, throws an Exception, just like the request method.
237
+ # On success, returns an OAuth2::Response object.
238
+ def self.destroy_group_nesting(account, group_nesting, options = {})
239
+ request_for_account(
240
+ account,
241
+ :delete,
242
+ "groups/#{group_nesting.container_group_id}/nestings/#{
243
+ group_nesting.member_group_id}",
244
+ options
245
+ )
246
+ end
247
+
248
+ # Creates a temporary user in Accounts.
249
+ # Also takes an options hash.
250
+ # On failure, throws an Exception, just like the request method.
251
+ # On success, returns an OAuth2::Response object.
252
+ def self.create_temp_account(attributes, options = {})
253
+ request(:post, "user/find-or-create", options.merge(
254
+ body: attributes.to_json
255
+ ))
256
+ end
257
+
258
+ protected
259
+
260
+ def self.client
261
+ @client ||= OAuth2::Client.new(
262
+ OpenStax::Accounts.configuration.openstax_application_id,
263
+ OpenStax::Accounts.configuration.openstax_application_secret,
264
+ :site => OpenStax::Accounts.configuration.openstax_accounts_url
265
+ )
266
+ end
267
+
268
+ end
269
+ end
270
+ end
@@ -0,0 +1,77 @@
1
+ module OpenStax
2
+ module Accounts
3
+ class Configuration
4
+ # openstax_accounts_url
5
+ # Base URL for OpenStax Accounts
6
+ attr_reader :openstax_accounts_url
7
+
8
+ # openstax_application_id
9
+ # OAuth client_id received from OpenStax Accounts
10
+ attr_accessor :openstax_application_id
11
+
12
+ # openstax_application_secret
13
+ # OAuth client_secret received from OpenStax Accounts
14
+ attr_accessor :openstax_application_secret
15
+
16
+ # enable_stubbing
17
+ # Set to true if you want this engine to fake all
18
+ # interaction with the accounts site.
19
+ attr_accessor :enable_stubbing
20
+
21
+ # logout_via
22
+ # HTTP method to accept for logout requests
23
+ attr_accessor :logout_via
24
+
25
+ attr_accessor :default_errors_partial
26
+ attr_accessor :default_errors_html_id
27
+ attr_accessor :default_errors_added_trigger
28
+
29
+ # security_transgression_exception
30
+ # Class to be used for security transgression exceptions
31
+ attr_accessor :security_transgression_exception
32
+
33
+ # account_user_mapper
34
+ # This class teaches the gem how to convert between accounts and users
35
+ # See the "account_user_mapper" discussion in the README
36
+ attr_accessor :account_user_mapper
37
+
38
+ # min_search_characters
39
+ # The minimum number of characters that can be used
40
+ # as a query in a call to the AccountsSearch handler
41
+ # If less are used, the handler will return an error instead
42
+ attr_accessor :min_search_characters
43
+
44
+ # max_search_items
45
+ # The maximum number of accounts that can be returned
46
+ # in a call to the AccountsSearch handler
47
+ # If more would be returned, the result will be empty instead
48
+ attr_accessor :max_search_items
49
+
50
+ def openstax_accounts_url=(url)
51
+ url.gsub!(/https|http/,'https') if !(url =~ /localhost/)
52
+ url = url + "/" if url[url.size-1] != '/'
53
+ @openstax_accounts_url = url
54
+ end
55
+
56
+ def initialize
57
+ @openstax_application_id = 'SET ME!'
58
+ @openstax_application_secret = 'SET ME!'
59
+ @openstax_accounts_url = 'https://accounts.openstax.org/'
60
+ @enable_stubbing = true
61
+ @logout_via = :get
62
+ @default_errors_partial = 'openstax/accounts/shared/attention'
63
+ @default_errors_html_id = 'openstax-accounts-attention'
64
+ @default_errors_added_trigger = 'openstax-accounts-errors-added'
65
+ @security_transgression_exception = SecurityTransgression
66
+ @account_user_mapper = OpenStax::Accounts::DefaultAccountUserMapper
67
+ @min_search_characters = 3
68
+ @max_search_items = 10
69
+ super
70
+ end
71
+
72
+ def enable_stubbing?
73
+ !Rails.env.production? && enable_stubbing
74
+ end
75
+ end
76
+ end
77
+ end