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.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/Gemfile +4 -0
  4. data/Gemfile.lock +299 -0
  5. data/LICENSE +21 -0
  6. data/README.md +73 -0
  7. data/Rakefile +6 -0
  8. data/app/controllers/admin/api/admin_base_controller.rb +116 -0
  9. data/app/controllers/admin/api/app_versions_controller.rb +26 -0
  10. data/app/controllers/admin/api/available_modules_controller.rb +20 -0
  11. data/app/controllers/api/base_controller.rb +82 -0
  12. data/app/controllers/api/v1/health_check_controller.rb +12 -0
  13. data/app/controllers/application_controller.rb +10 -0
  14. data/app/controllers/auth/passwords_controller.rb +7 -0
  15. data/app/controllers/concerns/.keep +0 -0
  16. data/app/controllers/concerns/exception_handler.rb +57 -0
  17. data/app/controllers/concerns/request_header_handler.rb +35 -0
  18. data/app/controllers/concerns/response_handler.rb +51 -0
  19. data/app/controllers/concerns/search_params_parser.rb +35 -0
  20. data/app/controllers/pages_controller.rb +36 -0
  21. data/app/controllers/users_controller.rb +3 -0
  22. data/app/models/ahoy/event.rb +8 -0
  23. data/app/models/ahoy/visit.rb +6 -0
  24. data/app/models/application_record.rb +11 -0
  25. data/app/models/concerns/account_deletable.rb +26 -0
  26. data/app/models/concerns/assets_uploadable.rb +20 -0
  27. data/app/models/concerns/codenamable.rb +13 -0
  28. data/app/models/concerns/image_sizable.rb +58 -0
  29. data/app/models/concerns/isolationable.rb +18 -0
  30. data/app/models/concerns/searchable.rb +94 -0
  31. data/app/models/concerns/taggable.rb +57 -0
  32. data/app/models/tag.rb +13 -0
  33. data/app/models/tag_attachable.rb +6 -0
  34. data/app/models/tag_category.rb +5 -0
  35. data/bin/console +14 -0
  36. data/bin/setup +8 -0
  37. data/db/migrate/20220617021719_create_ahoy_visits_and_events.rb +67 -0
  38. data/db/migrate/20240828040425_create_tags.rb +12 -0
  39. data/db/migrate/20240828040646_create_tag_attachables.rb +9 -0
  40. data/db/migrate/20240912054241_create_tag_categories.rb +11 -0
  41. data/db/schema.rb +24 -0
  42. data/lib/pixelforce_kit/engine.rb +17 -0
  43. data/lib/pixelforce_kit/initializer/ahoy.rb +50 -0
  44. data/lib/pixelforce_kit/railtie.rb +4 -0
  45. data/lib/pixelforce_kit/recipes/capistrano_recipes/base.rb +4 -0
  46. data/lib/pixelforce_kit/recipes/capistrano_recipes/elbas.rb +10 -0
  47. data/lib/pixelforce_kit/recipes/capistrano_recipes/logrotate.rb +10 -0
  48. data/lib/pixelforce_kit/recipes/capistrano_recipes/puma.rb +46 -0
  49. data/lib/pixelforce_kit/recipes/capistrano_recipes/resque.rb +43 -0
  50. data/lib/pixelforce_kit/recipes/capistrano_recipes/resque_scheduler.rb +43 -0
  51. data/lib/pixelforce_kit/recipes/capistrano_recipes/sidekiq.rb +88 -0
  52. data/lib/pixelforce_kit/recipes/capistrano_recipes/supervisor.rb +9 -0
  53. data/lib/pixelforce_kit/recipes/capistrano_recipes/unicorn.rb +58 -0
  54. data/lib/pixelforce_kit/recipes/templates/logrotate.erb +10 -0
  55. data/lib/pixelforce_kit/recipes/templates/nginx_config.erb +32 -0
  56. data/lib/pixelforce_kit/recipes/templates/nginx_puma_config.erb +43 -0
  57. data/lib/pixelforce_kit/recipes/templates/puma.rb.erb +34 -0
  58. data/lib/pixelforce_kit/recipes/templates/puma_systemd.erb +19 -0
  59. data/lib/pixelforce_kit/recipes/templates/resque_init.erb +49 -0
  60. data/lib/pixelforce_kit/recipes/templates/resque_scheduler_init.erb +49 -0
  61. data/lib/pixelforce_kit/recipes/templates/resque_scheduler_supervisor.erb +13 -0
  62. data/lib/pixelforce_kit/recipes/templates/resque_supervisor.erb +13 -0
  63. data/lib/pixelforce_kit/recipes/templates/sidekiq_init.erb +49 -0
  64. data/lib/pixelforce_kit/recipes/templates/sidekiq_supervisor.erb +12 -0
  65. data/lib/pixelforce_kit/recipes/templates/sidekiq_systemd.erb +17 -0
  66. data/lib/pixelforce_kit/recipes/templates/supervisor.erb +19 -0
  67. data/lib/pixelforce_kit/recipes/templates/unicorn_init.erb +55 -0
  68. data/lib/pixelforce_kit/recipes/templates/unicorn_supervisor.erb +13 -0
  69. data/lib/pixelforce_kit/recipes.rb +4 -0
  70. data/lib/pixelforce_kit/spec_helper.rb +120 -0
  71. data/lib/pixelforce_kit/version.rb +3 -0
  72. data/lib/pixelforce_kit.rb +8 -0
  73. data/pixelforce_kit.gemspec +33 -0
  74. 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,3 @@
1
+ class UsersController < ApplicationController
2
+ def reset_password_success; end
3
+ end
@@ -0,0 +1,8 @@
1
+ class Ahoy::Event < ApplicationRecord
2
+ include Ahoy::QueryMethods
3
+
4
+ self.table_name = 'ahoy_events'
5
+
6
+ belongs_to :visit
7
+ belongs_to :user, optional: true
8
+ end
@@ -0,0 +1,6 @@
1
+ class Ahoy::Visit < ApplicationRecord
2
+ self.table_name = 'ahoy_visits'
3
+
4
+ has_many :events, class_name: 'Ahoy::Event'
5
+ belongs_to :user, optional: true
6
+ end
@@ -0,0 +1,11 @@
1
+ class ApplicationRecord < ActiveRecord::Base
2
+ primary_abstract_class
3
+ include Searchable
4
+ include Taggable
5
+
6
+ class << self
7
+ def attach_user_id_to_admin_activity?
8
+ column_names.include?('user_id') || name == 'User'
9
+ end
10
+ end
11
+ 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,13 @@
1
+ module Codenamable
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_create :set_code_name
6
+
7
+ validates :name, presence: true
8
+ end
9
+
10
+ def set_code_name
11
+ self.code_name = name.parameterize(separator: '_') if code_name.blank?
12
+ end
13
+ 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
@@ -0,0 +1,6 @@
1
+ class TagAttachable < ApplicationRecord
2
+ belongs_to :taggable, polymorphic: true
3
+ belongs_to :tag, class_name: 'Tag'
4
+
5
+ validates :tag_id, presence: true
6
+ end
@@ -0,0 +1,5 @@
1
+ class TagCategory < ApplicationRecord
2
+ include Codenamable
3
+
4
+ has_many :tags, dependent: :destroy, class_name: 'Tag'
5
+ 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,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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