mno-enterprise-core 2.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.
- checksums.yaml +7 -0
- data/LICENSE +1 -0
- data/Rakefile +12 -0
- data/app/assets/images/mno_enterprise/main-logo.png +0 -0
- data/app/controllers/mno_enterprise/application_controller.rb +116 -0
- data/app/helpers/mno_enterprise/application_helper.rb +67 -0
- data/app/helpers/mno_enterprise/impersonate_helper.rb +27 -0
- data/app/models/mno_enterprise/ability.rb +6 -0
- data/app/models/mno_enterprise/app.rb +72 -0
- data/app/models/mno_enterprise/app_instance.rb +36 -0
- data/app/models/mno_enterprise/app_instances_sync.rb +6 -0
- data/app/models/mno_enterprise/arrears_situation.rb +6 -0
- data/app/models/mno_enterprise/audit_event.rb +21 -0
- data/app/models/mno_enterprise/base_resource.rb +228 -0
- data/app/models/mno_enterprise/credit_card.rb +40 -0
- data/app/models/mno_enterprise/deletion_request.rb +35 -0
- data/app/models/mno_enterprise/impac/dashboard.rb +36 -0
- data/app/models/mno_enterprise/impac/dashboard_provisioner.rb +5 -0
- data/app/models/mno_enterprise/impac/kpi.rb +9 -0
- data/app/models/mno_enterprise/impac/widget.rb +13 -0
- data/app/models/mno_enterprise/invoice.rb +53 -0
- data/app/models/mno_enterprise/org_invite.rb +50 -0
- data/app/models/mno_enterprise/organization.rb +33 -0
- data/app/models/mno_enterprise/team.rb +50 -0
- data/app/models/mno_enterprise/tenant.rb +5 -0
- data/app/models/mno_enterprise/tenant_invoice.rb +5 -0
- data/app/models/mno_enterprise/user.rb +183 -0
- data/app/pdf/mno_enterprise/invoice_pdf.rb +516 -0
- data/config/initializers/audit_log.rb +5 -0
- data/config/locales/devise.en.yml +60 -0
- data/config/routes.rb +2 -0
- data/config/styleguide.yml +106 -0
- data/lib/accountingjs_serializer.rb +51 -0
- data/lib/devise/controllers/extension_helpers.rb +52 -0
- data/lib/devise/extension_routes.rb +11 -0
- data/lib/devise/hooks/password_expirable.rb +5 -0
- data/lib/devise/models/password_expirable.rb +28 -0
- data/lib/devise/models/remote_authenticatable.rb +48 -0
- data/lib/devise/strategies/remote_authenticatable.rb +44 -0
- data/lib/devise_extension.rb +36 -0
- data/lib/faraday/adapter/net_http_no_proxy.rb +19 -0
- data/lib/generators/mno_enterprise/database_extension/USAGE +11 -0
- data/lib/generators/mno_enterprise/database_extension/database_extension_generator.rb +36 -0
- data/lib/generators/mno_enterprise/database_extension/templates/model.rb +9 -0
- data/lib/generators/mno_enterprise/dummy/dummy_generator.rb +98 -0
- data/lib/generators/mno_enterprise/dummy/templates/rails/application.rb.erb +9 -0
- data/lib/generators/mno_enterprise/dummy/templates/rails/boot.rb.erb +6 -0
- data/lib/generators/mno_enterprise/dummy/templates/rails/database.yml +22 -0
- data/lib/generators/mno_enterprise/dummy/templates/rails/routes.rb +8 -0
- data/lib/generators/mno_enterprise/dummy/templates/rails/test-env.rb +45 -0
- data/lib/generators/mno_enterprise/install/install_generator.rb +140 -0
- data/lib/generators/mno_enterprise/install/templates/Procfile +1 -0
- data/lib/generators/mno_enterprise/install/templates/config/initializers/mno_enterprise.rb +135 -0
- data/lib/generators/mno_enterprise/install/templates/config/mno_enterprise_styleguide.yml +104 -0
- data/lib/generators/mno_enterprise/install/templates/javascripts/mno_enterprise_extensions.js +7 -0
- data/lib/generators/mno_enterprise/install/templates/stylesheets/main.less_erb +25 -0
- data/lib/generators/mno_enterprise/install/templates/stylesheets/theme.less_erb +59 -0
- data/lib/generators/mno_enterprise/install/templates/stylesheets/variables.less +337 -0
- data/lib/generators/mno_enterprise/install/templates/tasks/sprites.rake +14 -0
- data/lib/generators/mno_enterprise/puma_stack/puma_stack_generator.rb +58 -0
- data/lib/generators/mno_enterprise/templates/scripts/monit/app-server.conf +8 -0
- data/lib/generators/mno_enterprise/templates/scripts/nginx/app +51 -0
- data/lib/generators/mno_enterprise/templates/scripts/puma.rb +25 -0
- data/lib/generators/mno_enterprise/templates/scripts/setup.sh +27 -0
- data/lib/generators/mno_enterprise/templates/scripts/upstart/app-web-hotrestart.conf +26 -0
- data/lib/generators/mno_enterprise/templates/scripts/upstart/app-web-server.conf +34 -0
- data/lib/generators/mno_enterprise/templates/scripts/upstart/app-web.conf +2 -0
- data/lib/generators/mno_enterprise/templates/scripts/upstart/app.conf +11 -0
- data/lib/her_extension/her_orm_adapter.rb +54 -0
- data/lib/her_extension/middleware/mnoe_api_v1_parse_json.rb +54 -0
- data/lib/her_extension/model/associations/association.rb +61 -0
- data/lib/her_extension/model/associations/association_proxy.rb +34 -0
- data/lib/her_extension/model/associations/has_many_association.rb +115 -0
- data/lib/her_extension/model/attributes.rb +43 -0
- data/lib/her_extension/model/orm.rb +59 -0
- data/lib/her_extension/model/parse.rb +40 -0
- data/lib/her_extension/model/relation.rb +92 -0
- data/lib/her_extension/validations/remote_uniqueness_validation.rb +33 -0
- data/lib/html_processor.rb +106 -0
- data/lib/mandrill_client.rb +58 -0
- data/lib/mno-enterprise-core.rb +1 -0
- data/lib/mno_enterprise/concerns.rb +4 -0
- data/lib/mno_enterprise/concerns/controllers.rb +6 -0
- data/lib/mno_enterprise/concerns/controllers/angular_csrf.rb +59 -0
- data/lib/mno_enterprise/concerns/controllers/auth.rb +9 -0
- data/lib/mno_enterprise/concerns/controllers/auth/confirmations_controller.rb +187 -0
- data/lib/mno_enterprise/concerns/controllers/auth/passwords_controller.rb +54 -0
- data/lib/mno_enterprise/concerns/controllers/auth/registrations_controller.rb +136 -0
- data/lib/mno_enterprise/concerns/controllers/auth/sessions_controller.rb +54 -0
- data/lib/mno_enterprise/concerns/controllers/auth/unlocks_controller.rb +50 -0
- data/lib/mno_enterprise/concerns/models.rb +6 -0
- data/lib/mno_enterprise/concerns/models/ability.rb +108 -0
- data/lib/mno_enterprise/concerns/models/app_instance.rb +100 -0
- data/lib/mno_enterprise/concerns/models/organization.rb +102 -0
- data/lib/mno_enterprise/core.rb +279 -0
- data/lib/mno_enterprise/database_extendable.rb +57 -0
- data/lib/mno_enterprise/engine.rb +33 -0
- data/lib/mno_enterprise/testing_support/ability_test_helper.rb +10 -0
- data/lib/mno_enterprise/testing_support/common_rake.rb +19 -0
- data/lib/mno_enterprise/testing_support/factories.rb +13 -0
- data/lib/mno_enterprise/testing_support/factories/app_instances.rb +30 -0
- data/lib/mno_enterprise/testing_support/factories/apps.rb +45 -0
- data/lib/mno_enterprise/testing_support/factories/arrears_situation.rb +14 -0
- data/lib/mno_enterprise/testing_support/factories/audit_event.rb +15 -0
- data/lib/mno_enterprise/testing_support/factories/credit_card.rb +33 -0
- data/lib/mno_enterprise/testing_support/factories/deletion_request.rb +17 -0
- data/lib/mno_enterprise/testing_support/factories/impac/dashboards.rb +15 -0
- data/lib/mno_enterprise/testing_support/factories/impac/kpis.rb +20 -0
- data/lib/mno_enterprise/testing_support/factories/impac/widgets.rb +15 -0
- data/lib/mno_enterprise/testing_support/factories/invoices.rb +51 -0
- data/lib/mno_enterprise/testing_support/factories/org_invite.rb +24 -0
- data/lib/mno_enterprise/testing_support/factories/organizations.rb +25 -0
- data/lib/mno_enterprise/testing_support/factories/team.rb +17 -0
- data/lib/mno_enterprise/testing_support/factories/tenant.rb +12 -0
- data/lib/mno_enterprise/testing_support/factories/tenant_invoice.rb +29 -0
- data/lib/mno_enterprise/testing_support/factories/users.rb +48 -0
- data/lib/mno_enterprise/testing_support/jpi_v1_test_helper.rb +49 -0
- data/lib/mno_enterprise/testing_support/mno_enterprise_api_test_helper.rb +167 -0
- data/lib/mno_enterprise/testing_support/mnoe_faraday_test_adapter.rb +173 -0
- data/lib/mno_enterprise/testing_support/organizations_shared_helpers.rb +175 -0
- data/lib/mno_enterprise/testing_support/user_action_shared.rb +47 -0
- data/lib/mno_enterprise/version.rb +3 -0
- data/lib/tasks/mno_enterprise_tasks.rake +22 -0
- data/spec/controllers/mno_enterprise/angular_csrf_spec.rb +42 -0
- data/spec/lib/her_extension/her_orm_adapter.rb +7 -0
- data/spec/lib/her_extension/model/relation_spec.rb +7 -0
- data/spec/lib/mandrill_client_spec.rb +64 -0
- data/spec/mno_enterprise_spec.rb +79 -0
- data/spec/models/mno_enterprise/app_instance_spec.rb +7 -0
- data/spec/models/mno_enterprise/app_spec.rb +62 -0
- data/spec/models/mno_enterprise/base_resource_spec.rb +28 -0
- data/spec/models/mno_enterprise/deletion_request_spec.rb +26 -0
- data/spec/models/mno_enterprise/invoice_spec.rb +7 -0
- data/spec/models/mno_enterprise/organization_spec.rb +7 -0
- data/spec/models/mno_enterprise/user_spec.rb +44 -0
- data/spec/rails_helper.rb +73 -0
- data/spec/spec_helper.rb +78 -0
- metadata +421 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# PR:
|
|
2
|
+
# fixed options loading in dynamically defined method
|
|
3
|
+
# changed build to not add the parent key automatically
|
|
4
|
+
# changed create to send raw parameters instead of doing smart stuff
|
|
5
|
+
module Her
|
|
6
|
+
module Model
|
|
7
|
+
module Associations
|
|
8
|
+
class HasManyAssociation < Association
|
|
9
|
+
|
|
10
|
+
# @private
|
|
11
|
+
def self.attach(klass, name, opts)
|
|
12
|
+
opts = {
|
|
13
|
+
:class_name => name.to_s.classify,
|
|
14
|
+
:name => name,
|
|
15
|
+
:data_key => name,
|
|
16
|
+
:default => Her::Collection.new,
|
|
17
|
+
:path => "/#{name}",
|
|
18
|
+
:inverse_of => nil
|
|
19
|
+
}.merge(opts)
|
|
20
|
+
klass.associations[:has_many] << opts
|
|
21
|
+
|
|
22
|
+
klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
23
|
+
def #{name}
|
|
24
|
+
cached_name = :"@_her_association_#{name}"
|
|
25
|
+
cached_data = (instance_variable_defined?(cached_name) && instance_variable_get(cached_name))
|
|
26
|
+
opts = Marshal.load(#{Marshal.dump(opts).inspect})
|
|
27
|
+
cached_data || instance_variable_set(cached_name, Her::Model::Associations::HasManyAssociation.proxy(self, opts))
|
|
28
|
+
end
|
|
29
|
+
RUBY
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Initialize a new object with a foreign key to the parent
|
|
33
|
+
#
|
|
34
|
+
# @example
|
|
35
|
+
# class User
|
|
36
|
+
# include Her::Model
|
|
37
|
+
# has_many :comments
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# class Comment
|
|
41
|
+
# include Her::Model
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# user = User.find(1)
|
|
45
|
+
# new_comment = user.comments.build(:body => "Hello!")
|
|
46
|
+
# new_comment # => #<Comment body="Hello!">
|
|
47
|
+
#
|
|
48
|
+
def build(attributes = {})
|
|
49
|
+
@klass.build(attributes)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Post an object to the nested resource collection endpoint then
|
|
53
|
+
# refetch the nested collection
|
|
54
|
+
#
|
|
55
|
+
# @example
|
|
56
|
+
# class User
|
|
57
|
+
# include Her::Model
|
|
58
|
+
# has_many :comments
|
|
59
|
+
# end
|
|
60
|
+
#
|
|
61
|
+
# class Comment
|
|
62
|
+
# include Her::Model
|
|
63
|
+
# end
|
|
64
|
+
#
|
|
65
|
+
# user = User.find(1)
|
|
66
|
+
# user.comments.create(:body => "Hello!")
|
|
67
|
+
# user.comments # => [#<Comment id=2 user_id=1 body="Hello!">]
|
|
68
|
+
def create(attributes = {})
|
|
69
|
+
resp = self.execute_request(:create,attributes)
|
|
70
|
+
@klass.build(resp)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Consider removing - not sure this method on a has_many collection has any meaning
|
|
74
|
+
def update(attributes = {})
|
|
75
|
+
self.execute_request(:update,attributes)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Consider removing - not sure this method on a has_many collection has any meaning
|
|
79
|
+
def destroy(attributes = {})
|
|
80
|
+
self.execute_request(:destroy,attributes)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def execute_request(action, attrs)
|
|
84
|
+
attributes = HashWithIndifferentAccess.new(attrs)
|
|
85
|
+
|
|
86
|
+
# Post data to the collection endpoint
|
|
87
|
+
resource = nil
|
|
88
|
+
path = self.build_request_path
|
|
89
|
+
method = self.method_for(action.to_sym)
|
|
90
|
+
|
|
91
|
+
# Add ID to path if resource method
|
|
92
|
+
if [:put, :patch, :delete].include?(method.to_sym)
|
|
93
|
+
path += "/#{attributes[@klass.primary_key]}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
params = self.to_params(attributes).merge(:_method => method, :_path => path)
|
|
97
|
+
self.request(params) do |parsed_data, response|
|
|
98
|
+
resource = parsed_data if response.success?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Reload nested collection
|
|
102
|
+
self.reload if resource
|
|
103
|
+
|
|
104
|
+
resource
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Override fetch to not do any smart stuff...
|
|
108
|
+
def fetch
|
|
109
|
+
super
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module Her
|
|
2
|
+
module Model
|
|
3
|
+
# TODO: make PR to Her project
|
|
4
|
+
# This patch fixes the detection of changed attributes
|
|
5
|
+
module Attributes
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
module ClassMethods
|
|
10
|
+
# Define the attributes that will be used to track dirty attributes and validations
|
|
11
|
+
#
|
|
12
|
+
# @param [Array] attributes
|
|
13
|
+
# @example
|
|
14
|
+
# class User
|
|
15
|
+
# include Her::Model
|
|
16
|
+
# attributes :name, :email
|
|
17
|
+
# end
|
|
18
|
+
def attributes(*attributes)
|
|
19
|
+
define_attribute_methods attributes
|
|
20
|
+
|
|
21
|
+
attributes.each do |attribute|
|
|
22
|
+
attribute = attribute.to_sym
|
|
23
|
+
|
|
24
|
+
unless instance_methods.include?(:"#{attribute}=")
|
|
25
|
+
define_method("#{attribute}=") do |value|
|
|
26
|
+
@attributes[:"#{attribute}"] = nil unless @attributes.include?(:"#{attribute}")
|
|
27
|
+
self.send(:"#{attribute}_will_change!") if @attributes[:"#{attribute}"] != value
|
|
28
|
+
@attributes[:"#{attribute}"] = value
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
unless instance_methods.include?(:"#{attribute}?")
|
|
33
|
+
define_method("#{attribute}?") do
|
|
34
|
+
@attributes.include?(:"#{attribute}") && @attributes[:"#{attribute}"].present?
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# FIX: Reset params when blank_relation is called so that query statement (like where)
|
|
2
|
+
# automatically start fresh
|
|
3
|
+
#
|
|
4
|
+
# FIX: scope to create real isolated scopes for each Her::Model
|
|
5
|
+
#
|
|
6
|
+
# FIX: add put method on model
|
|
7
|
+
module Her
|
|
8
|
+
module Model
|
|
9
|
+
module ORM
|
|
10
|
+
|
|
11
|
+
# Send raw PUT request to model - no data encapsulation performed
|
|
12
|
+
def put(attrs)
|
|
13
|
+
method = :put
|
|
14
|
+
self.class.request(attrs.merge(:_method => method, :_path => request_path)) do |parsed_data, response|
|
|
15
|
+
return parsed_data
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
module ClassMethods
|
|
20
|
+
# Create a new chainable scope
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# class User
|
|
24
|
+
# include Her::Model
|
|
25
|
+
#
|
|
26
|
+
# scope :admins, lambda { where(:admin => 1) }
|
|
27
|
+
# scope :page, lambda { |page| where(:page => page) }
|
|
28
|
+
# enc
|
|
29
|
+
#
|
|
30
|
+
# User.admins # Called via GET "/users?admin=1"
|
|
31
|
+
# User.page(2).all # Called via GET "/users?page=2"
|
|
32
|
+
def scope(name, code)
|
|
33
|
+
|
|
34
|
+
# Add the scope method to the class
|
|
35
|
+
metaclass = (class << self; self end)
|
|
36
|
+
metaclass.send(:define_method, name) do |*args|
|
|
37
|
+
instance_exec(*args, &code)
|
|
38
|
+
end
|
|
39
|
+
Relation.scopes["#{self.to_s}.#{name}"] = code
|
|
40
|
+
|
|
41
|
+
# Add the scope method to the Relation class
|
|
42
|
+
Relation.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
43
|
+
def #{name}(*args)
|
|
44
|
+
parent_klass = @parent.instance_variable_get("@klass") || @parent.to_s
|
|
45
|
+
instance_exec(*args,&self.class.scopes["\#{parent_klass}.#{name}"])
|
|
46
|
+
end
|
|
47
|
+
RUBY
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
def blank_relation
|
|
52
|
+
@blank_relation ||= Relation.new(self)
|
|
53
|
+
@blank_relation.params = {}
|
|
54
|
+
@blank_relation
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# TODO: PR to HER
|
|
2
|
+
# Fix to_params when embeded_params is nil
|
|
3
|
+
# Fix to_params when changes is nil
|
|
4
|
+
# --> allow all params - this is required to be able to make class level
|
|
5
|
+
# requests like MyModel.post(path,{some: 'data'})
|
|
6
|
+
#
|
|
7
|
+
module Her
|
|
8
|
+
module Model
|
|
9
|
+
# This module handles resource data parsing at the model level (after the parsing middleware)
|
|
10
|
+
module Parse
|
|
11
|
+
extend ActiveSupport::Concern
|
|
12
|
+
|
|
13
|
+
module ClassMethods
|
|
14
|
+
|
|
15
|
+
# @private
|
|
16
|
+
def to_params(attributes, changes = nil)
|
|
17
|
+
filtered_attributes = attributes.dup.symbolize_keys
|
|
18
|
+
filtered_attributes.merge!(embeded_params(attributes) || {})
|
|
19
|
+
if her_api && her_api.options[:send_only_modified_attributes] && !changes.nil?
|
|
20
|
+
filtered_attributes = changes.symbolize_keys.keys.inject({}) do |hash, attribute|
|
|
21
|
+
hash[attribute] = filtered_attributes[attribute]
|
|
22
|
+
hash
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if include_root_in_json?
|
|
27
|
+
if json_api_format?
|
|
28
|
+
{ included_root_element => [filtered_attributes] }
|
|
29
|
+
else
|
|
30
|
+
{ included_root_element => filtered_attributes }
|
|
31
|
+
end
|
|
32
|
+
else
|
|
33
|
+
filtered_attributes
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
require 'digest/md5'
|
|
2
|
+
|
|
3
|
+
# TODO: we should do a PR to the HER project which includes
|
|
4
|
+
# smarter filtering and more reliable caching
|
|
5
|
+
# Before doing so, we make the filtering behavior customizable
|
|
6
|
+
# (E.g.: configure name of filter/sort/limit keys)
|
|
7
|
+
module Her
|
|
8
|
+
module Model
|
|
9
|
+
class Relation
|
|
10
|
+
def initialize(parent)
|
|
11
|
+
@parent = parent
|
|
12
|
+
@params = {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Fetch a collection of resources
|
|
16
|
+
def fetch
|
|
17
|
+
path = @parent.build_request_path(@params)
|
|
18
|
+
method = @parent.method_for(:find)
|
|
19
|
+
@parent.request(@params.merge(:_method => method, :_path => path)) do |parsed_data, response|
|
|
20
|
+
@parent.new_collection(parsed_data)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Override Her::Model::Relation#where
|
|
25
|
+
# to follow jsonapi.org standards
|
|
26
|
+
# Use filter instead of raw parameters
|
|
27
|
+
def where(params = {})
|
|
28
|
+
return self if !params || params.empty?
|
|
29
|
+
self.params[:filter] ||= {}
|
|
30
|
+
self.params[:filter].merge!(params)
|
|
31
|
+
self
|
|
32
|
+
end
|
|
33
|
+
alias all where
|
|
34
|
+
|
|
35
|
+
# E.g:
|
|
36
|
+
# Product.order_by('created_at.desc','name.asc')
|
|
37
|
+
def order_by(*args)
|
|
38
|
+
return self if args.empty?
|
|
39
|
+
self.params[:sort] = [self.params[:sort],args].flatten.compact.uniq
|
|
40
|
+
self
|
|
41
|
+
end
|
|
42
|
+
alias sort_by order_by
|
|
43
|
+
|
|
44
|
+
# ActiveRecord-like order
|
|
45
|
+
# Product.order("created_at DESC, name ASC")
|
|
46
|
+
def order(string_query)
|
|
47
|
+
return self if !string_query || string_query.empty?
|
|
48
|
+
args = string_query.split(',').map do |q|
|
|
49
|
+
field, direction = q.strip.split(/\s+/).compact
|
|
50
|
+
[field, direction ? direction.downcase : nil].join('.')
|
|
51
|
+
end
|
|
52
|
+
self.order_by(*args)
|
|
53
|
+
end
|
|
54
|
+
alias sort order
|
|
55
|
+
|
|
56
|
+
# Limit the number of results returned
|
|
57
|
+
def limit(max = nil)
|
|
58
|
+
return self if !max
|
|
59
|
+
self.params[:limit] = max
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Skip result rows
|
|
64
|
+
def skip(nrows = nil)
|
|
65
|
+
return self if !nrows
|
|
66
|
+
self.params[:skip] = nrows
|
|
67
|
+
self
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Reset the query parameters
|
|
71
|
+
def reset_params
|
|
72
|
+
@params.clear
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Refetch the relation
|
|
76
|
+
def reload
|
|
77
|
+
fetch
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Hold the scopes defined on Her::Model classes
|
|
81
|
+
# E.g.: scope :admin, -> { where(admin: 1) }
|
|
82
|
+
def self.scopes
|
|
83
|
+
@scopes ||= {}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
def query_checkum
|
|
88
|
+
Digest::MD5.hexdigest(Marshal.dump(@params))
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# TODO:
|
|
2
|
+
# - spec module
|
|
3
|
+
# - spec that validator ignores records with the same id
|
|
4
|
+
module HerExtension
|
|
5
|
+
module Validations
|
|
6
|
+
|
|
7
|
+
# Validate the uniqueness of a field by performing a remote API call
|
|
8
|
+
class RemoteUniquenessValidator < ::ActiveModel::EachValidator
|
|
9
|
+
def validate_each(record,attribute,value)
|
|
10
|
+
list = record.class.where({ attribute => value }).limit(1)
|
|
11
|
+
|
|
12
|
+
if list.reject { |e| e.id == record.id }.any?
|
|
13
|
+
error_options = options.except(:case_sensitive, :scope, :conditions)
|
|
14
|
+
error_options[:value] = value
|
|
15
|
+
record.errors.add(attribute, :taken, error_options)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# This module overrides validates_uniqueness_of
|
|
21
|
+
module RemoteUniquenessValidation
|
|
22
|
+
def self.included(base)
|
|
23
|
+
base.extend(ClassMethods)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
module ClassMethods
|
|
27
|
+
def validates_uniqueness_of(*attr_names)
|
|
28
|
+
validates_with RemoteUniquenessValidator, _merge_attributes(attr_names)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
require 'redcarpet'
|
|
2
|
+
require 'nokogiri'
|
|
3
|
+
require 'sanitize'
|
|
4
|
+
|
|
5
|
+
# This utility class is used to
|
|
6
|
+
# work on html text
|
|
7
|
+
#
|
|
8
|
+
# You can initialize it with html or markdown text
|
|
9
|
+
class HtmlProcessor
|
|
10
|
+
attr_reader :html, :original
|
|
11
|
+
|
|
12
|
+
#======================================
|
|
13
|
+
# Constants
|
|
14
|
+
#======================================
|
|
15
|
+
DESCRIPTION_PROCESSING_ORDER = %w( p h1 h2 h3 h4 h5 h6 )
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Define Youtube transformer for Sanitize
|
|
19
|
+
YOUTUBE_TRANSFORMER = lambda do |env|
|
|
20
|
+
node = env[:node]
|
|
21
|
+
node_name = env[:node_name]
|
|
22
|
+
|
|
23
|
+
# Don't continue if this node is already whitelisted or is not an element.
|
|
24
|
+
return if env[:is_whitelisted] || !node.element?
|
|
25
|
+
|
|
26
|
+
# Don't continue unless the node is an iframe.
|
|
27
|
+
return unless node_name == 'iframe'
|
|
28
|
+
|
|
29
|
+
# Verify that the video URL is actually a valid YouTube video URL.
|
|
30
|
+
return unless node['src'] =~ %r|\A(?:https?:)?//(?:www\.)?youtube(?:-nocookie)?\.com/|
|
|
31
|
+
|
|
32
|
+
# We're now certain that this is a YouTube embed, but we still need to run
|
|
33
|
+
# it through a special Sanitize step to ensure that no unwanted elements or
|
|
34
|
+
# attributes that don't belong in a YouTube embed can sneak in.
|
|
35
|
+
Sanitize.node!(node, {
|
|
36
|
+
:elements => %w[iframe],
|
|
37
|
+
|
|
38
|
+
:attributes => {
|
|
39
|
+
'iframe' => %w[allowfullscreen frameborder height src width]
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
# Now that we're sure that this is a valid YouTube embed and that there are
|
|
44
|
+
# no unwanted elements or attributes hidden inside it, we can tell Sanitize
|
|
45
|
+
# to whitelist the current node.
|
|
46
|
+
{:node_whitelist => [node]}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Default options for Sanitize
|
|
50
|
+
SANITIZER_OPTS = Sanitize::Config::RELAXED.merge(
|
|
51
|
+
attributes: Sanitize::Config::RELAXED[:attributes].merge(
|
|
52
|
+
'a' => %w[href hreflang name rel target],
|
|
53
|
+
),
|
|
54
|
+
transformers: YOUTUBE_TRANSFORMER
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
#======================================
|
|
58
|
+
# Methods
|
|
59
|
+
#======================================
|
|
60
|
+
def initialize(text, options = { })
|
|
61
|
+
@original = text
|
|
62
|
+
|
|
63
|
+
# Process markdown or leave original
|
|
64
|
+
if options[:format].to_s == 'markdown' && text
|
|
65
|
+
html_options = { :safe_links_only => true, :hard_wrap => true, :filter_html => false }
|
|
66
|
+
renderer_options = { :autolink => true, :no_intraemphasis => true, :fenced_code_blocks => true, :superscript => true }
|
|
67
|
+
|
|
68
|
+
renderer = Redcarpet::Markdown.new(Redcarpet::Render::HTML.new(html_options), renderer_options)
|
|
69
|
+
raw_html = renderer.render(text)
|
|
70
|
+
@html = Sanitize.fragment(raw_html, SANITIZER_OPTS)
|
|
71
|
+
else
|
|
72
|
+
@html = text
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Return a Nokogiri document based
|
|
77
|
+
# on processor html
|
|
78
|
+
def document
|
|
79
|
+
@document ||= Nokogiri::HTML(@html)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Return a description of the document
|
|
83
|
+
# by returning the first sentence of the
|
|
84
|
+
# first DESCRIPTION_PROCESSING_ORDER found
|
|
85
|
+
def description
|
|
86
|
+
# Return cached value if one
|
|
87
|
+
return @description if @description
|
|
88
|
+
|
|
89
|
+
# Parse the html document to try to find
|
|
90
|
+
# a description
|
|
91
|
+
@description = ''
|
|
92
|
+
DESCRIPTION_PROCESSING_ORDER.each do |selector|
|
|
93
|
+
elem = self.document.css(selector).select { |e| e && !e.content.blank? }.first
|
|
94
|
+
next if elem.blank? #skip if nil or empty
|
|
95
|
+
|
|
96
|
+
# Try to get the first two sentences
|
|
97
|
+
match = elem.content.match(/([^.!?]+[.!?]?)([^.!?]+[.!?]?)?/)
|
|
98
|
+
if match && match.captures.any?
|
|
99
|
+
@description = match.captures.compact.join('')
|
|
100
|
+
end
|
|
101
|
+
break if !@description.empty?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
return @description
|
|
105
|
+
end
|
|
106
|
+
end
|