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