hyrax 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +3 -6
  3. data/.dassie/.env +1 -2
  4. data/.dassie/Gemfile +7 -3
  5. data/.dassie/app/models/user.rb +0 -2
  6. data/.dassie/config/analytics.yml +12 -5
  7. data/.dassie/config/environments/development.rb +2 -0
  8. data/.dassie/config/initializers/hyrax.rb +2 -0
  9. data/.dassie/db/migrate/20210921150120_enable_uuid_extension.valkyrie_engine.rb +7 -0
  10. data/.dassie/db/migrate/20210921150121_create_orm_resources.valkyrie_engine.rb +19 -0
  11. data/.dassie/db/migrate/20210921150122_add_model_type_to_orm_resources.valkyrie_engine.rb +7 -0
  12. data/.dassie/db/migrate/20210921150123_change_model_type_to_internal_model.valkyrie_engine.rb +7 -0
  13. data/.dassie/db/migrate/20210921150124_create_path_gin_index.valkyrie_engine.rb +7 -0
  14. data/.dassie/db/migrate/20210921150125_create_internal_resource_index.valkyrie_engine.rb +7 -0
  15. data/.dassie/db/migrate/20210921150126_create_updated_at_index.valkyrie_engine.rb +7 -0
  16. data/.dassie/db/migrate/20210921150127_add_optimistic_locking_to_orm_resources.valkyrie_engine.rb +7 -0
  17. data/.dassie/db/migrate/20211130181150_create_default_administrative_set.rb +8 -0
  18. data/.dassie/db/schema.rb +20 -1
  19. data/.env +7 -4
  20. data/.github/workflows/main.yml +17 -0
  21. data/.github/workflows/release.yml +17 -0
  22. data/.gitignore +1 -0
  23. data/.regen +1 -1
  24. data/CONTAINERS.md +13 -10
  25. data/README.md +37 -0
  26. data/app/assets/javascripts/hyrax/admin/graphs.es6 +34 -37
  27. data/app/assets/javascripts/hyrax/analytics_events.js +69 -0
  28. data/app/assets/javascripts/hyrax/collapse.js +24 -0
  29. data/app/assets/javascripts/hyrax/collections.js +1 -2
  30. data/app/assets/javascripts/hyrax/ga_events.js +2 -8
  31. data/app/assets/javascripts/hyrax/reports-buttons.js +33 -0
  32. data/app/assets/javascripts/hyrax.js +2 -1
  33. data/app/assets/stylesheets/_bootstrap-default-overrides.scss +9 -0
  34. data/app/authorities/qa/authorities/collections.rb +4 -5
  35. data/app/authorities/qa/authorities/find_works.rb +1 -1
  36. data/app/controllers/concerns/hyrax/breadcrumbs_for_collection_analytics.rb +26 -0
  37. data/app/controllers/concerns/hyrax/breadcrumbs_for_works_analytics.rb +26 -0
  38. data/app/controllers/concerns/hyrax/controller.rb +22 -0
  39. data/app/controllers/hyrax/admin/analytics/analytics_controller.rb +40 -0
  40. data/app/controllers/hyrax/admin/analytics/collection_reports_controller.rb +61 -0
  41. data/app/controllers/hyrax/admin/analytics/work_reports_controller.rb +122 -0
  42. data/app/controllers/hyrax/collections_controller.rb +4 -1
  43. data/app/controllers/hyrax/dashboard/collections_controller.rb +15 -6
  44. data/app/controllers/hyrax/dashboard_controller.rb +8 -0
  45. data/app/controllers/hyrax/stats_controller.rb +3 -1
  46. data/app/forms/hyrax/forms/pcdm_collection_form.rb +3 -0
  47. data/app/indexers/hyrax/valkyrie_file_set_indexer.rb +1 -1
  48. data/app/jobs/characterize_job.rb +28 -1
  49. data/app/jobs/valkyrie_ingest_job.rb +56 -0
  50. data/app/models/concerns/hyrax/ability.rb +26 -5
  51. data/app/models/concerns/hyrax/solr_document/metadata.rb +1 -0
  52. data/app/models/file_download_stat.rb +4 -4
  53. data/app/models/hyrax/default_administrative_set.rb +42 -0
  54. data/app/models/hyrax/statistic.rb +31 -4
  55. data/app/presenters/hyrax/admin/dashboard_presenter.rb +8 -6
  56. data/app/presenters/hyrax/admin/repository_growth_presenter.rb +10 -5
  57. data/app/presenters/hyrax/admin/user_activity_presenter.rb +8 -12
  58. data/app/presenters/hyrax/file_set_presenter.rb +2 -0
  59. data/app/presenters/hyrax/menu_presenter.rb +4 -0
  60. data/app/presenters/hyrax/pcdm_member_presenter_factory.rb +1 -1
  61. data/app/presenters/hyrax/work_show_presenter.rb +5 -2
  62. data/app/presenters/hyrax/work_usage.rb +1 -0
  63. data/app/search_builders/hyrax/README.md +1 -1
  64. data/app/search_builders/hyrax/dashboard/collections_search_builder.rb +1 -1
  65. data/app/search_builders/hyrax/my/collections_search_builder.rb +1 -1
  66. data/app/services/hyrax/admin_set_create_service.rb +76 -14
  67. data/app/services/hyrax/analytics/google/events.rb +37 -0
  68. data/app/services/hyrax/analytics/google/events_daily.rb +72 -0
  69. data/app/services/hyrax/analytics/google/visits.rb +44 -0
  70. data/app/services/hyrax/analytics/google/visits_daily.rb +49 -0
  71. data/app/services/hyrax/analytics/google.rb +204 -0
  72. data/app/services/hyrax/analytics/matomo.rb +193 -0
  73. data/app/services/hyrax/analytics/results.rb +79 -0
  74. data/app/services/hyrax/analytics.rb +12 -82
  75. data/app/services/hyrax/characterization/valkyrie_characterization_service.rb +134 -0
  76. data/app/services/hyrax/collections/nested_collection_query_service.rb +8 -3
  77. data/app/services/hyrax/listeners/acl_index_listener.rb +3 -1
  78. data/app/services/hyrax/listeners/active_fedora_acl_index_listener.rb +3 -1
  79. data/app/services/hyrax/listeners/batch_notification_listener.rb +3 -1
  80. data/app/services/hyrax/listeners/file_metadata_listener.rb +19 -0
  81. data/app/services/hyrax/listeners/file_set_lifecycle_listener.rb +6 -2
  82. data/app/services/hyrax/listeners/file_set_lifecycle_notification_listener.rb +6 -2
  83. data/app/services/hyrax/listeners/member_cleanup_listener.rb +3 -0
  84. data/app/services/hyrax/listeners/metadata_index_listener.rb +9 -3
  85. data/app/services/hyrax/listeners/object_lifecycle_listener.rb +9 -3
  86. data/app/services/hyrax/listeners/proxy_deposit_listener.rb +3 -1
  87. data/app/services/hyrax/listeners/trophy_cleanup_listener.rb +3 -0
  88. data/app/services/hyrax/listeners/workflow_listener.rb +3 -1
  89. data/app/services/hyrax/listeners.rb +8 -0
  90. data/app/services/hyrax/restriction_service.rb +4 -0
  91. data/app/services/hyrax/statistics/users/over_time.rb +8 -5
  92. data/app/services/hyrax/statistics/works/over_time.rb +10 -0
  93. data/app/services/hyrax/work_uploads_handler.rb +4 -1
  94. data/app/views/hyrax/admin/analytics/_date_range_form.html.erb +11 -0
  95. data/app/views/hyrax/admin/analytics/collection_reports/_custom_range.html.erb +39 -0
  96. data/app/views/hyrax/admin/analytics/collection_reports/_monthly_summary.html.erb +48 -0
  97. data/app/views/hyrax/admin/analytics/collection_reports/_summary.html.erb +55 -0
  98. data/app/views/hyrax/admin/analytics/collection_reports/_top_collections.html.erb +55 -0
  99. data/app/views/hyrax/admin/analytics/collection_reports/index.html.erb +70 -0
  100. data/app/views/hyrax/admin/analytics/collection_reports/show.html.erb +94 -0
  101. data/app/views/hyrax/admin/analytics/work_reports/_custom_range.html.erb +43 -0
  102. data/app/views/hyrax/admin/analytics/work_reports/_monthly_summary.html.erb +35 -0
  103. data/app/views/hyrax/admin/analytics/work_reports/_summary.html.erb +60 -0
  104. data/app/views/hyrax/admin/analytics/work_reports/_top_file_set_downloads.html.erb +33 -0
  105. data/app/views/hyrax/admin/analytics/work_reports/_top_works.html.erb +40 -0
  106. data/app/views/hyrax/admin/analytics/work_reports/_work_counts.html.erb +18 -0
  107. data/app/views/hyrax/admin/analytics/work_reports/_work_files.html.erb +41 -0
  108. data/app/views/hyrax/admin/analytics/work_reports/index.html.erb +77 -0
  109. data/app/views/hyrax/admin/analytics/work_reports/show.html.erb +90 -0
  110. data/app/views/hyrax/admin/stats/show.html.erb +1 -1
  111. data/app/views/hyrax/base/_relationships_parent_row.html.erb +0 -1
  112. data/app/views/hyrax/base/show.html.erb +6 -0
  113. data/app/views/hyrax/collections/show.html.erb +4 -0
  114. data/app/views/hyrax/dashboard/_repository_growth.html.erb +5 -5
  115. data/app/views/hyrax/dashboard/_resource_type_graph.html.erb +41 -0
  116. data/app/views/hyrax/dashboard/_sidebar.html.erb +4 -1
  117. data/app/views/hyrax/dashboard/_tabs.html.erb +11 -0
  118. data/app/views/hyrax/dashboard/_user_activity.html.erb +17 -23
  119. data/app/views/hyrax/dashboard/_user_activity_graph.html.erb +55 -0
  120. data/app/views/hyrax/dashboard/_visibility_graph.html.erb +31 -0
  121. data/app/views/hyrax/dashboard/_work_type_graph.html.erb +41 -0
  122. data/app/views/hyrax/dashboard/collections/_form.html.erb +2 -1
  123. data/app/views/hyrax/dashboard/show_admin.html.erb +24 -45
  124. data/app/views/hyrax/dashboard/sidebar/_activity.html.erb +22 -0
  125. data/app/views/hyrax/file_sets/_actions.html.erb +4 -3
  126. data/app/views/hyrax/file_sets/show.html.erb +6 -0
  127. data/app/views/hyrax/my/collections/index.html.erb +1 -1
  128. data/app/views/hyrax/stats/_downloads.html.erb +18 -0
  129. data/app/views/hyrax/stats/_pageviews.html.erb +18 -0
  130. data/app/views/hyrax/stats/work.html.erb +17 -9
  131. data/app/views/layouts/_head_tag_content.html.erb +7 -2
  132. data/app/views/{_ga.html.erb → shared/_ga.html.erb} +3 -7
  133. data/app/views/shared/_matomo.html.erb +15 -0
  134. data/chart/hyrax/Chart.yaml +1 -1
  135. data/chart/hyrax/values.yaml +1 -1
  136. data/config/i18n-tasks.yml +2 -2
  137. data/config/initializers/listeners.rb +5 -5
  138. data/config/locales/hyrax.de.yml +194 -0
  139. data/config/locales/hyrax.en.yml +190 -12
  140. data/config/locales/hyrax.es.yml +194 -0
  141. data/config/locales/hyrax.fr.yml +194 -0
  142. data/config/locales/hyrax.it.yml +194 -0
  143. data/config/locales/hyrax.pt-BR.yml +194 -0
  144. data/config/locales/hyrax.zh.yml +194 -0
  145. data/config/routes.rb +4 -0
  146. data/docker-compose.yml +3 -1
  147. data/documentation/developing-your-hyrax-based-app.md +2 -2
  148. data/documentation/legacyREADME.md +1 -1
  149. data/hyrax.gemspec +3 -1
  150. data/lib/generators/hyrax/templates/config/analytics.yml +13 -7
  151. data/lib/generators/hyrax/templates/config/initializers/hyrax.rb +0 -13
  152. data/lib/generators/hyrax/templates/db/migrate/20211130181150_create_default_administrative_set.rb.erb +8 -0
  153. data/lib/generators/hyrax/work/templates/feature_spec.rb.erb +3 -1
  154. data/lib/hyrax/configuration.rb +67 -5
  155. data/lib/hyrax/engine.rb +7 -6
  156. data/lib/hyrax/publisher.rb +4 -0
  157. data/lib/hyrax/transactions/admin_set_create.rb +22 -0
  158. data/lib/hyrax/transactions/container.rb +11 -0
  159. data/lib/hyrax/version.rb +1 -1
  160. data/lib/tasks/regenerate_derivatives.rake +1 -1
  161. data/lib/wings/setup.rb +15 -0
  162. data/lib/wings/valkyrie/persister.rb +16 -0
  163. data/template.rb +1 -1
  164. data/vendor/assets/javascripts/morris/morris.min.js +1 -7
  165. data/vendor/assets/stylesheets/morris.js/0.5.1/morris.css +1 -1
  166. metadata +87 -11
  167. data/app/views/hyrax/dashboard/_repository_objects.html.erb +0 -28
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+ module Hyrax
3
+ module Analytics
4
+ module Google
5
+ module Visits
6
+ extend Legato::Model
7
+
8
+ dimensions :user_type
9
+ metrics :sessions
10
+
11
+ def self.results_array(response)
12
+ results = []
13
+ response.to_a.each do |result|
14
+ results.push([result.date.to_date, result.result.sessions.to_i])
15
+ end
16
+ Hyrax::Analytics::Results.new(results)
17
+ end
18
+
19
+ def self.new_visits(profile, start_date, end_date)
20
+ x = Visits.results(profile,
21
+ start_date: start_date,
22
+ end_date: end_date).to_a
23
+ x.first.sessions.to_i
24
+ end
25
+
26
+ def self.return_visits(profile, start_date, end_date)
27
+ x = Visits.results(profile,
28
+ start_date: start_date,
29
+ end_date: end_date).to_a
30
+ x.last.sessions.to_i
31
+ end
32
+
33
+ def self.total_visits(profile, start_date, end_date)
34
+ x = Visits.results(profile,
35
+ start_date: start_date,
36
+ end_date: end_date).to_a
37
+ new_visits = x.first.sessions.to_i
38
+ returning_visits = x.last.sessions.to_i
39
+ new_visits + returning_visits
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+ module Hyrax
3
+ module Analytics
4
+ module Google
5
+ module VisitsDaily
6
+ extend Legato::Model
7
+
8
+ dimensions :date, :user_type
9
+ metrics :sessions
10
+
11
+ filter(:returning) { |_user_type| matches(:userType, 'Returning Visitor') }
12
+ filter(:new_visit) { |_user_type| matches(:userType, 'New Visitor') }
13
+
14
+ def self.new_visits(profile, start_date, end_date)
15
+ response = VisitsDaily.results(profile,
16
+ start_date: start_date,
17
+ end_date: end_date).new_visit.to_a
18
+ dates = (start_date.to_date...end_date.to_date)
19
+ results_array(response, dates)
20
+ end
21
+
22
+ def self.return_visits(profile, start_date, end_date)
23
+ response = VisitsDaily.results(profile,
24
+ start_date: start_date,
25
+ end_date: end_date).returning.to_a
26
+ dates = (start_date.to_date...end_date.to_date)
27
+ results_array(response, dates)
28
+ end
29
+
30
+ def self.results_array(response, dates)
31
+ results = []
32
+ response.to_a.each do |result|
33
+ results.push([result.date.to_date, result.sessions.to_i])
34
+ end
35
+ new_results = []
36
+ dates.each do |date|
37
+ match = results.detect { |a, _b| a == date }
38
+ if match
39
+ new_results.push(match)
40
+ else
41
+ new_results.push([date, 0])
42
+ end
43
+ end
44
+ Hyrax::Analytics::Results.new(new_results)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+ require 'oauth2'
3
+ require 'signet/oauth_2/client'
4
+
5
+ # rubocop:disable Metrics/ModuleLength
6
+ module Hyrax
7
+ module Analytics
8
+ module Google
9
+ extend ActiveSupport::Concern
10
+ # rubocop:disable Metrics/BlockLength
11
+ class_methods do
12
+ # Loads configuration options from config/analytics.yml. Expected structure:
13
+ # `analytics:`
14
+ # ` google:`
15
+ # ` app_name: <%= ENV['GOOGLE_OAUTH_APP_NAME']`
16
+ # ` app_version: <%= ENV['GOOGLE_OAUTH_APP_VERSION']`
17
+ # ` privkey_path: <%= ENV['GOOGLE_OAUTH_PRIVATE_KEY_PATH']`
18
+ # ` privkey_secret: <%= ENV['GOOGLE_OAUTH_PRIVATE_KEY_SECRET']`
19
+ # ` client_email: <%= ENV['GOOGLE_OAUTH_CLIENT_EMAIL']`
20
+ # @return [Config]
21
+ def config
22
+ @config ||= Config.load_from_yaml
23
+ end
24
+
25
+ class Config
26
+ def self.load_from_yaml
27
+ filename = Rails.root.join('config', 'analytics.yml')
28
+ yaml = YAML.safe_load(ERB.new(File.read(filename)).result)
29
+ unless yaml
30
+ Rails.logger.error("Unable to fetch any keys from #{filename}.")
31
+ return new({})
32
+ end
33
+ config = yaml.fetch('analytics')&.fetch('google', nil)
34
+ unless config
35
+ Deprecation.warn("Deprecated analytics configuration format found. Please update config/analytics.yml.")
36
+ config = yaml.fetch('analytics')
37
+ # this has to exist here with a placeholder so it can be set in the Hyrax initializer
38
+ # it is only for backward compatibility
39
+ config['analytics_id'] = '-'
40
+ end
41
+ new config
42
+ end
43
+
44
+ REQUIRED_KEYS = %w[analytics_id app_name app_version privkey_path privkey_secret client_email].freeze
45
+
46
+ def initialize(config)
47
+ @config = config
48
+ end
49
+
50
+ # @return [Boolean] are all the required values present?
51
+ def valid?
52
+ config_keys = @config.keys
53
+ REQUIRED_KEYS.all? { |required| config_keys.include?(required) }
54
+ end
55
+
56
+ REQUIRED_KEYS.each do |key|
57
+ class_eval %{ def #{key}; @config.fetch('#{key}'); end }
58
+ end
59
+
60
+ # This method allows setting the analytics id in the initializer
61
+ # @deprecated set the analytics id in either ENV['GOOGLE_ANALYTICS_ID'] or config/analytics.yaml
62
+ def analytics_id=(value)
63
+ @config['analytics_id'] = value
64
+ end
65
+ end
66
+
67
+ # Generate an OAuth2 token for Google Analytics
68
+ # @return [OAuth2::AccessToken] An OAuth2 access token for GA
69
+ def token(scope = 'https://www.googleapis.com/auth/analytics.readonly')
70
+ access_token = auth_client(scope).fetch_access_token!
71
+ OAuth2::AccessToken.new(oauth_client, access_token['access_token'], expires_in: access_token['expires_in'])
72
+ end
73
+
74
+ def oauth_client
75
+ OAuth2::Client.new('', '', authorize_url: 'https://accounts.google.com/o/oauth2/auth',
76
+ token_url: 'https://accounts.google.com/o/oauth2/token')
77
+ end
78
+
79
+ def auth_client(scope)
80
+ raise "Private key file for Google analytics was expected at '#{config.privkey_path}', but no file was found." unless File.exist?(config.privkey_path)
81
+ private_key = File.read(config.privkey_path)
82
+ Signet::OAuth2::Client.new token_credential_uri: 'https://accounts.google.com/o/oauth2/token',
83
+ audience: 'https://accounts.google.com/o/oauth2/token',
84
+ scope: scope,
85
+ issuer: config.client_email,
86
+ signing_key: OpenSSL::PKCS12.new(private_key, config.privkey_secret).key,
87
+ sub: config.client_email
88
+ end
89
+
90
+ # Return a user object linked to a Google Analytics account
91
+ # @return [Legato::User] A user account with GA access
92
+ def user
93
+ Legato::User.new(token)
94
+ end
95
+
96
+ # Return a Google Analytics profile matching specified ID
97
+ # @ return [Legato::Management::Profile] A user profile associated with GA
98
+ def profile
99
+ return unless config.valid?
100
+ @profile = user.profiles.detect do |profile|
101
+ profile.web_property_id == config.analytics_id
102
+ end
103
+ raise 'User does not have access to this property' unless @profile
104
+ @profile
105
+ end
106
+
107
+ # rubocop:disable Metrics/MethodLength
108
+ def to_date_range(period)
109
+ case period
110
+ when "day"
111
+ start_date = Time.zone.today
112
+ end_date = Time.zone.today
113
+ when "week"
114
+ start_date = Time.zone.today - 7.days
115
+ end_date = Time.zone.today
116
+ when "month"
117
+ start_date = Time.zone.today - 1.month
118
+ end_date = Time.zone.today
119
+ when "year"
120
+ start_date = Time.zone.today - 1.year
121
+ end_date = Time.zone.today
122
+ end
123
+
124
+ [start_date, end_date]
125
+ end
126
+ # rubocop:enabl e Metrics/MethodLength
127
+
128
+ def keyword_conversion(date)
129
+ case date
130
+ when "last12"
131
+ start_date = Time.zone.today - 11.months
132
+ end_date = Time.zone.today
133
+
134
+ [start_date, end_date]
135
+ else
136
+ date.split(",")
137
+ end
138
+ end
139
+
140
+ def date_period(period, date)
141
+ if period == "range"
142
+ date.split(",")
143
+ else
144
+ to_date_range(period)
145
+ end
146
+ end
147
+
148
+ # Configure analytics_start_date in ENV file
149
+ def default_date_range
150
+ "#{Hyrax.config.analytics_start_date},#{Time.zone.today + 1.day}"
151
+ end
152
+
153
+ # The number of events by day for an action
154
+ def daily_events(action, date = default_date_range)
155
+ date = date.split(",")
156
+ EventsDaily.summary(profile, date[0], date[1], action)
157
+ end
158
+
159
+ # The number of events by day for an action and ID
160
+ def daily_events_for_id(id, action, date = default_date_range)
161
+ date = date.split(",")
162
+ EventsDaily.by_id(profile, date[0], date[1], id, action)
163
+ end
164
+
165
+ # A list of events sorted by highest event count
166
+ def top_events(action, date = default_date_range)
167
+ date = date.split(",")
168
+ Events.send('list', profile, date[0], date[1], action)
169
+ end
170
+
171
+ def unique_visitors(date = default_date_range); end
172
+
173
+ def unique_visitors_for_id(id, date = default_date_range); end
174
+
175
+ def new_visitors(period = 'month', date = default_date_range)
176
+ date = date_period(period, date)
177
+ Visits.new_visits(profile, date[0], date[1])
178
+ end
179
+
180
+ def new_visits_by_day(date = default_date_range, _period = 'day')
181
+ date = date.split(",")
182
+ VisitsDaily.new_visits(profile, date[0], date[1])
183
+ end
184
+
185
+ def returning_visitors(period = 'month', date = default_date_range)
186
+ date = date_period(period, date)
187
+ Visits.return_visits(profile, date[0], date[1])
188
+ end
189
+
190
+ def returning_visits_by_day(date = default_date_range, _period = 'day')
191
+ date = date.split(",")
192
+ VisitsDaily.return_visits(profile, date[0], date[1])
193
+ end
194
+
195
+ def total_visitors(period = 'month', date = default_date_range)
196
+ date = date_period(period, date)
197
+ Visits.total_visits(profile, date[0], date[1])
198
+ end
199
+ end
200
+ # rubocop:enable Metrics/BlockLength
201
+ end
202
+ end
203
+ end
204
+ # rubocop:enable Metrics/ModuleLength
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/ModuleLength
4
+ module Hyrax
5
+ module Analytics
6
+ module Matomo
7
+ extend ActiveSupport::Concern
8
+
9
+ # rubocop:disable Metrics/BlockLength
10
+ class_methods do
11
+ # Loads configuration options from config/analytics.yml. Expected structure:
12
+ # `analytics:`
13
+ # ` matomo:`
14
+ # ` base_url: <%= ENV['MATOMOT_BASE_URL']`
15
+ # ` site_id: <%= ENV['MATOMOT_SITE_ID']`
16
+ # ` auth_token: <%= ENV['MATOMOT_AUTH_TOKEN']`
17
+ # @return [Config]
18
+ def config
19
+ @config ||= Config.load_from_yaml
20
+ end
21
+
22
+ class Config
23
+ def self.load_from_yaml
24
+ filename = Rails.root.join('config', 'analytics.yml')
25
+ yaml = YAML.safe_load(ERB.new(File.read(filename)).result)
26
+ unless yaml
27
+ Rails.logger.error("Unable to fetch any keys from #{filename}.")
28
+ return new({})
29
+ end
30
+ new yaml.fetch('analytics')&.fetch('matomo')
31
+ end
32
+
33
+ REQUIRED_KEYS = %w[base_url site_id auth_token].freeze
34
+
35
+ def initialize(config)
36
+ @config = config
37
+ end
38
+
39
+ # @return [Boolean] are all the required values present?
40
+ def valid?
41
+ config_keys = @config.keys
42
+ REQUIRED_KEYS.all? { |required| config_keys.include?(required) }
43
+ end
44
+
45
+ REQUIRED_KEYS.each do |key|
46
+ class_eval %{ def #{key}; @config.fetch('#{key}'); end }
47
+ end
48
+ end
49
+
50
+ # Period Options = "day, week, month, year, range"
51
+ # Date Format = "2021-01-01,2021-01-31"
52
+
53
+ def default_date_range
54
+ "#{Hyrax.config.analytics_start_date},#{Time.zone.today}"
55
+ end
56
+
57
+ # Returns a total count of an event action over a date range
58
+ def total_events(action, date = default_date_range)
59
+ additional_params = { label: action }
60
+ response = api_params('Events.getAction', 'range', date, additional_params)
61
+ response&.first ? response.first["nb_events"] : 0
62
+ end
63
+
64
+ # Returns a total count of an event action for an id over a date range
65
+ def total_events_for_id(id, action, date = default_date_range)
66
+ additional_params = {
67
+ flat: 1,
68
+ label: "#{id} - #{action}"
69
+ }
70
+ response = api_params('Events.getName', 'range', date, additional_params)
71
+ response&.first ? response.first["nb_events"] : 0
72
+ end
73
+
74
+ def daily_events(action, date = default_date_range)
75
+ additional_params = { label: action }
76
+ response = api_params('Events.getAction', 'day', date, additional_params)
77
+ results_array(response, 'nb_events')
78
+ end
79
+
80
+ # Pass in an action name and an id and get back the daily count of events for that id. [date, event_count]
81
+ def daily_events_for_id(id, action, date = default_date_range)
82
+ additional_params = {
83
+ flat: 1,
84
+ label: "#{id} - #{action}"
85
+ }
86
+ response = api_params('Events.getName', 'day', date, additional_params)
87
+ results_array(response, 'nb_events')
88
+ end
89
+
90
+ # Returns a list of the total of events by id in the format of [["id", event_count]]
91
+ def top_events(action, date = default_date_range)
92
+ additional_params = {
93
+ flat: '1',
94
+ filter_column: 'Events_EventAction',
95
+ filter_pattern: action.to_s,
96
+ filter_limit: '-1',
97
+ filter_sort_column: 'nb_events',
98
+ filter_sort_order: 'desc'
99
+ }
100
+ response = api_params('Events.getName', 'range', date, additional_params)
101
+ response.map { |res| [res['Events_EventName'], res['nb_events']] }
102
+ end
103
+
104
+ # Filter the daily events by a specific action and get back the daily count of number of events.
105
+ # TODO(geezy): NOT IN USE BUT SAVING FOR POTENTIAL REFACTOR
106
+ def filter_by_action(action, response)
107
+ results = []
108
+ response.each do |result|
109
+ if result[1].empty?
110
+ results.push([result[0].to_date, 0])
111
+ elsif result[1].is_a?(Array)
112
+ result[1].each do |subtable|
113
+ results.push([result[0].to_date, subtable["nb_events"].to_i]) if subtable["label"] == action
114
+ end
115
+ end
116
+ end
117
+ Hyrax::Analytics::Results.new(results)
118
+ end
119
+
120
+ def unique_visitors(date = default_date_range)
121
+ response = api_params('Actions.get', 'day', date)
122
+ results_array(response, 'nb_uniq_pageviews')
123
+ end
124
+
125
+ def unique_visitors_for_id(url, date = default_date_range)
126
+ # additional_params = { pageUrl: url }
127
+ # response = api_params('Actions.getPageUrl', 'day', date, additional_params)
128
+ # results_array(response, 'nb_uniq_visitors')
129
+ end
130
+
131
+ def new_visitors(period = 'month', date = 'today')
132
+ response = api_params('VisitFrequency.get', period, date)
133
+ response["nb_visits_new"]
134
+ end
135
+
136
+ def new_visits_by_day(date = default_date_range, period = 'day')
137
+ result = api_params('VisitFrequency.get', period, date)
138
+ results_array(result, 'nb_visits_new')
139
+ end
140
+
141
+ def returning_visitors(period = 'month', date = 'today')
142
+ response = api_params('VisitFrequency.get', period, date)
143
+ response["nb_visits_returning"]
144
+ end
145
+
146
+ def returning_visits_by_day(date = default_date_range, period = 'day')
147
+ result = api_params('VisitFrequency.get', period, date)
148
+ results_array(result, 'nb_visits_returning')
149
+ end
150
+
151
+ def total_visitors(period = 'month', date = 'today')
152
+ response = api_params('VisitFrequency.get', period, date)
153
+ response["nb_visits_returning"].to_i + response["nb_visits_new"].to_i
154
+ end
155
+
156
+ def results_array(response, metric)
157
+ results = []
158
+ response.each do |result|
159
+ if result[1].empty?
160
+ results.push([result[0].to_date, 0])
161
+ elsif result[1].is_a?(Array)
162
+ results.push([result[0].to_date, result[1].first[metric]])
163
+ else
164
+ results.push([result[0].to_date, result[1][metric].presence || 0])
165
+ end
166
+ end
167
+ Hyrax::Analytics::Results.new(results)
168
+ end
169
+
170
+ def get(params)
171
+ response = Faraday.get(config.base_url, params)
172
+ return [] if response.status != 200
173
+ JSON.parse(response.body)
174
+ end
175
+
176
+ def api_params(method, period, date, additional_params = {})
177
+ params = {
178
+ module: "API",
179
+ idSite: config.site_id,
180
+ method: method,
181
+ period: period,
182
+ date: date,
183
+ format: "JSON",
184
+ token_auth: config.auth_token
185
+ }
186
+ params.merge!(additional_params)
187
+ get(params)
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
193
+ # rubocop:enable Metrics/ModuleLength
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+ module Hyrax
3
+ module Analytics
4
+ class Results
5
+ require 'csv'
6
+
7
+ attr_accessor :results
8
+
9
+ def initialize(results)
10
+ @results ||= results
11
+ end
12
+
13
+ def all
14
+ results.inject(0) { |sum, a| sum + a[1] }
15
+ end
16
+
17
+ def day(date = Time.zone.today)
18
+ start_date = date.at_beginning_of_day
19
+ end_date = date.at_end_of_day
20
+ range_results = []
21
+ results.each do |result|
22
+ range_results.push(result) if (start_date..end_date).cover?(result[0])
23
+ end
24
+ range_results.inject(0) { |sum, a| sum + a[1] }
25
+ end
26
+
27
+ def week(date = Time.zone.today)
28
+ start_date = date.at_beginning_of_week
29
+ end_date = date.at_end_of_week
30
+ range_results = []
31
+ results.each do |result|
32
+ range_results.push(result) if (start_date..end_date).cover?(result[0])
33
+ end
34
+ range_results.inject(0) { |sum, a| sum + a[1] }
35
+ end
36
+
37
+ def month(date = Time.zone.today)
38
+ start_date = date.at_beginning_of_month
39
+ end_date = date.at_end_of_month
40
+ range_results = []
41
+ results.each do |result|
42
+ range_results.push(result) if (start_date..end_date).cover?(result[0])
43
+ end
44
+ range_results.inject(0) { |sum, a| sum + a[1] }
45
+ end
46
+
47
+ def year(date = Time.zone.today)
48
+ start_date = date.at_beginning_of_year
49
+ end_date = date.at_end_of_year
50
+ range_results = []
51
+ results.each do |result|
52
+ range_results.push(result) if (start_date..end_date).cover?(result[0])
53
+ end
54
+ range_results.inject(0) { |sum, a| sum + a[1] }
55
+ end
56
+
57
+ def range(start_date = Time.zone.today - 1.month, end_date = Time.zone.today)
58
+ range_results = []
59
+ results.each do |result|
60
+ range_results.push(result) if (start_date..end_date).cover?(result[0])
61
+ end
62
+ range_results.inject(0) { |sum, a| sum + a[1] }
63
+ end
64
+
65
+ def to_csv
66
+ results.inject([]) { |csv, row| csv << CSV.generate_line(row) }.join("")
67
+ end
68
+
69
+ def list
70
+ results.inject([]) { |line, row| line << row }.reverse
71
+ end
72
+
73
+ def to_flot
74
+ fields = [:date, :pageviews]
75
+ results.map { |row| fields.zip(row).to_h }
76
+ end
77
+ end
78
+ end
79
+ end
@@ -1,89 +1,19 @@
1
1
  # frozen_string_literal: true
2
- require 'oauth2'
3
- require 'signet/oauth_2/client'
4
-
5
2
  module Hyrax
6
3
  module Analytics
7
- # Loads configuration options from config/analytics.yml. Expected structure:
8
- # `analytics:`
9
- # ` app_name: <%= ENV['GOOGLE_OAUTH_APP_NAME']`
10
- # ` app_version: <%= ENV['GOOGLE_OAUTH_APP_VERSION']`
11
- # ` privkey_path: <%= ENV['GOOGLE_OAUTH_PRIVATE_KEY_PATH']`
12
- # ` privkey_secret: <%= ENV['GOOGLE_OAUTH_PRIVATE_KEY_SECRET']`
13
- # ` client_email: <%= ENV['GOOGLE_OAUTH_CLIENT_EMAIL']`
14
- # @return [Config]
15
- def self.config
16
- @config ||= Config.load_from_yaml
17
- end
18
- private_class_method :config
19
-
20
- class Config
21
- def self.load_from_yaml
22
- filename = Rails.root.join('config', 'analytics.yml')
23
- yaml = YAML.safe_load(ERB.new(File.read(filename)).result)
24
- unless yaml
25
- Rails.logger.error("Unable to fetch any keys from #{filename}.")
26
- return new({})
27
- end
28
- new yaml.fetch('analytics')
29
- end
30
-
31
- REQUIRED_KEYS = %w[app_name app_version privkey_path privkey_secret client_email].freeze
32
-
33
- def initialize(config)
34
- @config = config
35
- end
36
-
37
- # @return [Boolean] are all the required values present?
38
- def valid?
39
- config_keys = @config.keys
40
- REQUIRED_KEYS.all? { |required| config_keys.include?(required) }
41
- end
42
-
43
- REQUIRED_KEYS.each do |key|
44
- class_eval %{ def #{key}; @config.fetch('#{key}'); end }
45
- end
46
- end
47
-
48
- # Generate an OAuth2 token for Google Analytics
49
- # @return [OAuth2::AccessToken] An OAuth2 access token for GA
50
- def self.token(scope = 'https://www.googleapis.com/auth/analytics.readonly')
51
- access_token = auth_client(scope).fetch_access_token!
52
- OAuth2::AccessToken.new(oauth_client, access_token['access_token'], expires_in: access_token['expires_in'])
4
+ ##
5
+ # a completely empty module to include if no parser is configured
6
+ module NullAnalyticsParser; end
7
+
8
+ def self.provider_parser
9
+ "Hyrax::Analytics::#{Hyrax.config.analytics_provider.to_s.capitalize}".constantize
10
+ rescue NameError => err
11
+ Hyrax.logger.warn("Couldn't find an Analytics provider matching "\
12
+ " #{Hyrax.config.analytics_provider}. Loading " \
13
+ " NullAnalyticsProvider.\n#{err.message}")
14
+ NullAnalyticsParser
53
15
  end
54
16
 
55
- def self.oauth_client
56
- OAuth2::Client.new('', '', authorize_url: 'https://accounts.google.com/o/oauth2/auth',
57
- token_url: 'https://accounts.google.com/o/oauth2/token')
58
- end
59
-
60
- def self.auth_client(scope)
61
- raise "Private key file for Google analytics was expected at '#{config.privkey_path}', but no file was found." unless File.exist?(config.privkey_path)
62
- private_key = File.read(config.privkey_path)
63
- Signet::OAuth2::Client.new token_credential_uri: 'https://accounts.google.com/o/oauth2/token',
64
- audience: 'https://accounts.google.com/o/oauth2/token',
65
- scope: scope,
66
- issuer: config.client_email,
67
- signing_key: OpenSSL::PKCS12.new(private_key, config.privkey_secret).key,
68
- sub: config.client_email
69
- end
70
-
71
- private_class_method :token
72
-
73
- # Return a user object linked to a Google Analytics account
74
- # @return [Legato::User] A user account wit GA access
75
- def self.user
76
- Legato::User.new(token)
77
- end
78
- private_class_method :user
79
-
80
- # Return a Google Analytics profile matching specified ID
81
- # @ return [Legato::Management::Profile] A user profile associated with GA
82
- def self.profile
83
- return unless config.valid?
84
- user.profiles.detect do |profile|
85
- profile.web_property_id == Hyrax.config.google_analytics_id
86
- end
87
- end
17
+ include provider_parser
88
18
  end
89
19
  end