pixelforce_kit 0.9

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