pixelforce_kit 0.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 +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
|