openstax_accounts 0.3.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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