introspective_grape 0.0.4 → 0.1.9
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.
- checksums.yaml +4 -4
- data/.coveralls.yml +2 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +1164 -0
- data/.ruby-version +1 -0
- data/.travis.yml +4 -2
- data/CHANGELOG.md +58 -0
- data/Gemfile +5 -3
- data/README.md +70 -17
- data/introspective_grape.gemspec +8 -8
- data/lib/.DS_Store +0 -0
- data/lib/introspective_grape/api.rb +177 -216
- data/lib/introspective_grape/camel_snake.rb +28 -59
- data/lib/introspective_grape/filters.rb +66 -0
- data/lib/introspective_grape/formatter/camel_json.rb +14 -0
- data/lib/introspective_grape/helpers.rb +63 -0
- data/lib/introspective_grape/traversal.rb +54 -0
- data/lib/introspective_grape/version.rb +1 -1
- data/lib/introspective_grape.rb +11 -0
- data/spec/.DS_Store +0 -0
- data/spec/dummy/Gemfile +5 -3
- data/spec/dummy/app/api/.DS_Store +0 -0
- data/spec/dummy/app/api/api_helpers.rb +5 -6
- data/spec/dummy/app/api/dummy/chat_api.rb +1 -2
- data/spec/dummy/app/api/dummy/company_api.rb +16 -1
- data/spec/dummy/app/api/dummy/location_api.rb +3 -3
- data/spec/dummy/app/api/dummy/project_api.rb +1 -0
- data/spec/dummy/app/api/dummy/sessions.rb +4 -8
- data/spec/dummy/app/api/dummy/user_api.rb +3 -1
- data/spec/dummy/app/api/dummy_api.rb +6 -6
- data/spec/dummy/app/api/error_handlers.rb +2 -2
- data/spec/dummy/app/models/chat_user.rb +1 -1
- data/spec/dummy/app/models/image.rb +2 -2
- data/spec/dummy/app/models/role.rb +1 -1
- data/spec/dummy/app/models/user/chatter.rb +6 -6
- data/spec/dummy/app/models/user_project_job.rb +3 -3
- data/spec/dummy/config/application.rb +1 -1
- data/spec/dummy/db/migrate/20150824215701_create_images.rb +3 -3
- data/spec/dummy/db/schema.rb +1 -1
- data/spec/models/image_spec.rb +1 -1
- data/spec/models/role_spec.rb +5 -5
- data/spec/models/user_location_spec.rb +2 -2
- data/spec/models/user_project_job_spec.rb +1 -1
- data/spec/rails_helper.rb +3 -1
- data/spec/requests/company_api_spec.rb +28 -0
- data/spec/requests/location_api_spec.rb +19 -2
- data/spec/requests/project_api_spec.rb +34 -3
- data/spec/requests/sessions_api_spec.rb +1 -1
- data/spec/requests/user_api_spec.rb +24 -3
- data/spec/support/blueprints.rb +3 -3
- data/spec/support/location_helper.rb +26 -21
- data/spec/support/request_helpers.rb +1 -3
- metadata +58 -28
- data/spec/dummy/app/api/active_record_helpers.rb +0 -17
@@ -0,0 +1,66 @@
|
|
1
|
+
module IntrospectiveGrape::Filters
|
2
|
+
#
|
3
|
+
# Allow filters on all whitelisted model attributes (from api_params)
|
4
|
+
#
|
5
|
+
def simple_filters(klass, model, api_params)
|
6
|
+
@simple_filters ||= api_params.select {|p| p.is_a? Symbol }.map { |field|
|
7
|
+
(klass.param_type(model,field) == DateTime ? ["#{field}_start", "#{field}_end"] : field.to_s)
|
8
|
+
}.flatten
|
9
|
+
end
|
10
|
+
|
11
|
+
def timestamp_filter(klass,model,field)
|
12
|
+
filter = field.sub(/_(end|start)\z/,'')
|
13
|
+
if field =~ /_(end|start)\z/ && klass.param_type(model,filter) == DateTime
|
14
|
+
filter
|
15
|
+
else
|
16
|
+
false
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def identifier_filter(klass,model,field)
|
21
|
+
if field.ends_with?('id') && klass.param_type(model,field) == Integer
|
22
|
+
field
|
23
|
+
else
|
24
|
+
false
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def declare_filter_params(dsl, klass, model, api_params)
|
29
|
+
# Declare optional parameters for filtering parameters, create two parameters per
|
30
|
+
# timestamp, a Start and an End, to apply a date range.
|
31
|
+
simple_filters(klass, model, api_params).each do |field|
|
32
|
+
if timestamp_filter(klass,model,field)
|
33
|
+
terminal = field.ends_with?("_start") ? "initial" : "terminal"
|
34
|
+
dsl.optional field, type: klass.param_type(model,field), description: "Constrain #{field} by #{terminal} date."
|
35
|
+
elsif identifier_filter(klass,model,field)
|
36
|
+
dsl.optional field, type: Array[Integer], coerce_with: ->(val) { val.split(',') }
|
37
|
+
else
|
38
|
+
dsl.optional field, type: klass.param_type(model,field), description: "Filter on #{field} by value."
|
39
|
+
end
|
40
|
+
end
|
41
|
+
dsl.optional :filter, type: String, description: "JSON of conditions for query. If you're familiar with ActiveRecord's query conventions you can build more complex filters, e.g. against included child associations, e.g. {\"<association_name>_<parent>\":{\"field\":\"value\"}}"
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
def apply_filter_params(klass, model, api_params, params, records)
|
46
|
+
simple_filters(klass, model, api_params).each do |field|
|
47
|
+
next if params[field].blank?
|
48
|
+
|
49
|
+
if timestamp_filter(klass,model,field)
|
50
|
+
op = field.ends_with?("_start") ? ">=" : "<="
|
51
|
+
records = records.where("#{timestamp_filter(klass,model,field)} #{op} ?", Time.zone.parse(params[field]))
|
52
|
+
else
|
53
|
+
records = records.where(field => params[field])
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
if params[:filter].present?
|
58
|
+
filters = JSON.parse( params[:filter].delete('\\') )
|
59
|
+
filters.each do |key, value|
|
60
|
+
records = records.where(key => value) if value.present?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
records
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# Add a formatter to grape that converts all snake case hash keys from ruby to camel case.
|
2
|
+
module IntrospectiveGrape
|
3
|
+
module Formatter
|
4
|
+
module CamelJson
|
5
|
+
def self.call(object, _env)
|
6
|
+
if object.respond_to?(:to_json)
|
7
|
+
JSON.parse(object.to_json).with_camel_keys.to_json
|
8
|
+
else
|
9
|
+
MultiJson.dump(object).with_camel_keys.to_json
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module IntrospectiveGrape::Helpers
|
2
|
+
API_ACTIONS = [:index,:show,:create,:update,:destroy].freeze
|
3
|
+
def authentication_method=(method)
|
4
|
+
# IntrospectiveGrape::API.authentication_method=
|
5
|
+
@authentication_method = method
|
6
|
+
end
|
7
|
+
|
8
|
+
def authentication_method(context)
|
9
|
+
# Default to "authenticate!" or as grape docs once suggested, "authorize!"
|
10
|
+
if @authentication_method
|
11
|
+
@authentication_method
|
12
|
+
elsif context.respond_to?('authenticate!')
|
13
|
+
'authenticate!'
|
14
|
+
elsif context.respond_to?('authorize!')
|
15
|
+
'authorize!'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def paginate(args={})
|
20
|
+
@pagination = args
|
21
|
+
end
|
22
|
+
|
23
|
+
def pagination
|
24
|
+
@pagination
|
25
|
+
end
|
26
|
+
|
27
|
+
def exclude_actions(model, *args)
|
28
|
+
@exclude_actions ||= {}; @exclude_actions[model.name] ||= []
|
29
|
+
args.flatten!
|
30
|
+
args = API_ACTIONS if args.include?(:all)
|
31
|
+
args = [] if args.include?(:none)
|
32
|
+
|
33
|
+
undefined_actions = args.compact-API_ACTIONS
|
34
|
+
raise "#{model.name} defines invalid actions: #{undefined_actions}" if undefined_actions.present?
|
35
|
+
|
36
|
+
@exclude_actions[model.name] = args.present? ? args.compact : @exclude_actions[model.name] || []
|
37
|
+
end
|
38
|
+
|
39
|
+
def include_actions(model, *args)
|
40
|
+
@exclude_actions ||= {}; @exclude_actions[model.name] ||= []
|
41
|
+
@exclude_actions[model.name] = API_ACTIONS-exclude_actions(model, args)
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
def default_includes(model, *args)
|
46
|
+
@default_includes ||= {}
|
47
|
+
@default_includes[model.name] = args.present? ? args.flatten : @default_includes[model.name] || []
|
48
|
+
end
|
49
|
+
|
50
|
+
def whitelist(whitelist=nil)
|
51
|
+
return @whitelist if !whitelist
|
52
|
+
@whitelist = whitelist
|
53
|
+
end
|
54
|
+
|
55
|
+
def skip_presence_validations(fields=nil)
|
56
|
+
return @skip_presence_fields||[] if !fields
|
57
|
+
@skip_presence_fields = [fields].flatten
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module IntrospectiveGrape::Traversal
|
2
|
+
# For deeply nested endpoints we want to present the record being affected, these
|
3
|
+
# methods traverse down from the parent instance to the child model associations
|
4
|
+
# of the deeply nested route.
|
5
|
+
|
6
|
+
def find_leaves(routes, record, params)
|
7
|
+
# Traverse down our route and find the leaf's siblings from its parent, e.g.
|
8
|
+
# project/#/teams/#/team_users ~> project.find.teams.find.team_users
|
9
|
+
# (the traversal of the intermediate nodes occurs in find_leaf())
|
10
|
+
return record if routes.size < 2 # the leaf is the root
|
11
|
+
record = find_leaf(routes, record, params)
|
12
|
+
if record
|
13
|
+
assoc = routes.last
|
14
|
+
if assoc.many?
|
15
|
+
leaves = record.send( assoc.reflection.name ).includes( default_includes(assoc.model) )
|
16
|
+
verify_records_found(leaves, routes)
|
17
|
+
leaves
|
18
|
+
else
|
19
|
+
# has_one associations don't return a CollectionProxy and so don't support
|
20
|
+
# eager loading.
|
21
|
+
record.send( assoc.reflection.name )
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def verify_records_found(leaves, routes)
|
27
|
+
unless (leaves.map(&:class) - [routes.last.model]).empty?
|
28
|
+
raise ActiveRecord::RecordNotFound.new("Records contain the wrong models, they should all be #{routes.last.model.name}, found #{records.map(&:class).map(&:name).join(',')}")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def find_leaf(routes, record, params)
|
33
|
+
return record unless routes.size > 1
|
34
|
+
# For deeply nested routes we need to search from the root of the API to the leaf
|
35
|
+
# of its nested associations in order to guarantee the validity of the relationship,
|
36
|
+
# the authorization on the parent model, and the sanity of passed parameters.
|
37
|
+
routes[1..-1].each_with_index do |r|
|
38
|
+
if record && params[r.key]
|
39
|
+
ref = r.reflection
|
40
|
+
record = record.send(ref.name).where( id: params[r.key] ).first if ref
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
verify_record_found(routes, params, record)
|
45
|
+
record
|
46
|
+
end
|
47
|
+
|
48
|
+
def verify_record_found(routes, params, record)
|
49
|
+
if params[routes.last.key] && record.class != routes.last.model
|
50
|
+
raise ActiveRecord::RecordNotFound.new("No #{routes.last.model.name} with ID '#{params[routes.last.key]}'")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
data/lib/introspective_grape.rb
CHANGED
@@ -1,4 +1,15 @@
|
|
1
1
|
module IntrospectiveGrape
|
2
2
|
autoload :API, 'introspective_grape/api'
|
3
3
|
autoload :CamelSnake, 'introspective_grape/camel_snake'
|
4
|
+
autoload :Filters, 'introspective_grape/filters'
|
5
|
+
autoload :Helpers, 'introspective_grape/helpers'
|
6
|
+
autoload :Traversal, 'introspective_grape/traversal'
|
7
|
+
|
8
|
+
module Formatter
|
9
|
+
autoload :CamelJson, 'introspective_grape/formatter/camel_json'
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.config
|
13
|
+
@config = OpenStruct.new(camelize_parameters: true)
|
14
|
+
end
|
4
15
|
end
|
data/spec/.DS_Store
ADDED
Binary file
|
data/spec/dummy/Gemfile
CHANGED
Binary file
|
@@ -1,5 +1,4 @@
|
|
1
1
|
module ApiHelpers
|
2
|
-
include IntrospectiveGrape::CamelSnake
|
3
2
|
def warden
|
4
3
|
env['warden']
|
5
4
|
end
|
@@ -8,13 +7,13 @@ module ApiHelpers
|
|
8
7
|
warden.user || params[:api_key].present? && @user = User.find_by_authentication_token(params[:api_key])
|
9
8
|
end
|
10
9
|
|
11
|
-
def
|
12
|
-
|
10
|
+
def authenticate!
|
11
|
+
unauthenticated! unless current_user
|
13
12
|
end
|
14
13
|
|
15
|
-
# returns an '
|
16
|
-
def
|
17
|
-
respond_error!('
|
14
|
+
# returns an 'unauthenticated' response
|
15
|
+
def unauthenticated!(error_type = nil)
|
16
|
+
respond_error!('unauthenticated', error_type, 401)
|
18
17
|
end
|
19
18
|
|
20
19
|
# returns a error response with given type, message_key and status
|
@@ -1,5 +1,20 @@
|
|
1
1
|
class Dummy::CompanyAPI < IntrospectiveGrape::API
|
2
|
-
|
2
|
+
paginate
|
3
|
+
|
4
|
+
restful Company do
|
5
|
+
|
6
|
+
desc "Test default values in an extra endpoint"
|
7
|
+
params do
|
8
|
+
optional :boolean_default, type: Boolean, default: false
|
9
|
+
optional :string_default, type: String, default: "foo"
|
10
|
+
optional :integer_default, type: Integer, default: 123
|
11
|
+
end
|
12
|
+
get '/special/list' do
|
13
|
+
authorize Company.new, :index?
|
14
|
+
present params
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
3
18
|
|
4
19
|
class CompanyEntity < Grape::Entity
|
5
20
|
expose :id, :name, :short_name, :created_at, :updated_at
|
@@ -1,10 +1,10 @@
|
|
1
1
|
class Dummy::LocationAPI < IntrospectiveGrape::API
|
2
|
+
exclude_actions Location, :none
|
3
|
+
include_actions LocationBeacon, :index
|
4
|
+
include_actions LocationGps, :index
|
2
5
|
|
3
6
|
default_includes Location, :child_locations, :gps, :beacons, :locatables
|
4
7
|
|
5
|
-
exclude_actions LocationBeacon, :show,:create,:update,:destroy
|
6
|
-
exclude_actions LocationGps, :show,:create,:update,:destroy
|
7
|
-
|
8
8
|
restful Location, [:name, :kind,
|
9
9
|
{gps_attributes: [:id, :lat, :lng, :alt, :_destroy]},
|
10
10
|
{beacons_attributes: [:id, :company_id, :mac_address, :uuid, :major, :minor, :_destroy]},
|
@@ -9,6 +9,7 @@ class Dummy::ProjectAPI < IntrospectiveGrape::API
|
|
9
9
|
exclude_actions Team, :show
|
10
10
|
exclude_actions TeamUser, :show,:update
|
11
11
|
|
12
|
+
paginate per_page: 2, max_per_page: 10, offset: 2
|
12
13
|
|
13
14
|
restful Project, [:id, teams_attributes: [:id,:name,:_destroy, team_users_attributes: [:id, :user_id, :_destroy] ]]
|
14
15
|
|
@@ -16,15 +16,10 @@ class Dummy::Sessions < Grape::API
|
|
16
16
|
if user && user.valid_password?(params[:password]) && user.valid_for_authentication?
|
17
17
|
|
18
18
|
# commented out for now, User model is not yet confirmable
|
19
|
-
#
|
19
|
+
#unauthenticated! DummyAPI::USER_NOT_CONFIRMED unless user.confirmed?
|
20
20
|
|
21
21
|
token = nil
|
22
22
|
if params[:token]
|
23
|
-
payload = {
|
24
|
-
uid: "#{user.id}", # uid must be a string
|
25
|
-
email: user.email,
|
26
|
-
avatar_url: user.avatar_url
|
27
|
-
}
|
28
23
|
user.authentication_token = SecureRandom.urlsafe_base64(nil, false)
|
29
24
|
user.save
|
30
25
|
end
|
@@ -33,7 +28,7 @@ class Dummy::Sessions < Grape::API
|
|
33
28
|
env['warden'].set_user(user, scope: :user)
|
34
29
|
present user, with: Dummy::Entities::User, token: token
|
35
30
|
else
|
36
|
-
|
31
|
+
unauthenticated! DummyAPI::BAD_LOGIN
|
37
32
|
end
|
38
33
|
end
|
39
34
|
|
@@ -43,7 +38,8 @@ class Dummy::Sessions < Grape::API
|
|
43
38
|
end
|
44
39
|
delete '/' do
|
45
40
|
authorize User.new, :sessions?
|
46
|
-
|
41
|
+
u = User.find_by_authentication_token(params[:api_key])
|
42
|
+
if u
|
47
43
|
u.authentication_token = nil
|
48
44
|
{status: u.save!}
|
49
45
|
else
|
@@ -2,10 +2,12 @@ class Dummy::UserAPI < IntrospectiveGrape::API
|
|
2
2
|
|
3
3
|
skip_presence_validations :password
|
4
4
|
|
5
|
+
include_actions User, :all
|
5
6
|
exclude_actions Role, :show,:update
|
6
7
|
exclude_actions UserProjectJob, :show,:update
|
7
8
|
|
8
9
|
restful User, [:id, :email, :password, :first_name, :last_name, :skip_confirmation_email,
|
10
|
+
:created_at, :updated_at,
|
9
11
|
user_project_jobs_attributes: [:id, :job_id, :project_id, :_destroy],
|
10
12
|
roles_attributes: [:id, :ownable_type, :ownable_id, :_destroy],
|
11
13
|
avatar_attributes: [:id, :file, :_destroy]
|
@@ -24,7 +26,7 @@ class Dummy::UserAPI < IntrospectiveGrape::API
|
|
24
26
|
end
|
25
27
|
|
26
28
|
class UserEntity < Grape::Entity
|
27
|
-
expose :id, :email, :first_name, :last_name, :avatar_url
|
29
|
+
expose :id, :email, :first_name, :last_name, :avatar_url, :created_at
|
28
30
|
expose :roles, as: :roles_attributes, using: RoleEntity
|
29
31
|
expose :user_project_jobs, as: :user_project_jobs_attributes, using: UserProjectJobEntity
|
30
32
|
end
|
@@ -1,27 +1,27 @@
|
|
1
1
|
#require 'grape-swagger'
|
2
2
|
#require 'grape-entity'
|
3
|
-
require 'active_record_helpers'
|
4
3
|
#require 'introspective_grape/camel_snake'
|
5
4
|
|
6
5
|
class DummyAPI < Grape::API
|
7
6
|
version 'v1', using: :path
|
8
|
-
format
|
7
|
+
format :json
|
8
|
+
formatter :json, IntrospectiveGrape::Formatter::CamelJson
|
9
9
|
default_format :json
|
10
10
|
|
11
11
|
include ErrorHandlers
|
12
12
|
helpers PermissionsHelper
|
13
13
|
helpers ApiHelpers
|
14
14
|
|
15
|
-
USER_NOT_CONFIRMED =
|
16
|
-
BAD_LOGIN
|
15
|
+
USER_NOT_CONFIRMED = 'user_not_confirmed'.freeze
|
16
|
+
BAD_LOGIN = 'bad_login'.freeze
|
17
17
|
|
18
18
|
before do
|
19
19
|
# sets server date in response header. This can be used on the client side
|
20
|
-
header "X-Server-Date",
|
20
|
+
header "X-Server-Date", Time.now.to_i.to_s
|
21
21
|
header "Expires", 1.year.ago.httpdate
|
22
22
|
# Convert incoming camel case params to snake case: grape will totally blow this
|
23
23
|
# if the params hash is not a Hashie::Mash, so make it one of those:
|
24
|
-
#@params = Hashie::Mash.new(
|
24
|
+
#@params = Hashie::Mash.new(params.with_snake_keys)
|
25
25
|
end
|
26
26
|
|
27
27
|
before_validation do
|
@@ -17,11 +17,11 @@ module ErrorHandlers
|
|
17
17
|
error_response message: "Join record not found! #{e.message}", status: 404
|
18
18
|
end
|
19
19
|
|
20
|
-
m.rescue_from Pundit::NotAuthorizedError do
|
20
|
+
m.rescue_from Pundit::NotAuthorizedError do
|
21
21
|
error_response message: "Forbidden", status: 403
|
22
22
|
end
|
23
23
|
|
24
|
-
m.rescue_from Pundit::NotDefinedError do
|
24
|
+
m.rescue_from Pundit::NotDefinedError do
|
25
25
|
error_response message: "Policy not implemented", status: 501
|
26
26
|
end
|
27
27
|
end
|
@@ -10,7 +10,7 @@ class ChatUser < AbstractAdapter
|
|
10
10
|
validate :user_not_already_active, on: :create
|
11
11
|
|
12
12
|
def user_not_already_active
|
13
|
-
errors[:base] << "#{user.name} is already present in this chat." if chat.chat_users.where(user_id: user.id, departed_at: nil).count > 0
|
13
|
+
errors[:base] << "#{user.name} is already present in this chat." if chat.chat_users.where(user_id: user.id, departed_at: nil).count > 0 && user.persisted?
|
14
14
|
end
|
15
15
|
|
16
16
|
end
|
@@ -11,10 +11,10 @@ class Image < ActiveRecord::Base
|
|
11
11
|
|
12
12
|
#process_in_background :file, processing_image_url: 'empty_avatar.png'
|
13
13
|
|
14
|
-
Paperclip.interpolates :imageable_type do |attachment,
|
14
|
+
Paperclip.interpolates :imageable_type do |attachment, _style|
|
15
15
|
attachment.instance.imageable_type.try(:pluralize)
|
16
16
|
end
|
17
|
-
Paperclip.interpolates :imageable_id do |attachment,
|
17
|
+
Paperclip.interpolates :imageable_id do |attachment, _style|
|
18
18
|
attachment.instance.imageable_id
|
19
19
|
end
|
20
20
|
|
@@ -15,7 +15,7 @@ class Role < AbstractAdapter
|
|
15
15
|
ownable_type == 'SuperUser' ? SuperUser.new : super
|
16
16
|
end
|
17
17
|
|
18
|
-
def self.ownable_assign_options(
|
18
|
+
def self.ownable_assign_options(_model=nil)
|
19
19
|
([SuperUser.new] + Company.all + Project.all).map { |i| [ "#{i.class}: #{i.name}", "#{i.class}-#{i.id}"] }
|
20
20
|
end
|
21
21
|
|
@@ -2,15 +2,15 @@ module User::Chatter
|
|
2
2
|
|
3
3
|
def message_query(chat_id, new = true)
|
4
4
|
messages.joins(:chat_message_users)
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
.where('chat_message_users.user_id'=> id)
|
6
|
+
.where(new ? {'chat_message_users.read_at'=>nil} : '')
|
7
|
+
.where(chat_id ? {'chat_messages.chat_id'=> chat_id} : '')
|
8
|
+
.order('') # or it will add an order by id clause that breaks the count query.
|
9
9
|
end
|
10
10
|
|
11
11
|
def new_messages?(chat=nil) # returns a hash of chat_ids with new message counts
|
12
12
|
chat_id = chat.kind_of?(Chat) ? chat.id : chat
|
13
|
-
new = message_query(chat_id
|
13
|
+
new = message_query(chat_id)
|
14
14
|
.select("chat_messages.chat_id, count(chat_messages.id) as count")
|
15
15
|
.group('chat_id')
|
16
16
|
|
@@ -20,7 +20,7 @@ module User::Chatter
|
|
20
20
|
def read_messages(chat= nil, mark_as_read= false, new= true)
|
21
21
|
chat_id = chat.kind_of?(Chat) ? chat.id : chat
|
22
22
|
new = message_query(chat_id, new).order('chat_messages.created_at').includes(:author) # :chat?
|
23
|
-
new.map(&:chat).uniq.each {|
|
23
|
+
new.map(&:chat).uniq.each {|c| mark_as_read(c) } if mark_as_read
|
24
24
|
new
|
25
25
|
end
|
26
26
|
|
@@ -5,9 +5,9 @@ class UserProjectJob < AbstractAdapter
|
|
5
5
|
|
6
6
|
validates_inclusion_of :job, in: proc {|r| r.project.try(:jobs) || [] }
|
7
7
|
|
8
|
-
delegate :email, :avatar_url, to: :user
|
9
|
-
delegate :title,
|
10
|
-
delegate :name,
|
8
|
+
delegate :email, :avatar_url, to: :user, allow_nil: true
|
9
|
+
delegate :title, to: :job, allow_nil: true
|
10
|
+
delegate :name, to: :project, allow_nil: true
|
11
11
|
|
12
12
|
def self.options_for_job(project=nil)
|
13
13
|
project.jobs
|
@@ -5,7 +5,7 @@ require "active_record/railtie"
|
|
5
5
|
require "action_controller/railtie"
|
6
6
|
require "action_mailer/railtie"
|
7
7
|
require "action_view/railtie"
|
8
|
-
require "sprockets/railtie"
|
8
|
+
#require "sprockets/railtie"
|
9
9
|
require 'activerecord-tableless'
|
10
10
|
require 'devise'
|
11
11
|
require 'devise/async'
|
@@ -4,10 +4,10 @@ class CreateImages < ActiveRecord::Migration
|
|
4
4
|
t.references :imageable, polymorphic: true, index: true
|
5
5
|
t.attachment :file
|
6
6
|
t.boolean :file_processing, null: false, default: false
|
7
|
-
t.json
|
7
|
+
t.json :meta
|
8
8
|
t.string :source
|
9
|
-
t.float
|
10
|
-
t.float
|
9
|
+
t.float :lat
|
10
|
+
t.float :lng
|
11
11
|
t.timestamp :taken_at
|
12
12
|
t.timestamps null: false
|
13
13
|
end
|
data/spec/dummy/db/schema.rb
CHANGED
@@ -102,7 +102,7 @@ ActiveRecord::Schema.define(version: 20150909225019) do
|
|
102
102
|
t.integer "file_file_size"
|
103
103
|
t.datetime "file_updated_at"
|
104
104
|
t.boolean "file_processing", default: false, null: false
|
105
|
-
t.text
|
105
|
+
t.text "meta"
|
106
106
|
t.string "source"
|
107
107
|
t.float "lat"
|
108
108
|
t.float "lng"
|
data/spec/models/image_spec.rb
CHANGED
@@ -6,7 +6,7 @@ RSpec.describe Image, type: :model do
|
|
6
6
|
|
7
7
|
it { should have_attached_file(:file) }
|
8
8
|
it { should validate_attachment_content_type(:file).
|
9
|
-
|
9
|
+
allowing('image/png', 'image/gif', 'image/jpeg')
|
10
10
|
}
|
11
11
|
it { should validate_attachment_size(:file).less_than(2.megabytes) }
|
12
12
|
|
data/spec/models/role_spec.rb
CHANGED
@@ -25,35 +25,35 @@ RSpec.describe Role, type: :model do
|
|
25
25
|
context "User helper methods" do
|
26
26
|
it "should register a user as a super user" do
|
27
27
|
user.superuser?.should == false
|
28
|
-
|
28
|
+
Role.create!(user:user, ownable: SuperUser.new)
|
29
29
|
user.reload
|
30
30
|
user.superuser?.should == true
|
31
31
|
end
|
32
32
|
|
33
33
|
it "should register a company admin" do
|
34
34
|
user.admin?(company).should == false
|
35
|
-
|
35
|
+
Role.create!(user:user, ownable: company)
|
36
36
|
user.reload
|
37
37
|
user.admin?(company).should == true
|
38
38
|
end
|
39
39
|
|
40
40
|
it "should register a project administrator" do
|
41
41
|
user.admin?(project).should == false
|
42
|
-
|
42
|
+
Role.create!(user:user, ownable: project)
|
43
43
|
user.reload
|
44
44
|
user.admin?(project).should == true
|
45
45
|
end
|
46
46
|
|
47
47
|
it "should register a user a company admin if admin of any company" do
|
48
48
|
user.company_admin?.should == false
|
49
|
-
|
49
|
+
Role.create!(user:user, ownable: company)
|
50
50
|
user.reload
|
51
51
|
user.company_admin?.should == true
|
52
52
|
end
|
53
53
|
|
54
54
|
it "should register a user as a project admin if admin of any project" do
|
55
55
|
user.project_admin?.should == false
|
56
|
-
|
56
|
+
Role.create!(user:user, ownable: project)
|
57
57
|
user.reload
|
58
58
|
user.project_admin?.should == true
|
59
59
|
end
|
@@ -16,7 +16,7 @@ RSpec.describe UserLocation, type: :model do
|
|
16
16
|
|
17
17
|
it "logs a user's locations by beacon" do
|
18
18
|
beacon = LocationBeacon.last
|
19
|
-
|
19
|
+
user.user_locations.build(location: beacon.location, detectable: beacon, coords: rand_coords)
|
20
20
|
user.save.should == true
|
21
21
|
p2 = user.user_locations.build(location: beacon.location, detectable: beacon, coords: rand_coords)
|
22
22
|
user.save.should == true
|
@@ -25,7 +25,7 @@ RSpec.describe UserLocation, type: :model do
|
|
25
25
|
|
26
26
|
it "logs a user's beacon location by gps" do
|
27
27
|
gps = LocationGps.last
|
28
|
-
|
28
|
+
user.user_locations.build(location: gps.location, detectable: gps, coords: rand_coords)
|
29
29
|
user.save.should == true
|
30
30
|
p2 = user.user_locations.build(location: gps.location, detectable: gps, coords: rand_coords)
|
31
31
|
user.save.should == true
|