openstax_accounts 0.3.0 → 1.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 (49) hide show
  1. checksums.yaml +15 -0
  2. data/README.md +20 -0
  3. data/app/controllers/openstax/accounts/dev/base_controller.rb +19 -0
  4. data/app/controllers/openstax/accounts/dev/users_controller.rb +10 -9
  5. data/app/handlers/openstax/accounts/dev/users_index.rb +39 -0
  6. data/app/handlers/openstax/accounts/sessions_omniauth_authenticated.rb +2 -1
  7. data/app/models/openstax/accounts/application_user.rb +7 -0
  8. data/app/models/openstax/accounts/user.rb +15 -3
  9. data/app/representers/openstax/accounts/api/v1/application_user_representer.rb +5 -1
  10. data/app/representers/openstax/accounts/api/v1/application_user_search_representer.rb +19 -0
  11. data/app/representers/openstax/accounts/api/v1/application_users_representer.rb +16 -0
  12. data/app/representers/openstax/accounts/api/v1/user_representer.rb +2 -6
  13. data/app/routines/openstax/accounts/dev/create_user.rb +4 -4
  14. data/app/routines/openstax/accounts/dev/search_users.rb +27 -0
  15. data/app/routines/openstax/accounts/search_users.rb +174 -32
  16. data/app/routines/openstax/accounts/sync_users.rb +44 -0
  17. data/app/views/openstax/accounts/dev/users/_search_results.html.erb +50 -0
  18. data/app/views/openstax/accounts/dev/users/index.js.erb +3 -0
  19. data/app/views/openstax/accounts/dev/users/login.html.erb +6 -5
  20. data/app/views/openstax/accounts/shared/_attention.html.erb +1 -1
  21. data/app/views/openstax/accounts/shared/users/_index.html.erb +24 -0
  22. data/config/routes.rb +5 -4
  23. data/lib/generators/openstax/accounts/schedule/USAGE +8 -0
  24. data/lib/generators/openstax/accounts/schedule/schedule_generator.rb +18 -0
  25. data/lib/generators/openstax/accounts/schedule/templates/schedule.rb +3 -0
  26. data/lib/omniauth/strategies/openstax.rb +1 -1
  27. data/lib/openstax/accounts/engine.rb +2 -0
  28. data/lib/openstax/accounts/version.rb +1 -1
  29. data/lib/openstax_accounts.rb +69 -26
  30. data/spec/controllers/openstax/accounts/dev/users_controller_spec.rb +4 -3
  31. data/spec/dummy/app/controllers/api/application_users_controller.rb +12 -0
  32. data/spec/dummy/config/application.rb +3 -0
  33. data/spec/dummy/config/initializers/openstax_accounts.rb +1 -0
  34. data/spec/dummy/config/routes.rb +5 -4
  35. data/spec/lib/openstax_accounts_spec.rb +19 -11
  36. data/spec/routines/openstax/accounts/dev/create_user_spec.rb +26 -0
  37. data/spec/routines/openstax/accounts/search_users_spec.rb +129 -0
  38. metadata +47 -68
  39. data/app/controllers/openstax/accounts/dev/dev_controller.rb +0 -13
  40. data/app/handlers/openstax/accounts/dev/users_search.rb +0 -38
  41. data/app/representers/openstax/accounts/api/v1/contact_info_representer.rb +0 -23
  42. data/app/representers/openstax/accounts/api/v1/email_address_representer.rb +0 -9
  43. data/app/views/openstax/accounts/dev/users/search.js.erb +0 -21
  44. data/app/views/openstax/accounts/users/_action_create_form.html.erb +0 -9
  45. data/app/views/openstax/accounts/users/_action_dialog.html.erb +0 -10
  46. data/app/views/openstax/accounts/users/_action_list.html.erb +0 -33
  47. data/app/views/openstax/accounts/users/_action_search.html.erb +0 -25
  48. data/app/views/openstax/accounts/users/action_search.js.erb +0 -8
  49. data/spec/dummy/app/controllers/api/users_controller.rb +0 -7
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ Mjc2YjE2MDk2NTlkODU2NWVkNjJhYjAzZjY2MzRiZmMxOTRmZDNkZg==
5
+ data.tar.gz: !binary |-
6
+ NzZmMzllNDRiODQ1MWVhMmRjMWNmYWYxYzc4ODUzZjNiMjkyZWFlNA==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ YzgxYzZkMjI5NDc5NTk3ZjQyOGFjMDVjYThiNDg4NDY2MTM1MWRlZTI4YTQ2
10
+ NDE2MzVkNDc4YzI1ODNkMDQ3ZWU0YjFkNmQxZmY5YWU2YTQzM2VmZjI4Zjk0
11
+ MWI1MzkyMzQ0NzA1MmQxMGZkYjFiZmRlOGI4NDczYzc5ZjVlMjc=
12
+ data.tar.gz: !binary |-
13
+ ODRkZDMzMDEwNGQwNTRhYjU5NzkwODU5YzljZGE4YzhiZWUwNDkzMzkwMWMx
14
+ ZmEwMmNlYTUzZDVjY2VhYzg5MWJiNzJkZjcwYjQ3MjVmZjE0ZTAwOGJjOWRl
15
+ MTE5ODIxN2NjYTY1MzFiZjI5NjNjMjE4ZTIwNTBlNzI5N2RlNDM=
data/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  accounts-rails
2
2
  =============
3
3
 
4
+ [![Gem Version](https://badge.fury.io/rb/openstax_accounts.svg)](http://badge.fury.io/rb/openstax_accounts)
5
+ [![Build Status](https://travis-ci.org/openstax/accounts-rails.svg?branch=master)](https://travis-ci.org/openstax/accounts-rails)
6
+
4
7
  A rails engine for interfacing with OpenStax's accounts server.
5
8
 
6
9
  Usage
@@ -75,6 +78,23 @@ Make sure to install the engine's migrations:
75
78
 
76
79
  rake openstax_accounts:install:migrations
77
80
 
81
+ Syncing with Accounts
82
+ ---------------------
83
+
84
+ OpenStax::Accounts requires your app to periodically sync user information with the Accounts server. The easiest way to do this is to use the "whenever" gem.
85
+
86
+ To create or append to the schedule.rb file, run the following command:
87
+
88
+ ```sh
89
+ rails g openstax:accounts:schedule
90
+ ```
91
+
92
+ Then, after installing the "whenever" gem, run the `whenever` command for instructions to set up your crontab:
93
+
94
+ ```sh
95
+ whenever
96
+ ```
97
+
78
98
  Accounts API
79
99
  ------------
80
100
 
@@ -0,0 +1,19 @@
1
+ module OpenStax
2
+ module Accounts
3
+ module Dev
4
+ class BaseController < OpenStax::Accounts::ApplicationController
5
+
6
+ before_filter Proc.new{
7
+ raise SecurityTransgression if Rails.env.production?
8
+ }
9
+
10
+ skip_before_filter :authenticate_user!
11
+ skip_before_filter :require_registration!
12
+
13
+ fine_print_skip_signatures :general_terms_of_use, :privacy_policy \
14
+ if respond_to?(:fine_print_skip_signatures)
15
+
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,21 +1,22 @@
1
1
  module OpenStax
2
2
  module Accounts
3
3
  module Dev
4
- class UsersController < OpenStax::Accounts::Dev::DevController
5
-
6
- def login; end
4
+ class UsersController < OpenStax::Accounts::Dev::BaseController
7
5
 
8
- def search
9
- handle_with(OpenStax::Accounts::Dev::UsersSearch,
10
- complete: lambda { render 'search' })
6
+ def index
7
+ handle_with(UsersIndex,
8
+ complete: lambda { render 'index' })
11
9
  end
12
10
 
11
+ def login; end
12
+
13
13
  def become
14
- sign_in(User.find(params[:user_id]))
15
- redirect_to return_url(true)
14
+ @user = User.find(params[:id])
15
+ sign_in(@user)
16
+ redirect_to return_url
16
17
  end
17
18
 
18
19
  end
19
20
  end
20
21
  end
21
- end
22
+ end
@@ -0,0 +1,39 @@
1
+ module OpenStax
2
+ module Accounts
3
+ module Dev
4
+ class UsersIndex
5
+
6
+ lev_handler transaction: :no_transaction
7
+
8
+ paramify :search do
9
+ attribute :terms, type: String
10
+ attribute :type, type: String
11
+ attribute :page, type: Integer
12
+ end
13
+
14
+ uses_routine OpenStax::Accounts::Dev::SearchUsers,
15
+ as: :search_users,
16
+ translations: { outputs: {type: :verbatim} }
17
+
18
+ protected
19
+
20
+ def authorized?
21
+ !Rails.env.production?
22
+ end
23
+
24
+ def handle
25
+ case search_params.type
26
+ when 'Name'
27
+ query = "name:#{search_params.terms.gsub(/\s/,',')}"
28
+ when 'Username'
29
+ query = "username:#{search_params.terms.gsub(/\s/,',')}"
30
+ else
31
+ query = search_params.terms || ''
32
+ end
33
+ run(:search_users, query, page: search_params.page || 0)
34
+ end
35
+
36
+ end
37
+ end
38
+ end
39
+ end
@@ -2,9 +2,10 @@ module OpenStax
2
2
  module Accounts
3
3
 
4
4
  class SessionsOmniauthAuthenticated
5
+
5
6
  lev_handler
6
7
 
7
- protected
8
+ protected
8
9
 
9
10
  def setup
10
11
  @auth_data = request.env['omniauth.auth']
@@ -0,0 +1,7 @@
1
+ module OpenStax
2
+ module Accounts
3
+ class ApplicationUser
4
+ attr_accessor :id, :application_id, :user, :unread_updates, :default_contact_info_id
5
+ end
6
+ end
7
+ end
@@ -2,11 +2,17 @@ module OpenStax
2
2
  module Accounts
3
3
  class User < ActiveRecord::Base
4
4
 
5
- validates :username, uniqueness: true
6
- validates :username, presence: true
5
+ USERNAME_DISCARDED_CHAR_REGEX = /[^A-Za-z\d_]/
6
+ USERNAME_MAX_LENGTH = 50
7
+
8
+ attr_accessor :updating_from_accounts
9
+
10
+ validates :username, uniqueness: true, presence: true
7
11
  validates :openstax_uid, presence: true
8
12
 
9
- # first and last names are not required
13
+ attr_accessible :username, :first_name, :last_name, :full_name, :title
14
+
15
+ before_update :update_openstax_accounts
10
16
 
11
17
  def name
12
18
  (first_name || last_name) ? [first_name, last_name].compact.join(" ") : username
@@ -26,6 +32,12 @@ module OpenStax
26
32
  @@anonymous ||= AnonymousUser.new
27
33
  end
28
34
 
35
+ def update_openstax_accounts
36
+ return if updating_from_accounts || \
37
+ OpenStax::Accounts.configuration.enable_stubbing?
38
+ OpenStax::Accounts.user_update(self)
39
+ end
40
+
29
41
  class AnonymousUser < User
30
42
  before_save { false }
31
43
  def initialize(attributes=nil)
@@ -11,7 +11,11 @@ module OpenStax
11
11
  property :application_id,
12
12
  type: Integer
13
13
 
14
- property :user_id,
14
+ property :user,
15
+ class: OpenStax::Accounts::User,
16
+ decorator: UserRepresenter
17
+
18
+ property :unread_updates,
15
19
  type: Integer
16
20
 
17
21
  property :default_contact_info_id,
@@ -0,0 +1,19 @@
1
+ module OpenStax
2
+ module Accounts
3
+ module Api
4
+ module V1
5
+ class ApplicationUserSearchRepresenter < UserSearchRepresenter
6
+
7
+ collection :application_users,
8
+ class: OpenStax::Accounts::ApplicationUser,
9
+ decorator: ApplicationUserRepresenter,
10
+ schema_info: {
11
+ description: "The ApplicationUsers associated with the matching Users",
12
+ minItems: 0
13
+ }
14
+
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ require 'representable/json/collection'
2
+
3
+ module OpenStax
4
+ module Accounts
5
+ module Api
6
+ module V1
7
+ class ApplicationUsersRepresenter < Roar::Decorator
8
+ include Representable::JSON::Collection
9
+
10
+ items class: OpenStax::Accounts::ApplicationUser,
11
+ decorator: ApplicationUserRepresenter
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -5,7 +5,8 @@ module OpenStax
5
5
  class UserRepresenter < Roar::Decorator
6
6
  include Roar::Representer::JSON
7
7
 
8
- property :id,
8
+ property :openstax_uid,
9
+ as: :id,
9
10
  type: Integer
10
11
 
11
12
  property :username,
@@ -23,11 +24,6 @@ module OpenStax
23
24
  property :title,
24
25
  type: String
25
26
 
26
- # TODO: Not yet implemented in this gem
27
- # collection :contact_infos,
28
- # class: OpenStax::Accounts::ContactInfo,
29
- # decorator: ContactInfoRepresenter
30
-
31
27
  end
32
28
  end
33
29
  end
@@ -4,14 +4,14 @@ module OpenStax
4
4
  class CreateUser
5
5
  lev_routine
6
6
 
7
- protected
7
+ protected
8
8
 
9
9
  def exec(inputs={})
10
10
 
11
11
  username = inputs[:username]
12
12
 
13
13
  if username.nil? || inputs[:ensure_no_errors]
14
- loop do
14
+ loop do
15
15
  break if !username.nil? && OpenStax::Accounts::User.where(username: username).none?
16
16
  username = "#{inputs[:username] || 'user'}#{rand(1000000)}"
17
17
  end
@@ -23,7 +23,7 @@ module OpenStax
23
23
  user.username = username
24
24
  user.openstax_uid = available_negative_openstax_uid
25
25
  end
26
-
26
+
27
27
  transfer_errors_from(outputs[:user], {type: :verbatim})
28
28
  end
29
29
 
@@ -34,4 +34,4 @@ module OpenStax
34
34
  end
35
35
  end
36
36
  end
37
- end
37
+ end
@@ -0,0 +1,27 @@
1
+ # Routine for searching for users
2
+ #
3
+ # Caller provides a query and some options. The query follows the rules of
4
+ # https://github.com/bruce/keyword_search, e.g.:
5
+ #
6
+ # "username:jps,richb" --> returns the "jps" and "richb" users
7
+ # "name:John" --> returns Users with first, last, or full name starting with "John"
8
+ #
9
+ # Query terms can be combined, e.g. "username:jp first_name:john"
10
+ #
11
+ # There are currently two options to control query pagination:
12
+ #
13
+ # :per_page -- the max number of results to return (default: 20)
14
+ # :page -- the zero-indexed page to return (default: 0)
15
+
16
+ module OpenStax
17
+ module Accounts
18
+ module Dev
19
+ class SearchUsers < OpenStax::Accounts::SearchUsers
20
+ def exec(query, options={})
21
+ options = options.merge!(:max_matching_users => Float::INFINITY)
22
+ super
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,47 +1,189 @@
1
- require 'squeel'
1
+ # Routine for searching for users
2
+ #
3
+ # Caller provides a query and some options. The query follows the rules of
4
+ # https://github.com/bruce/keyword_search , e.g.:
5
+ #
6
+ # "username:jps,richb" --> returns the "jps" and "richb" users
7
+ # "name:John" --> returns Users with first, last, or full name
8
+ # starting with "John"
9
+ #
10
+ # Query terms can be combined, e.g. "username:jp first_name:john"
11
+ #
12
+ # There are currently two options to control query pagination:
13
+ #
14
+ # :per_page -- the max number of results to return per page (default: 20)
15
+ # :page -- the zero-indexed page to return (default: 0)
16
+ #
17
+ # There is also an option to control the ordering:
18
+ #
19
+ # :order_by -- comma-separated list of fields to sort by, with an optional
20
+ # space-separated sort direction (default: "username ASC")
21
+ #
22
+ # Finally, you can also specify a maximum allowed number of results:
23
+ #
24
+ # :max_matching_users -- the max number of results allowed (default: 10)
25
+ #
26
+ # This routine will return an empty relation if the number of results exceeds
27
+ # max_matching_users. You can tell that this happened because the result will
28
+ # have a non-zero num_matching_users.
2
29
 
3
30
  module OpenStax
4
31
  module Accounts
5
-
6
32
  class SearchUsers
33
+
7
34
  lev_routine transaction: :no_transaction
35
+
36
+ protected
37
+
38
+ SORTABLE_FIELDS = ['username', 'first_name', 'last_name', 'id']
39
+ SORT_ASCENDING = 'ASC'
40
+ SORT_DESCENDING = 'DESC'
41
+
42
+ def exec(query, options={})
43
+
44
+ if !OpenStax::Accounts.configuration.enable_stubbing? &&\
45
+ KeywordSearch.search(query).values_at('email', :default).compact.any?
46
+ # Delegate to Accounts
47
+
48
+ response = OpenStax::Accounts.application_users_index(query)
49
+
50
+ user_search = OpenStruct.new
51
+ search_rep = OpenStax::Accounts::Api::V1::UserSearchRepresenter.new(user_search)
52
+ search_rep.from_json(response.body)
53
+
54
+ # Need to query local database in order to obtain ID's (primary keys)
55
+ outputs[:users] = OpenStax::Accounts::User.where{
56
+ openstax_uid.in user_search.users.collect{ |u| u.openstax_uid }
57
+ }
58
+ outputs[:query] = user_search.q
59
+ outputs[:per_page] = user_search.per_page
60
+ outputs[:page] = user_search.page
61
+ outputs[:order_by] = user_search.order_by
62
+ outputs[:num_matching_users] = user_search.num_matching_users
63
+
64
+ else
65
+
66
+ # Local search
67
+ users = OpenStax::Accounts::User.scoped
68
+
69
+ KeywordSearch.search(query) do |with|
70
+
71
+ with.default_keyword :any
72
+
73
+ with.keyword :username do |usernames|
74
+ users = users.where{username.like_any my{prep_usernames(usernames)}}
75
+ end
76
+
77
+ with.keyword :first_name do |first_names|
78
+ users = users.where{lower(first_name).like_any my{prep_names(first_names)}}
79
+ end
80
+
81
+ with.keyword :last_name do |last_names|
82
+ users = users.where{lower(last_name).like_any my{prep_names(last_names)}}
83
+ end
84
+
85
+ with.keyword :full_name do |full_names|
86
+ users = users.where{lower(full_name).like_any my{prep_names(full_names)}}
87
+ end
88
+
89
+ with.keyword :name do |names|
90
+ names = prep_names(names)
91
+ users = users.where{ (lower(full_name).like_any names) |
92
+ (lower(last_name).like_any names) |
93
+ (lower(first_name).like_any names) }
94
+ end
95
+
96
+ with.keyword :id do |ids|
97
+ users = users.where{openstax_uid.in ids}
98
+ end
99
+
100
+ with.keyword :email do |emails|
101
+ users = OpenStax::Accounts::User.where('0=1')
102
+ end
103
+
104
+ # Rerun the queries above for 'any' terms (which are ones without a
105
+ # prefix).
106
+
107
+ with.keyword :any do |terms|
108
+ names = prep_names(terms)
109
+
110
+ users = users.where{
111
+ ( lower(username).like_any my{prep_usernames(terms)}) |
112
+ (lower(first_name).like_any names) |
113
+ (lower(last_name).like_any names) |
114
+ (lower(full_name).like_any names) |
115
+ (id.in terms)
116
+ }
117
+ end
8
118
 
9
- protected
10
-
11
- def exec(terms, type=:any)
12
- # Return empty results if no search terms
13
- return User.where{id == nil}.where{id != nil} if terms.blank?
14
-
15
- # Note: % is the wildcard. This allows the user to search
16
- # for stuff that "begins with" but not "ends with".
17
- case type
18
- when :name
19
- users = User.scoped
20
- terms.gsub(/[%,]/, '').split.each do |t|
21
- next if t.blank?
22
- query = t + '%'
23
- users = users.where{(first_name =~ query) | (last_name =~ query)}
24
119
  end
25
- when :username
26
- query = terms.gsub('%', '') + '%'
27
- users = User.where{username =~ query}
28
- when :any
29
- users = User.scoped
30
- terms.gsub(/[%,]/, '').split.each do |t|
31
- next if t.blank?
32
- query = t + '%'
33
- users = users.where{(first_name =~ query) |
34
- (last_name =~ query) |
35
- (username =~ query)}
120
+
121
+ # Pagination
122
+
123
+ page = options[:page] || 0
124
+ per_page = options[:per_page] || 20
125
+
126
+ users = users.limit(per_page).offset(per_page*page)
127
+
128
+ #
129
+ # Ordering
130
+ #
131
+
132
+ # Parse the input
133
+ order_bys = (options[:order_by] || 'username').split(',').collect{|ob| ob.strip.split(' ')}
134
+
135
+ # Toss out bad input, provide default direction
136
+ order_bys = order_bys.collect do |order_by|
137
+ field, direction = order_by
138
+ next if !SORTABLE_FIELDS.include?(field)
139
+ direction ||= SORT_ASCENDING
140
+ next if direction != SORT_ASCENDING && direction != SORT_DESCENDING
141
+ [field, direction]
36
142
  end
37
- else
38
- fatal_error(:unknown_user_search_type, data: type)
143
+
144
+ order_bys.compact!
145
+
146
+ # Use a default sort if none provided
147
+ order_bys = ['username', SORT_ASCENDING] if order_bys.empty?
148
+
149
+ # Convert to query style
150
+ order_bys = order_bys.collect{|order_by| "#{order_by[0]} #{order_by[1]}"}
151
+
152
+ # Make the ordering call
153
+ order_bys.each do |order_by|
154
+ users = users.order(order_by)
155
+ end
156
+
157
+ # Translate to routine outputs
158
+
159
+ outputs[:users] = users
160
+ outputs[:query] = query
161
+ outputs[:per_page] = per_page
162
+ outputs[:page] = page
163
+ outputs[:order_by] = order_bys.join(', ') # convert back to one string
164
+ outputs[:num_matching_users] = users.except(:offset, :limit, :order).count
165
+
39
166
  end
40
167
 
41
- outputs[:users] = users
168
+ # Return no results if query exceeds maximum allowed number of matches
169
+ max_users = options[:max_matching_users] || \
170
+ OpenStax::Accounts.configuration.max_matching_users
171
+ outputs[:users] = OpenStax::Accounts::User.where('0=1') \
172
+ if outputs[:num_matching_users] > max_users
173
+
174
+ end
175
+
176
+ # Downcase, and put a wildcard at the end.
177
+ # For the moment don't exclude characters.
178
+ def prep_names(names)
179
+ names.collect{|name| name.downcase + '%'}
180
+ end
181
+
182
+ def prep_usernames(usernames)
183
+ usernames.collect{|username| username.gsub(OpenStax::Accounts::User::USERNAME_DISCARDED_CHAR_REGEX, '').downcase + '%'}
42
184
  end
43
185
 
44
186
  end
45
187
 
46
188
  end
47
- end
189
+ end