pixelforce_kit 0.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +299 -0
- data/LICENSE +21 -0
- data/README.md +73 -0
- data/Rakefile +6 -0
- data/app/controllers/admin/api/admin_base_controller.rb +116 -0
- data/app/controllers/admin/api/app_versions_controller.rb +26 -0
- data/app/controllers/admin/api/available_modules_controller.rb +20 -0
- data/app/controllers/api/base_controller.rb +82 -0
- data/app/controllers/api/v1/health_check_controller.rb +12 -0
- data/app/controllers/application_controller.rb +10 -0
- data/app/controllers/auth/passwords_controller.rb +7 -0
- data/app/controllers/concerns/.keep +0 -0
- data/app/controllers/concerns/exception_handler.rb +57 -0
- data/app/controllers/concerns/request_header_handler.rb +35 -0
- data/app/controllers/concerns/response_handler.rb +51 -0
- data/app/controllers/concerns/search_params_parser.rb +35 -0
- data/app/controllers/pages_controller.rb +36 -0
- data/app/controllers/users_controller.rb +3 -0
- data/app/models/ahoy/event.rb +8 -0
- data/app/models/ahoy/visit.rb +6 -0
- data/app/models/application_record.rb +11 -0
- data/app/models/concerns/account_deletable.rb +26 -0
- data/app/models/concerns/assets_uploadable.rb +20 -0
- data/app/models/concerns/codenamable.rb +13 -0
- data/app/models/concerns/image_sizable.rb +58 -0
- data/app/models/concerns/isolationable.rb +18 -0
- data/app/models/concerns/searchable.rb +94 -0
- data/app/models/concerns/taggable.rb +57 -0
- data/app/models/tag.rb +13 -0
- data/app/models/tag_attachable.rb +6 -0
- data/app/models/tag_category.rb +5 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/db/migrate/20220617021719_create_ahoy_visits_and_events.rb +67 -0
- data/db/migrate/20240828040425_create_tags.rb +12 -0
- data/db/migrate/20240828040646_create_tag_attachables.rb +9 -0
- data/db/migrate/20240912054241_create_tag_categories.rb +11 -0
- data/db/schema.rb +24 -0
- data/lib/pixelforce_kit/engine.rb +17 -0
- data/lib/pixelforce_kit/initializer/ahoy.rb +50 -0
- data/lib/pixelforce_kit/railtie.rb +4 -0
- data/lib/pixelforce_kit/recipes/capistrano_recipes/base.rb +4 -0
- data/lib/pixelforce_kit/recipes/capistrano_recipes/elbas.rb +10 -0
- data/lib/pixelforce_kit/recipes/capistrano_recipes/logrotate.rb +10 -0
- data/lib/pixelforce_kit/recipes/capistrano_recipes/puma.rb +46 -0
- data/lib/pixelforce_kit/recipes/capistrano_recipes/resque.rb +43 -0
- data/lib/pixelforce_kit/recipes/capistrano_recipes/resque_scheduler.rb +43 -0
- data/lib/pixelforce_kit/recipes/capistrano_recipes/sidekiq.rb +88 -0
- data/lib/pixelforce_kit/recipes/capistrano_recipes/supervisor.rb +9 -0
- data/lib/pixelforce_kit/recipes/capistrano_recipes/unicorn.rb +58 -0
- data/lib/pixelforce_kit/recipes/templates/logrotate.erb +10 -0
- data/lib/pixelforce_kit/recipes/templates/nginx_config.erb +32 -0
- data/lib/pixelforce_kit/recipes/templates/nginx_puma_config.erb +43 -0
- data/lib/pixelforce_kit/recipes/templates/puma.rb.erb +34 -0
- data/lib/pixelforce_kit/recipes/templates/puma_systemd.erb +19 -0
- data/lib/pixelforce_kit/recipes/templates/resque_init.erb +49 -0
- data/lib/pixelforce_kit/recipes/templates/resque_scheduler_init.erb +49 -0
- data/lib/pixelforce_kit/recipes/templates/resque_scheduler_supervisor.erb +13 -0
- data/lib/pixelforce_kit/recipes/templates/resque_supervisor.erb +13 -0
- data/lib/pixelforce_kit/recipes/templates/sidekiq_init.erb +49 -0
- data/lib/pixelforce_kit/recipes/templates/sidekiq_supervisor.erb +12 -0
- data/lib/pixelforce_kit/recipes/templates/sidekiq_systemd.erb +17 -0
- data/lib/pixelforce_kit/recipes/templates/supervisor.erb +19 -0
- data/lib/pixelforce_kit/recipes/templates/unicorn_init.erb +55 -0
- data/lib/pixelforce_kit/recipes/templates/unicorn_supervisor.erb +13 -0
- data/lib/pixelforce_kit/recipes.rb +4 -0
- data/lib/pixelforce_kit/spec_helper.rb +120 -0
- data/lib/pixelforce_kit/version.rb +3 -0
- data/lib/pixelforce_kit.rb +8 -0
- data/pixelforce_kit.gemspec +33 -0
- metadata +269 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
module ExceptionHandler
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
rescue_from StandardError do |e|
|
6
|
+
notify_error(e, airbrake_notify: true)
|
7
|
+
render_error(500, e.message)
|
8
|
+
end
|
9
|
+
|
10
|
+
rescue_from ActiveRecord::RecordNotFound do |e|
|
11
|
+
notify_error(e)
|
12
|
+
message = (e.message.present? && e.message != 'ActiveRecord::RecordNotFound') ? e.message : I18n.t('errors.record_not_found')
|
13
|
+
render_error(404, message)
|
14
|
+
end
|
15
|
+
|
16
|
+
rescue_from ActiveRecord::RecordInvalid do |e|
|
17
|
+
notify_error(e)
|
18
|
+
message = (e.message.present? && e.message != 'ActiveRecord::RecordInvalid') ? e.message : I18n.t('errors.record_invalid')
|
19
|
+
render_error(422, message)
|
20
|
+
end
|
21
|
+
|
22
|
+
rescue_from ActionController::ParameterMissing do |e|
|
23
|
+
notify_error(e)
|
24
|
+
render_error(404, e.message)
|
25
|
+
end
|
26
|
+
|
27
|
+
rescue_from JSON::ParserError do |e|
|
28
|
+
notify_error(e)
|
29
|
+
render_error(404, I18n.t('errors.record_invalid'))
|
30
|
+
end
|
31
|
+
|
32
|
+
rescue_from ActiveRecord::RecordNotUnique do |e|
|
33
|
+
notify_error(e)
|
34
|
+
message = (e.message.present? && e.message != 'ActiveRecord::RecordNotUnique') ? e.message : I18n.t('errors.record_not_unique')
|
35
|
+
render_error(404, message)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def notify_error(error, airbrake_notify: false)
|
41
|
+
airbrake_notify(error) if airbrake_notify
|
42
|
+
raise_error(error)
|
43
|
+
end
|
44
|
+
|
45
|
+
def raise_error(error)
|
46
|
+
if Rails.env.test? || Rails.env.development?
|
47
|
+
raise error
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def airbrake_notify(error)
|
52
|
+
if Rails.env.production? || Rails.env.staging?
|
53
|
+
Airbrake.notify(error)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module RequestHeaderHandler
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
def cloudfront_headers
|
5
|
+
@cloudfront_headers ||= {
|
6
|
+
cloudfront_viewer_address: request.headers['CloudFront-Viewer-Address'],
|
7
|
+
cloudfront_viewer_country: request.headers['CloudFront-Viewer-Country'],
|
8
|
+
cloudfront_is_ios_viewer: request.headers['CloudFront-Is-Ios-Viewer'],
|
9
|
+
cloudfront_is_tablet_viewer: request.headers['CloudFront-Is-Tablet-Viewer'],
|
10
|
+
cloudfront_viewer_country_name: request.headers['CloudFront-Viewer-Country-Name'],
|
11
|
+
cloudfront_is_mobile_viewer: request.headers['CloudFront-Is-Mobile-Viewer'],
|
12
|
+
cloudfront_is_smarttv_viewer: request.headers['CloudFront-Is-Smarttv-Viewer'],
|
13
|
+
cloudfront_viewer_country_region: request.headers['CloudFront-Viewer-Country-Region'],
|
14
|
+
cloudfront_is_android_viewer: request.headers['CloudFront-Is-Android-Viewer'],
|
15
|
+
cloudfront_viewer_country_region_name: request.headers['CloudFront-Viewer-Country-Region-Name'],
|
16
|
+
cloudfront_viewer_city: request.headers['CloudFront-Viewer-City'],
|
17
|
+
cloudfront_viewer_latitude: request.headers['CloudFront-Viewer-Latitude'],
|
18
|
+
cloudfront_viewer_longitude: request.headers['CloudFront-Viewer-Longitude'],
|
19
|
+
cloudfront_viewer_postal_code: request.headers['CloudFront-Viewer-Postal-Code'],
|
20
|
+
cloudfront_is_desktop_viewer: request.headers['CloudFront-Is-Desktop-Viewer']
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def device_headers
|
25
|
+
@device_headers ||= {
|
26
|
+
app_version: request.headers['HTTP_X_APP_VERSION'],
|
27
|
+
platform: request.headers['HTTP_X_PLATFORM'],
|
28
|
+
device_model: request.headers['HTTP_X_DEVICE_MODEL'],
|
29
|
+
os_version: request.headers['HTTP_X_OS_VERSION'],
|
30
|
+
app_build_version: request.headers['HTTP_X_APP_BUILD_VERSION'],
|
31
|
+
device_token: request.headers['HTTP_X_DEVICE_TOKEN'],
|
32
|
+
user_timezone: request.headers['HTTP_X_USER_TIMEZONE']
|
33
|
+
}
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module ResponseHandler
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
before_action :config_default_response_settings
|
6
|
+
layout false
|
7
|
+
end
|
8
|
+
|
9
|
+
def config_default_response_settings
|
10
|
+
set_response_format
|
11
|
+
end
|
12
|
+
|
13
|
+
def set_response_format
|
14
|
+
if request.format.to_s != 'text/csv'
|
15
|
+
request.format = :json
|
16
|
+
self.content_type = 'application/json'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def render_success
|
21
|
+
render json: {}, status: :ok
|
22
|
+
end
|
23
|
+
|
24
|
+
def render_no_content
|
25
|
+
render json: {}, status: :no_content
|
26
|
+
end
|
27
|
+
|
28
|
+
def render_error(status, message, errors = nil, source: nil, meta: {}, admin_server_error: false)
|
29
|
+
response = {
|
30
|
+
'status' => 'error',
|
31
|
+
'source' => source,
|
32
|
+
'errors' => {},
|
33
|
+
'meta' => meta
|
34
|
+
}
|
35
|
+
|
36
|
+
if errors.is_a?(ActiveModel::Errors)
|
37
|
+
errors.each do |error|
|
38
|
+
attribute = error.attribute.to_s
|
39
|
+
error_message = error.message
|
40
|
+
response['errors'][attribute] ||= []
|
41
|
+
response['errors'][attribute] << error_message
|
42
|
+
end
|
43
|
+
elsif errors.is_a?(Hash)
|
44
|
+
response['errors'] = errors
|
45
|
+
else
|
46
|
+
response['errors'] = admin_server_error ? { 'root' => { 'serverError' => { message: message } } } : { 'server' => [message] }
|
47
|
+
end
|
48
|
+
|
49
|
+
render json: response, status: status
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module SearchParamsParser
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
def get_search_params
|
5
|
+
return @get_search_params if defined?(@get_search_params)
|
6
|
+
|
7
|
+
@get_search_params = search_params(search_filter_params)
|
8
|
+
end
|
9
|
+
|
10
|
+
def search_filter_params
|
11
|
+
return {} if params[:filter].blank?
|
12
|
+
|
13
|
+
@search_filter_params ||= additional_search_filter_params
|
14
|
+
@search_filter_params
|
15
|
+
end
|
16
|
+
|
17
|
+
def additional_search_filter_params
|
18
|
+
params.require(:filter).permit!
|
19
|
+
end
|
20
|
+
|
21
|
+
def search_params(extra_params = {})
|
22
|
+
id = keywords&.first&.delete('%')&.to_i
|
23
|
+
@search_params = if id && !id.zero?
|
24
|
+
extra_params.delete('keyword')
|
25
|
+
{
|
26
|
+
id: id
|
27
|
+
}
|
28
|
+
else
|
29
|
+
{
|
30
|
+
keyword: keywords
|
31
|
+
}
|
32
|
+
end
|
33
|
+
@search_params.merge!(extra_params)
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class PagesController < ApplicationController
|
2
|
+
layout 'rails_template'
|
3
|
+
|
4
|
+
def index; end
|
5
|
+
|
6
|
+
def fonts_page; end
|
7
|
+
|
8
|
+
def spinners_page; end
|
9
|
+
|
10
|
+
def animations_page; end
|
11
|
+
|
12
|
+
def global_notice_page
|
13
|
+
flash.now[:notice] = 'Here is a notice flash message!'
|
14
|
+
flash.now[:success] = 'Here is a success flash message!'
|
15
|
+
flash.now[:warning] = 'Here is a warning flash message!'
|
16
|
+
flash.now[:error] = 'Here is an error flash message!'
|
17
|
+
end
|
18
|
+
|
19
|
+
def new
|
20
|
+
@template_model = TemplateModel.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def create
|
24
|
+
@template_model = TemplateModel.new(template_model_params)
|
25
|
+
if @template_model.save
|
26
|
+
flash[:success] = 'Template has been created successfully.'
|
27
|
+
redirect_to new_template_model_path
|
28
|
+
else
|
29
|
+
render :new
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def template_model_params
|
34
|
+
params.require(:template_model).permit(:first_name, :last_name, :email, :dob, :gender, :message, :level, :active)
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module AccountDeletable
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
def soft_delete!
|
6
|
+
ActiveRecord::Base.transaction do
|
7
|
+
# raise error if user has any active subscription
|
8
|
+
if try(:payment_subscriptions)&.active&.exists?
|
9
|
+
raise StandardError, I18n.t('errors.user_deletion_error')
|
10
|
+
end
|
11
|
+
|
12
|
+
user_devices.destroy_all
|
13
|
+
update_columns(
|
14
|
+
deleted_at: Time.zone.now,
|
15
|
+
tokens: nil,
|
16
|
+
email: nil,
|
17
|
+
uid: SecureRandom.uuid,
|
18
|
+
encrypted_password: '',
|
19
|
+
first_name: nil,
|
20
|
+
last_name: nil,
|
21
|
+
email_before_deleted: email
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module AssetsUploadable
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
class_methods do
|
5
|
+
def has_uploadable(*fields, **_options)
|
6
|
+
fields.each do |field|
|
7
|
+
attr_accessor "#{field}_blob_id"
|
8
|
+
|
9
|
+
after_save do
|
10
|
+
blob_id = send("#{field}_blob_id")
|
11
|
+
if blob_id.present?
|
12
|
+
blob = ActiveStorage::Blob.find_signed(blob_id)
|
13
|
+
send("#{field}_blob_id=", nil)
|
14
|
+
send(field).attach(blob)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module ImageSizable
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
class_methods do
|
5
|
+
def has_sizable(*fields, **_options)
|
6
|
+
fields.each do |field|
|
7
|
+
define_method("#{field}_urls") do |*selected_sizes|
|
8
|
+
attachment = send(field)
|
9
|
+
return {} unless attachment.attached?
|
10
|
+
|
11
|
+
generate_urls(attachment, *selected_sizes)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def generate_urls(blob, *selected_sizes)
|
18
|
+
selected_sizes = default_sizes if selected_sizes.empty?
|
19
|
+
selected_sizes.map!(&:to_s)
|
20
|
+
|
21
|
+
urls = {}
|
22
|
+
if blob.image?
|
23
|
+
selected_sizes.each do |size|
|
24
|
+
if size == 'original'
|
25
|
+
urls[size] = Rails.application.routes.url_helpers.rails_blob_url(blob)
|
26
|
+
else
|
27
|
+
variant = blob.variant(resize_to_limit: size_detail(size))
|
28
|
+
urls[size] = Rails.application.routes.url_helpers.rails_representation_url(variant)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
else
|
32
|
+
urls['original'] = Rails.application.routes.url_helpers.rails_blob_url(blob)
|
33
|
+
end
|
34
|
+
urls
|
35
|
+
end
|
36
|
+
|
37
|
+
def size_detail(size)
|
38
|
+
case size
|
39
|
+
when 'small'
|
40
|
+
[600, 600]
|
41
|
+
when 'medium'
|
42
|
+
[1200, 1200]
|
43
|
+
when 'large'
|
44
|
+
[1920, 1920]
|
45
|
+
else
|
46
|
+
'original'
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def default_sizes
|
51
|
+
@default_sizes ||= %w[
|
52
|
+
original
|
53
|
+
small
|
54
|
+
medium
|
55
|
+
large
|
56
|
+
]
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Isolationable
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
def run_in_transaction(retry_times: 10, level: :repeatable_read, &block)
|
5
|
+
if Rails.env.test?
|
6
|
+
ActiveRecord::Base.transaction(&block)
|
7
|
+
else
|
8
|
+
ActiveRecord::Base.transaction isolation: level, &block
|
9
|
+
end
|
10
|
+
rescue ActiveRecord::SerializationFailure => e
|
11
|
+
retry_times -= 1
|
12
|
+
if retry_times.positive?
|
13
|
+
retry
|
14
|
+
else
|
15
|
+
raise ActiveRecord::SerializationFailure
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Searchable
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
class_methods do
|
5
|
+
def search(params = {}, search_order: { id: :desc }, scope: nil, &custom_filter_block)
|
6
|
+
params = {} if params.nil?
|
7
|
+
validate_search_params(params)
|
8
|
+
validate_search_order(search_order)
|
9
|
+
results = scope ||= all
|
10
|
+
|
11
|
+
results = apply_keyword_search(results, params[:keyword]) if params[:keyword].present?
|
12
|
+
results = apply_attribute_filters(results, params)
|
13
|
+
results = apply_custom_filters(results, params, &custom_filter_block)
|
14
|
+
results = none if results.nil?
|
15
|
+
apply_ordering(results, search_order)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def default_attributes
|
21
|
+
%i[id created_at updated_at]
|
22
|
+
end
|
23
|
+
|
24
|
+
def validate_search_params(params)
|
25
|
+
valid_keys = searchable_attributes + %i[keyword order] + default_attributes
|
26
|
+
invalid_keys = params.keys.map(&:to_sym) - valid_keys
|
27
|
+
if invalid_keys.any?
|
28
|
+
raise ArgumentError, "Invalid search parameters: #{invalid_keys.join(', ')}, check searchable_attributes"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def validate_search_order(search_order)
|
33
|
+
valid_order_keys = searchable_orders
|
34
|
+
invalid_keys = search_order.keys.map(&:to_sym) - valid_order_keys
|
35
|
+
|
36
|
+
if invalid_keys.any?
|
37
|
+
raise ArgumentError, "Invalid order: #{invalid_keys.join(', ')}, check searchable_orders"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def apply_keyword_search(results, keyword)
|
42
|
+
keyword = ["%#{keyword}%"] if keyword.is_a?(String)
|
43
|
+
keyword_fields = searchable_keyword_fields
|
44
|
+
|
45
|
+
if keyword_fields.any?
|
46
|
+
string_fields = keyword_fields.select { |field| column_for_attribute(field).type == :string }
|
47
|
+
conditions = string_fields.map { |field| "#{field} ILIKE ANY ( array[:keyword] )" }.join(' OR ')
|
48
|
+
results = results.where(conditions, keyword:)
|
49
|
+
else
|
50
|
+
results
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def apply_attribute_filters(results, params)
|
55
|
+
(searchable_attributes + default_attributes).each do |attr|
|
56
|
+
value = params[attr] || params[attr.to_s]
|
57
|
+
|
58
|
+
next if value.blank?
|
59
|
+
results = results.where(attr => value)
|
60
|
+
end
|
61
|
+
results
|
62
|
+
end
|
63
|
+
|
64
|
+
def apply_custom_filters(results, params)
|
65
|
+
if block_given?
|
66
|
+
yield(results, params)
|
67
|
+
else
|
68
|
+
results
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def apply_ordering(results, search_order)
|
73
|
+
if search_order.present?
|
74
|
+
results.order(search_order)
|
75
|
+
else
|
76
|
+
results.order(id: :desc)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def searchable_attributes
|
81
|
+
# Override this method in the including class to specify which attributes are searchable
|
82
|
+
column_names.map(&:to_sym) & %i[name code_name id user_id transaction_id payment_subscription_id]
|
83
|
+
end
|
84
|
+
|
85
|
+
def searchable_orders
|
86
|
+
%i[id created_at updated_at]
|
87
|
+
end
|
88
|
+
|
89
|
+
def searchable_keyword_fields
|
90
|
+
# Override this method in the including class to specify which fields should be searched for keywords
|
91
|
+
column_names.map(&:to_sym) & %i[name code_name id user_id transaction_id payment_subscription_id identifier]
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Taggable
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
# tag_filter = {
|
5
|
+
# groups: [
|
6
|
+
# { category: 'workout' },
|
7
|
+
# { code_names: ['10-minutes', '20-minutes'] }
|
8
|
+
# ],
|
9
|
+
# logical_operator: 'and'
|
10
|
+
# }
|
11
|
+
|
12
|
+
included do
|
13
|
+
has_many :tag_attachables, as: :taggable, dependent: :destroy, inverse_of: :taggable, class_name: 'TagAttachable'
|
14
|
+
has_many :tags, through: :tag_attachables, source: :tag, class_name: 'Tag'
|
15
|
+
|
16
|
+
accepts_nested_attributes_for :tag_attachables, allow_destroy: true
|
17
|
+
|
18
|
+
scope :tagged_with, lambda { |tag_filter|
|
19
|
+
groups = tag_filter[:groups]
|
20
|
+
logical_operator = tag_filter[:logical_operator]&.downcase
|
21
|
+
|
22
|
+
queries = groups.map do |group|
|
23
|
+
if group[:code_names]
|
24
|
+
subquery = joins(:tags).where(tags: { code_name: group[:code_names] })
|
25
|
+
.group("#{table_name}.id")
|
26
|
+
.having('COUNT(DISTINCT tags.id) = ?', group[:code_names].size)
|
27
|
+
elsif group[:tag_category_id]
|
28
|
+
subquery = joins(:tags).where(tags: { tag_category_id: group[:tag_category_id] })
|
29
|
+
.group("#{table_name}.id")
|
30
|
+
else
|
31
|
+
next
|
32
|
+
end
|
33
|
+
|
34
|
+
subquery
|
35
|
+
end.compact
|
36
|
+
|
37
|
+
if queries.empty?
|
38
|
+
all
|
39
|
+
else
|
40
|
+
if logical_operator == 'or'
|
41
|
+
combined_query = queries.map(&:to_sql).join(' UNION ')
|
42
|
+
combined_query = from("(#{combined_query}) AS #{table_name}")
|
43
|
+
else
|
44
|
+
combined_query = queries.reduce do |combined, query|
|
45
|
+
if combined.nil?
|
46
|
+
query
|
47
|
+
else
|
48
|
+
combined.where(id: query.select(:id))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
combined_query.distinct
|
54
|
+
end
|
55
|
+
}
|
56
|
+
end
|
57
|
+
end
|
data/app/models/tag.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
class Tag < ApplicationRecord
|
2
|
+
include Codenamable
|
3
|
+
|
4
|
+
has_many :tag_attachables, dependent: :destroy, class_name: 'TagAttachable'
|
5
|
+
has_many :taggables, through: :tag_attachables
|
6
|
+
belongs_to :tag_category, class_name: 'TagCategory'
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def searchable_attributes
|
10
|
+
%i[name tag_category_id]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "pixelforce_kit"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
class CreateAhoyVisitsAndEvents < ActiveRecord::Migration[6.1]
|
2
|
+
def change
|
3
|
+
create_table :ahoy_visits do |t|
|
4
|
+
t.string :visit_token
|
5
|
+
t.string :visitor_token
|
6
|
+
|
7
|
+
# the rest are recommended but optional
|
8
|
+
# simply remove any you don't want
|
9
|
+
|
10
|
+
# user
|
11
|
+
t.references :user
|
12
|
+
|
13
|
+
# standard
|
14
|
+
t.string :ip
|
15
|
+
t.text :user_agent
|
16
|
+
t.text :referrer
|
17
|
+
t.string :referring_domain
|
18
|
+
t.text :landing_page
|
19
|
+
|
20
|
+
# technology
|
21
|
+
t.string :browser
|
22
|
+
t.string :os
|
23
|
+
t.string :device_type
|
24
|
+
|
25
|
+
# location
|
26
|
+
t.string :country
|
27
|
+
t.string :region
|
28
|
+
t.string :city
|
29
|
+
t.float :latitude
|
30
|
+
t.float :longitude
|
31
|
+
|
32
|
+
# utm parameters
|
33
|
+
t.string :utm_source
|
34
|
+
t.string :utm_medium
|
35
|
+
t.string :utm_term
|
36
|
+
t.string :utm_content
|
37
|
+
t.string :utm_campaign
|
38
|
+
|
39
|
+
# native apps
|
40
|
+
t.string :app_version
|
41
|
+
t.string :os_version
|
42
|
+
t.string :platform
|
43
|
+
|
44
|
+
t.datetime :started_at
|
45
|
+
end
|
46
|
+
|
47
|
+
add_index :ahoy_visits, :visit_token, unique: true
|
48
|
+
|
49
|
+
create_table :ahoy_events do |t|
|
50
|
+
t.references :visit
|
51
|
+
t.bigint :user_id
|
52
|
+
t.bigint :admin_action_on_user_id
|
53
|
+
|
54
|
+
t.string :user_type
|
55
|
+
t.string :name
|
56
|
+
t.jsonb :properties
|
57
|
+
t.jsonb :cloudfront_headers, default: {}
|
58
|
+
t.datetime :time
|
59
|
+
end
|
60
|
+
|
61
|
+
add_index :ahoy_events, [:name, :time]
|
62
|
+
add_index :ahoy_events, :properties, using: :gin, opclass: :jsonb_path_ops
|
63
|
+
add_index :ahoy_events, %i[user_id user_type]
|
64
|
+
add_index :ahoy_events, :cloudfront_headers, using: :gin
|
65
|
+
add_index :ahoy_events, :admin_action_on_user_id
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class CreateTags < ActiveRecord::Migration[7.1]
|
2
|
+
def change
|
3
|
+
create_table :tags do |t|
|
4
|
+
t.string :name
|
5
|
+
t.string :code_name
|
6
|
+
t.references :tag_category
|
7
|
+
t.timestamps
|
8
|
+
end
|
9
|
+
|
10
|
+
add_index :tags, [:code_name, :tag_category_id], unique: true
|
11
|
+
end
|
12
|
+
end
|