hyrax 5.0.1 → 5.0.3

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 (247) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +7 -176
  3. data/.dassie/.env +8 -3
  4. data/.dassie/Gemfile +13 -2
  5. data/.dassie/app/controllers/hyrax/generic_work_resources_controller.rb +17 -0
  6. data/.dassie/app/controllers/hyrax/generic_works_controller.rb +7 -1
  7. data/.dassie/app/forms/generic_work_resource_form.rb +20 -0
  8. data/.dassie/app/indexers/generic_work_resource_indexer.rb +16 -0
  9. data/.dassie/app/models/admin_set_resource.rb +9 -0
  10. data/.dassie/app/models/collection_resource.rb +2 -0
  11. data/.dassie/app/models/file_set.rb +2 -0
  12. data/.dassie/app/models/generic_work_resource.rb +10 -0
  13. data/.dassie/app/views/hyrax/generic_work_resources/_generic_work_resource.html.erb +2 -0
  14. data/.dassie/config/analytics.yml +6 -1
  15. data/.dassie/config/application.rb +24 -0
  16. data/.dassie/config/initializers/hyrax.rb +13 -3
  17. data/.dassie/config/initializers/wings.rb +109 -0
  18. data/.dassie/config/metadata/generic_work_resource.yaml +22 -0
  19. data/.dassie/config/valkyrie_index.yml +4 -10
  20. data/.dassie/db/migrate/20240506070809_valkyrie_id_to_string.rb +5 -0
  21. data/.dassie/db/schema.rb +2 -2
  22. data/.dassie/spec/indexers/generic_work_resource_indexer_spec.rb +13 -0
  23. data/.dassie/spec/models/generic_work_resource_spec.rb +12 -0
  24. data/.dassie/spec/views/generic_work_resources/_generic_work_resource.html.erb_spec.rb +7 -0
  25. data/.dockerignore +6 -4
  26. data/.github/release.yml +3 -0
  27. data/.github/workflows/lint-build-test.yml +130 -0
  28. data/.github/workflows/test-results.yml +40 -0
  29. data/.koppie/.env +7 -5
  30. data/.koppie/Gemfile +12 -1
  31. data/.koppie/config/analytics.yml +6 -1
  32. data/.koppie/config/environments/test.rb +2 -0
  33. data/.koppie/config/initializers/1_valkyrie.rb +6 -2
  34. data/.koppie/config/solr.yml +1 -1
  35. data/.regen +1 -1
  36. data/.rubocop.yml +5 -0
  37. data/Dockerfile +16 -36
  38. data/Gemfile +2 -0
  39. data/app/assets/javascripts/hydra-editor/field_manager.es6 +187 -0
  40. data/app/assets/javascripts/hyrax/analytics_events.js +48 -24
  41. data/app/assets/javascripts/hyrax/collapse.js +4 -4
  42. data/app/assets/javascripts/hyrax/file_manager/save_manager.es6 +2 -0
  43. data/app/assets/javascripts/hyrax/search.js +2 -3
  44. data/app/assets/javascripts/hyrax/select_work_type.es6 +3 -1
  45. data/app/assets/javascripts/hyrax/uploader.js +20 -18
  46. data/app/assets/javascripts/hyrax.js +1 -0
  47. data/app/assets/stylesheets/_bootstrap-default-overrides.scss +4 -0
  48. data/app/assets/stylesheets/hyrax/_card.scss +4 -0
  49. data/app/assets/stylesheets/hyrax/_catalog.scss +21 -0
  50. data/app/assets/stylesheets/hyrax/_collections.scss +1 -1
  51. data/app/assets/stylesheets/hyrax/_facets.scss +15 -3
  52. data/app/assets/stylesheets/hyrax/_featured.scss +4 -0
  53. data/app/assets/stylesheets/hyrax/_form.scss +4 -0
  54. data/app/assets/stylesheets/hyrax/_forms.scss +2 -1
  55. data/app/assets/stylesheets/hyrax/_nestable.scss +9 -8
  56. data/app/assets/stylesheets/hyrax/_select_work_type.scss +12 -0
  57. data/app/assets/stylesheets/hyrax/_styles.scss +4 -0
  58. data/app/assets/stylesheets/hyrax/_work-show.scss +3 -0
  59. data/app/controllers/concerns/hyrax/singular_subresource_controller.rb +7 -2
  60. data/app/controllers/concerns/hyrax/valkyrie_downloads_controller_behavior.rb +11 -2
  61. data/app/controllers/concerns/hyrax/works_controller_behavior.rb +9 -2
  62. data/app/controllers/hyrax/admin/analytics/collection_reports_controller.rb +2 -2
  63. data/app/controllers/hyrax/admin/analytics/work_reports_controller.rb +7 -8
  64. data/app/controllers/hyrax/dashboard/collections_controller.rb +2 -1
  65. data/app/controllers/hyrax/downloads_controller.rb +24 -3
  66. data/app/controllers/hyrax/file_sets_controller.rb +32 -6
  67. data/app/controllers/hyrax/my/works_controller.rb +20 -0
  68. data/app/controllers/hyrax/stats_controller.rb +1 -1
  69. data/app/controllers/hyrax/uploads_controller.rb +28 -2
  70. data/app/forms/hyrax/forms/admin/appearance.rb +1 -1
  71. data/app/forms/hyrax/forms/admin/collection_type_form.rb +1 -7
  72. data/app/forms/hyrax/forms/pcdm_collection_form.rb +9 -0
  73. data/app/forms/hyrax/forms/work_embargo_form.rb +6 -0
  74. data/app/forms/hyrax/forms/work_lease_form.rb +6 -0
  75. data/app/indexers/concerns/hyrax/location_indexer.rb +2 -2
  76. data/app/indexers/hyrax/indexers/file_set_indexer.rb +4 -0
  77. data/app/indexers/hyrax/indexers/resource_indexer.rb +1 -0
  78. data/app/indexers/hyrax/valkyrie_indexer.rb +3 -5
  79. data/app/jobs/migrate_files_to_valkyrie_job.rb +109 -0
  80. data/app/jobs/migrate_resources_job.rb +34 -0
  81. data/app/jobs/valkyrie_create_derivatives_job.rb +2 -1
  82. data/app/models/admin_set.rb +1 -0
  83. data/app/models/concerns/hyrax/ar_resource.rb +104 -0
  84. data/app/models/concerns/hyrax/solr_document/ordered_members.rb +2 -1
  85. data/app/models/concerns/hyrax/solr_document_behavior.rb +13 -2
  86. data/app/models/concerns/hyrax/valkyrie_lazy_migration.rb +82 -0
  87. data/app/models/file_download_stat.rb +1 -1
  88. data/app/models/file_view_stat.rb +1 -1
  89. data/app/models/hyrax/collection_type.rb +12 -4
  90. data/app/models/hyrax/file_metadata.rb +19 -0
  91. data/app/models/hyrax/file_set.rb +25 -0
  92. data/app/models/hyrax/model_registry.rb +2 -3
  93. data/app/models/hyrax/resource.rb +5 -0
  94. data/app/models/hyrax/statistic.rb +12 -37
  95. data/app/presenters/hyrax/file_set_presenter.rb +2 -1
  96. data/app/presenters/hyrax/file_usage.rb +3 -3
  97. data/app/presenters/hyrax/iiif_manifest_presenter.rb +2 -1
  98. data/app/presenters/hyrax/member_presenter_factory.rb +7 -1
  99. data/app/presenters/hyrax/menu_presenter.rb +1 -1
  100. data/app/presenters/hyrax/stats_usage_presenter.rb +2 -1
  101. data/app/presenters/hyrax/work_show_presenter.rb +13 -17
  102. data/app/presenters/hyrax/work_usage.rb +5 -2
  103. data/app/search_builders/hyrax/expired_embargo_search_builder.rb +7 -1
  104. data/app/search_builders/hyrax/expired_lease_search_builder.rb +7 -1
  105. data/app/search_builders/hyrax/filter_by_type.rb +1 -3
  106. data/app/search_builders/hyrax/valkyrie_abstract_type_relation.rb +7 -2
  107. data/app/services/hyrax/access_control_list.rb +1 -1
  108. data/app/services/hyrax/admin_set_create_service.rb +16 -5
  109. data/app/services/hyrax/admin_set_service.rb +2 -1
  110. data/app/services/hyrax/analytics/ga4/base.rb +96 -0
  111. data/app/services/hyrax/analytics/ga4/events.rb +25 -0
  112. data/app/services/hyrax/analytics/ga4/events_daily.rb +36 -0
  113. data/app/services/hyrax/analytics/ga4/visits.rb +33 -0
  114. data/app/services/hyrax/analytics/ga4/visits_daily.rb +24 -0
  115. data/app/services/hyrax/analytics/ga4.rb +204 -0
  116. data/app/services/hyrax/analytics/google.rb +16 -2
  117. data/app/services/hyrax/analytics/matomo.rb +16 -3
  118. data/app/services/hyrax/analytics/results.rb +6 -0
  119. data/app/services/hyrax/custom_queries/find_access_control.rb +1 -1
  120. data/app/services/hyrax/custom_queries/find_by_date_range.rb +6 -23
  121. data/app/services/hyrax/custom_queries/find_collections_by_type.rb +2 -2
  122. data/app/services/hyrax/custom_queries/find_count_by.rb +3 -31
  123. data/app/services/hyrax/custom_queries/find_file_metadata.rb +2 -2
  124. data/app/services/hyrax/custom_queries/find_models_by_access.rb +5 -27
  125. data/app/services/hyrax/embargo_manager.rb +2 -1
  126. data/app/services/hyrax/listeners/file_listener.rb +2 -2
  127. data/app/services/hyrax/lock_manager.rb +6 -6
  128. data/app/services/hyrax/lockable.rb +4 -3
  129. data/app/services/hyrax/simple_schema_loader.rb +1 -1
  130. data/app/services/hyrax/solr_service.rb +22 -8
  131. data/app/services/hyrax/statistics/query_service.rb +1 -1
  132. data/app/services/hyrax/statistics/works/over_time.rb +1 -1
  133. data/app/services/hyrax/thumbnail_path_service.rb +2 -0
  134. data/app/services/hyrax/user_stat_importer.rb +5 -5
  135. data/app/services/hyrax/valkyrie_upload.rb +9 -7
  136. data/app/services/hyrax/versioning_service.rb +10 -2
  137. data/app/services/hyrax/work_query_service.rb +2 -2
  138. data/app/services/migrate_resource_service.rb +55 -0
  139. data/app/views/_controls.html.erb +5 -5
  140. data/app/views/_masthead.html.erb +1 -1
  141. data/app/views/catalog/_search_form.html.erb +9 -16
  142. data/app/views/catalog/_thumbnail_list_collection.html.erb +1 -1
  143. data/app/views/catalog/_thumbnail_list_default.html.erb +2 -2
  144. data/app/views/hyrax/admin/analytics/collection_reports/index.html.erb +4 -4
  145. data/app/views/hyrax/admin/analytics/work_reports/index.html.erb +1 -1
  146. data/app/views/hyrax/admin/collection_types/_form.html.erb +4 -4
  147. data/app/views/hyrax/admin/collection_types/index.html.erb +1 -1
  148. data/app/views/hyrax/admin/features/index.html.erb +1 -1
  149. data/app/views/hyrax/base/_file_manager_actions.html.erb +1 -1
  150. data/app/views/hyrax/base/_file_manager_member.html.erb +7 -4
  151. data/app/views/hyrax/base/_file_manager_thumbnail.html.erb +1 -1
  152. data/app/views/hyrax/base/_form_files.html.erb +1 -1
  153. data/app/views/hyrax/base/_form_member_of_collections.html.erb +4 -0
  154. data/app/views/hyrax/base/_show_actions.html.erb +7 -8
  155. data/app/views/hyrax/base/_work_button_row.html.erb +1 -1
  156. data/app/views/hyrax/batch_select/_add_button.html.erb +1 -1
  157. data/app/views/hyrax/content_blocks/_form.html.erb +3 -3
  158. data/app/views/hyrax/dashboard/_sidebar.html.erb +1 -1
  159. data/app/views/hyrax/dashboard/_user_activity.html.erb +2 -2
  160. data/app/views/hyrax/dashboard/collections/_form.html.erb +4 -4
  161. data/app/views/hyrax/dashboard/collections/_form_share.html.erb +6 -4
  162. data/app/views/hyrax/dashboard/collections/_list_collections.html.erb +1 -1
  163. data/app/views/hyrax/dashboard/collections/_show_document_list_row.html.erb +1 -1
  164. data/app/views/hyrax/dashboard/show_admin.html.erb +18 -19
  165. data/app/views/hyrax/dashboard/sidebar/_activity.html.erb +1 -1
  166. data/app/views/hyrax/embargoes/_list_expired_active_embargoes.html.erb +7 -7
  167. data/app/views/hyrax/file_sets/_actions.html.erb +9 -1
  168. data/app/views/hyrax/file_sets/_permission_form.html.erb +4 -2
  169. data/app/views/hyrax/file_sets/_show_actions.html.erb +1 -1
  170. data/app/views/hyrax/homepage/_featured.html.erb +1 -1
  171. data/app/views/hyrax/homepage/_recent_document.html.erb +2 -2
  172. data/app/views/hyrax/leases/_list_expired_active_leases.html.erb +6 -6
  173. data/app/views/hyrax/my/collections/_list_collections.html.erb +1 -1
  174. data/app/views/hyrax/my/collections/_tabs.html.erb +1 -1
  175. data/app/views/hyrax/pages/_form.html.erb +8 -8
  176. data/app/views/hyrax/transfers/_received.html.erb +1 -1
  177. data/app/views/hyrax/uploads/create.json.jbuilder +2 -2
  178. data/app/views/hyrax/users/_activity_log.html.erb +15 -9
  179. data/app/views/hyrax/users/_user_row.html.erb +6 -3
  180. data/app/views/hyrax/users/_vitals.html.erb +3 -2
  181. data/app/views/layouts/_head_tag_content.html.erb +2 -0
  182. data/app/views/shared/_appearance_styles.html.erb +5 -1
  183. data/app/views/shared/_ga4.html.erb +11 -0
  184. data/app/views/shared/_select_work_type_modal.html.erb +10 -1
  185. data/bin/db-migrate-seed.sh +3 -3
  186. data/bin/dev-entrypoint.sh +7 -2
  187. data/bin/{db-wait.sh → service-wait.sh} +1 -1
  188. data/bin/worker-entrypoint.sh +8 -0
  189. data/chart/hyrax/templates/deployment-worker.yaml +2 -2
  190. data/config/locales/hyrax.en.yml +4 -2
  191. data/config/metadata/basic_metadata.yaml +20 -0
  192. data/config/metadata/hyrax_internal_metadata.yaml +1 -1
  193. data/docker-compose-dassie.yml +167 -0
  194. data/docker-compose-koppie.yml +21 -36
  195. data/docker-compose-sirenia.yml +50 -44
  196. data/docker-compose.yml +2 -183
  197. data/documentation/developing-your-hyrax-based-app.md +2 -2
  198. data/hyrax.gemspec +5 -4
  199. data/lib/freyja/custom_query_container.rb +5 -0
  200. data/lib/freyja/metadata_adapter.rb +32 -0
  201. data/lib/freyja/persister.rb +42 -0
  202. data/lib/freyja/query_service.rb +20 -0
  203. data/lib/freyja/resource_factory.rb +8 -0
  204. data/lib/freyja.rb +14 -0
  205. data/lib/frigg/custom_query_container.rb +5 -0
  206. data/lib/frigg/metadata_adapter.rb +22 -0
  207. data/lib/frigg/persister.rb +33 -0
  208. data/lib/frigg/query_service.rb +15 -0
  209. data/lib/frigg.rb +13 -0
  210. data/lib/generators/hyrax/install_generator.rb +5 -0
  211. data/lib/generators/hyrax/templates/config/analytics.yml +6 -1
  212. data/lib/generators/hyrax/templates/config/initializers/1_valkyrie.rb +6 -2
  213. data/lib/generators/hyrax/templates/config/valkyrie_index.yml +1 -1
  214. data/lib/goddess/custom_query_container.rb +71 -0
  215. data/lib/goddess/metadata.rb +13 -0
  216. data/lib/goddess/query.rb +176 -0
  217. data/lib/hyrax/configuration.rb +83 -0
  218. data/lib/hyrax/engine.rb +2 -0
  219. data/lib/hyrax/form_fields.rb +1 -3
  220. data/lib/hyrax/name.rb +5 -0
  221. data/lib/hyrax/rubocop/custom_cops.rb +30 -0
  222. data/lib/hyrax/specs/capybara.rb +10 -6
  223. data/lib/hyrax/specs/shared_specs/factories/admin_sets.rb +2 -0
  224. data/lib/hyrax/specs/shared_specs/factories/hyrax_embargo.rb +4 -0
  225. data/lib/hyrax/specs/shared_specs/factories/hyrax_lease.rb +4 -0
  226. data/lib/hyrax/specs/shared_specs/factories/hyrax_work.rb +16 -2
  227. data/lib/hyrax/specs/shared_specs/hydra_works.rb +1 -1
  228. data/lib/hyrax/transactions/admin_set_destroy.rb +2 -1
  229. data/lib/hyrax/transactions/collection_destroy.rb +2 -1
  230. data/lib/hyrax/transactions/container.rb +9 -0
  231. data/lib/hyrax/transactions/steps/add_file_sets.rb +2 -1
  232. data/lib/hyrax/transactions/steps/delete_permission_template.rb +30 -0
  233. data/lib/hyrax/transactions/steps/delete_resource.rb +1 -1
  234. data/lib/hyrax/transactions/steps/save_collection_logo.rb +2 -1
  235. data/lib/hyrax/valkyrie_can_can_adapter.rb +8 -1
  236. data/lib/hyrax/version.rb +1 -1
  237. data/lib/wings/active_fedora_converter.rb +13 -5
  238. data/lib/wings/converter_value_mapper.rb +1 -0
  239. data/lib/wings/services/custom_queries/find_collections_by_type.rb +2 -1
  240. data/lib/wings/services/custom_queries/find_file_metadata.rb +2 -2
  241. data/lib/wings/setup.rb +12 -3
  242. data/lib/wings/transformer_value_mapper.rb +5 -1
  243. data/lib/wings/valkyrie/persister.rb +3 -1
  244. data/template.rb +1 -1
  245. metadata +77 -19
  246. data/.koppie/scripts/db-migrate-seed.sh +0 -9
  247. data/.koppie/scripts/entrypoint.sh +0 -10
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+ module Hyrax
3
+ module Analytics
4
+ module Ga4
5
+ class Base
6
+ attr_reader :start_date, :end_date, :dimensions, :metrics
7
+
8
+ def initialize(start_date:,
9
+ end_date:,
10
+ dimensions: [],
11
+ metrics: [])
12
+ @start_date = start_date.to_date
13
+ @end_date = end_date.to_date
14
+ @dimensions = dimensions
15
+ @metrics = metrics
16
+ end
17
+
18
+ def filters
19
+ @filters ||= {}
20
+ end
21
+
22
+ def filters=(value)
23
+ value
24
+ end
25
+
26
+ def add_filter(dimension:, values:)
27
+ # reset any cached results
28
+ @results = nil
29
+ filters[dimension] ||= []
30
+ filters[dimension] += values
31
+ end
32
+
33
+ def results
34
+ @results ||= Hyrax::Analytics.client.run_report(report).rows
35
+ end
36
+
37
+ def report
38
+ ::Google::Analytics::Data::V1beta::RunReportRequest.new(
39
+ property: Hyrax::Analytics.property,
40
+ metrics: metrics,
41
+ date_ranges: [{ start_date: start_date.iso8601, end_date: end_date.iso8601 }],
42
+ dimensions: dimensions,
43
+ dimension_filter: dimension_filter
44
+ )
45
+ end
46
+
47
+ def dimension_filter
48
+ return nil if filters.blank?
49
+ {
50
+ and_group: {
51
+ expressions: dimension_expressions
52
+ }
53
+ }
54
+ end
55
+
56
+ def dimension_expressions
57
+ filters.map do |dimension, values|
58
+ {
59
+ filter: {
60
+ field_name: dimension,
61
+ in_list_filter: { values: values.uniq }
62
+ }
63
+ }
64
+ end
65
+ end
66
+
67
+ def results_array(target_type = nil)
68
+ r = {}
69
+ # prefill dates so that all dates at least have 0
70
+ (start_date..end_date).each do |date|
71
+ r[date] = 0
72
+ end
73
+ results.each do |result|
74
+ date = unwrap_dimension(metric: result, dimension: 0)
75
+ type = unwrap_dimension(metric: result, dimension: 1)
76
+ next if date.nil? || type.nil?
77
+ next if target_type && type != target_type
78
+ date = date.to_date
79
+ r[date] += unwrap_metric(result)
80
+ end
81
+ Hyrax::Analytics::Results.new(r.to_a)
82
+ end
83
+
84
+ protected
85
+
86
+ def unwrap_dimension(metric:, dimension: 0)
87
+ metric.dimension_values[dimension]&.value
88
+ end
89
+
90
+ def unwrap_metric(metric)
91
+ metric.metric_values.first.value.to_i
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+ module Hyrax
3
+ module Analytics
4
+ module Ga4
5
+ class Events < Hyrax::Analytics::Ga4::Base
6
+ def initialize(start_date:,
7
+ end_date:,
8
+ dimensions: [{ name: 'eventName' }, { name: 'contentType' }, { name: 'contentId' }],
9
+ metrics: [{ name: 'eventCount' }])
10
+ super
11
+ end
12
+
13
+ def self.list(start_date, end_date, action)
14
+ events = Events.new(start_date: start_date, end_date: end_date)
15
+ events.add_filter(dimension: 'eventName', values: [action])
16
+ events.top_result_array
17
+ end
18
+
19
+ def top_result_array
20
+ results.map { |r| [unwrap_dimension(metric: r, dimension: 2), unwrap_metric(r)] }.sort_by { |r| r[1] }
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+ module Hyrax
3
+ module Analytics
4
+ module Ga4
5
+ class EventsDaily < Hyrax::Analytics::Ga4::Base
6
+ def initialize(start_date:,
7
+ end_date:,
8
+ dimensions: [{ name: 'date' }, { name: 'eventName' }, { name: 'contentType' }, { name: 'contentId' }],
9
+ metrics: [{ name: 'eventCount' }])
10
+ super
11
+ end
12
+
13
+ # returns a daily number of events for a specific action
14
+ def self.summary(start_date, end_date, action)
15
+ events_daily = EventsDaily.new(
16
+ start_date: start_date,
17
+ end_date: end_date
18
+ )
19
+ events_daily.add_filter(dimension: 'eventName', values: [action])
20
+ events_daily.results_array
21
+ end
22
+
23
+ # returns a daily number of events for a specific action
24
+ def self.by_id(start_date, end_date, id, action)
25
+ events_daily = EventsDaily.new(
26
+ start_date: start_date,
27
+ end_date: end_date
28
+ )
29
+ events_daily.add_filter(dimension: 'contentId', values: [id])
30
+ events_daily.add_filter(dimension: 'eventName', values: [action])
31
+ events_daily.results_array
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+ module Hyrax
3
+ module Analytics
4
+ module Ga4
5
+ class Visits < Hyrax::Analytics::Ga4::Base
6
+ def initialize(start_date:, end_date:, dimensions: [{ name: 'newVsReturning' }], metrics: [{ name: 'sessions' }])
7
+ super
8
+ end
9
+
10
+ def new_visits
11
+ unwrap_metric(results.detect { |r| unwrap_dimension(metric: r) == 'new' })
12
+ end
13
+
14
+ def return_visits
15
+ unwrap_metric(results.detect { |r| unwrap_dimension(metric: r) == 'returning' })
16
+ end
17
+
18
+ def unknown_visits
19
+ empty_metrics = results.detect { |r| unwrap_dimension(metric: r) == '' }
20
+ not_set_metrics = results.detect { |r| unwrap_dimension(metric: r) == '(not set)' }
21
+ unknown = 0
22
+ unknown += unwrap_metric(empty_metrics) if empty_metrics.present?
23
+ unknown += unwrap_metric(not_set_metrics) if not_set_metrics.present?
24
+ unknown
25
+ end
26
+
27
+ def total_visits
28
+ new_visits + return_visits + unknown_visits
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+ module Hyrax
3
+ module Analytics
4
+ module Ga4
5
+ class VisitsDaily < Hyrax::Analytics::Ga4::Base
6
+ def initialize(start_date:, end_date:, dimensions: [{ name: 'date' }, { name: 'newVsReturning' }], metrics: [{ name: 'sessions' }])
7
+ super
8
+ end
9
+
10
+ def new_visits
11
+ results_array('new')
12
+ end
13
+
14
+ def return_visits
15
+ results_array('returning')
16
+ end
17
+
18
+ def total_visits
19
+ results_array
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'oauth2'
4
+
5
+ begin
6
+ require "google/analytics/data/v1beta"
7
+ rescue LoadError
8
+ $stderr.puts "Unable to load 'google/analytics/data/v1beta'; this is okay unless you are trying to do analytics reporting."
9
+ end
10
+
11
+ module Hyrax
12
+ module Analytics
13
+ # rubocop:disable Metrics/ModuleLength
14
+ module Ga4
15
+ extend ActiveSupport::Concern
16
+ # rubocop:disable Metrics/BlockLength
17
+ class_methods do
18
+ # Loads configuration options from config/analytics.yml. You only need PRIVATE_KEY_PATH or
19
+ # PRIVATE_KEY_VALUE. VALUE takes precedence.
20
+ # Expected structure:
21
+ # `analytics:`
22
+ # ` ga4:`
23
+ # analytics_id: <%= ENV['GOOGLE_ANALYTICS_ID'] %>
24
+ # property_id: <%= ENV['GOOGLE_ANALYTICS_PROPERTY_ID'] %>
25
+ # account_json: <%= ENV['GOOGLE_ACCOUNT_JSON'] %>
26
+ # account_json_path: <%= ENV['GOOGLE_ACCOUNT_JSON_PATH'] %>
27
+ # @return [Config]
28
+ def config
29
+ @config ||= Config.load_from_yaml
30
+ end
31
+
32
+ class Config
33
+ def self.load_from_yaml
34
+ filename = Rails.root.join('config', 'analytics.yml')
35
+ yaml = YAML.safe_load(ERB.new(File.read(filename)).result)
36
+ unless yaml
37
+ Hyrax.logger.error("Unable to fetch any keys from #{filename}.")
38
+ return new({})
39
+ end
40
+ config = yaml.fetch('analytics')&.fetch('ga4', nil)
41
+ unless config
42
+ Deprecation.warn("Deprecated analytics configuration format found. Please update config/analytics.yml.")
43
+ config = yaml.fetch('analytics')
44
+ # this has to exist here with a placeholder so it can be set in the Hyrax initializer
45
+ # it is only for backward compatibility
46
+ config['analytics_id'] = '-'
47
+ end
48
+ new config
49
+ end
50
+
51
+ KEYS = %w[analytics_id property_id account_json account_json_path].freeze
52
+ REQUIRED_KEYS = %w[analytics_id property_id].freeze
53
+
54
+ def initialize(config)
55
+ @config = config
56
+ end
57
+
58
+ # @return [Boolean] are all the required values present?
59
+ def valid?
60
+ return false unless @config['account_json'].present? || @config['account_json_path'].present?
61
+
62
+ REQUIRED_KEYS.all? { |required| @config[required].present? }
63
+ end
64
+
65
+ def base64?(value)
66
+ value.is_a?(String) && Base64.strict_encode64(Base64.decode64(value)) == value
67
+ end
68
+
69
+ def account_json_string
70
+ return @account_json_string if @account_json_string
71
+ @account_json_string = if @config['account_json']
72
+ base64?(@config['account_json']) ? Base64.decode64(@config['account_json']) : @config['account_json']
73
+ else
74
+ File.read(@config['account_json_path'])
75
+ end
76
+ end
77
+
78
+ def account_info
79
+ @account_info ||= JSON.parse(account_json_string)
80
+ end
81
+
82
+ KEYS.each do |key|
83
+ # rubocop:disable Style/EvalWithLocation
84
+ class_eval %{ def #{key}; @config.fetch('#{key}'); end }
85
+ class_eval %{ def #{key}=(value); @config['#{key}'] = value; end }
86
+ # rubocop:enable Style/EvalWithLocation
87
+ end
88
+ end
89
+
90
+ def client
91
+ @client ||= ::Google::Analytics::Data::V1beta::AnalyticsData::Client.new do |conf|
92
+ conf.credentials = config.account_info
93
+ end
94
+ end
95
+
96
+ def property
97
+ "properties/#{config.property_id}"
98
+ end
99
+
100
+ # rubocop:disable Metrics/MethodLength
101
+ def to_date_range(period)
102
+ case period
103
+ when "day"
104
+ start_date = Time.zone.today
105
+ end_date = Time.zone.today
106
+ when "week"
107
+ start_date = Time.zone.today - 7.days
108
+ end_date = Time.zone.today
109
+ when "month"
110
+ start_date = Time.zone.today - 1.month
111
+ end_date = Time.zone.today
112
+ when "year"
113
+ start_date = Time.zone.today - 1.year
114
+ end_date = Time.zone.today
115
+ end
116
+
117
+ [start_date, end_date]
118
+ end
119
+ # rubocop:enable Metrics/MethodLength
120
+
121
+ def keyword_conversion(date)
122
+ case date
123
+ when "last12"
124
+ start_date = Time.zone.today - 11.months
125
+ end_date = Time.zone.today
126
+
127
+ [start_date, end_date]
128
+ else
129
+ date.split(",")
130
+ end
131
+ end
132
+
133
+ def date_period(period, date)
134
+ if period == "range"
135
+ date.split(",")
136
+ else
137
+ to_date_range(period)
138
+ end
139
+ end
140
+
141
+ # Configure analytics_start_date in ENV file
142
+ def default_date_range
143
+ "#{Hyrax.config.analytics_start_date},#{Time.zone.today + 1.day}"
144
+ end
145
+
146
+ # The number of events by day for an action
147
+ def daily_events(action, date = default_date_range)
148
+ date = date.split(",")
149
+ EventsDaily.summary(date[0], date[1], action)
150
+ end
151
+
152
+ # The number of events by day for an action and ID
153
+ def daily_events_for_id(id, action, date = default_date_range)
154
+ date = date.split(",")
155
+ EventsDaily.by_id(date[0], date[1], id, action)
156
+ end
157
+
158
+ # A list of events sorted by highest event count
159
+ def top_events(action, date = default_date_range)
160
+ date = date.split(",")
161
+ Events.list(date[0], date[1], action)
162
+ end
163
+
164
+ def unique_visitors(date = default_date_range); end
165
+
166
+ def unique_visitors_for_id(id, date = default_date_range); end
167
+
168
+ def new_visitors(period = 'month', date = default_date_range)
169
+ start_date, end_date = date_period(period, date)
170
+ Visits.new(start_date: start_date, end_date: end_date).new_visits
171
+ end
172
+
173
+ def new_visits_by_day(date = default_date_range, period = 'range')
174
+ start_date, end_date = date_period(period, date)
175
+ VisitsDaily.new(start_date: start_date, end_date: end_date).new_visits
176
+ end
177
+
178
+ def returning_visitors(period = 'month', date = default_date_range)
179
+ start_date, end_date = date_period(period, date)
180
+ Visits.new(start_date: start_date, end_date: end_date).return_visits
181
+ end
182
+
183
+ def returning_visits_by_day(date = default_date_range, period = 'range')
184
+ start_date, end_date = date_period(period, date)
185
+ VisitsDaily.new(start_date: start_date, end_date: end_date).return_visits
186
+ end
187
+
188
+ def total_visitors(period = 'month', date = default_date_range)
189
+ start_date, end_date = date_period(period, date)
190
+ Visits.new(start_date: start_date, end_date: end_date).total_visits
191
+ end
192
+
193
+ def page_statistics(start_date, object)
194
+ visits = VisitsDaily.new(start_date: start_date, end_date: Date.yesterday)
195
+ visits.add_filter(dimension: 'contentId', values: [object.id.to_s])
196
+ visits.total_visits
197
+ end
198
+ end
199
+ # rubocop:enable Metrics/BlockLength
200
+ end
201
+ # rubocop:enable Metrics/ModuleLength
202
+ end
203
+ end
204
+ # rubocop:enable Metrics/ModuleLength
@@ -55,7 +55,6 @@ module Hyrax
55
55
  # @return [Boolean] are all the required values present?
56
56
  def valid?
57
57
  return false unless @config['privkey_value'].present? || @config['privkey_path'].present?
58
-
59
58
  REQUIRED_KEYS.all? { |required| @config[required].present? }
60
59
  end
61
60
 
@@ -203,10 +202,25 @@ module Hyrax
203
202
  date = date_period(period, date)
204
203
  Visits.total_visits(profile, date[0], date[1])
205
204
  end
205
+
206
+ # Hyrax::Download is sent to Hyrax::Analytics.profile as #hyrax__download
207
+ # see Legato::ProfileMethods.method_name_from_klass
208
+ def page_statistics(start_date, object)
209
+ path = Rails.application.routes.url_helpers.polymorphic_path(object)
210
+ profile = Hyrax::Analytics.profile
211
+ unless profile
212
+ Hyrax.logger.error("Google Analytics profile has not been established. Unable to fetch statistics.")
213
+ return []
214
+ end
215
+ profile.hyrax__pageview(sort: 'date',
216
+ start_date: start_date,
217
+ end_date: Date.yesterday,
218
+ limit: 10_000)
219
+ .for_path(path)
220
+ end
206
221
  end
207
222
  # rubocop:enable Metrics/BlockLength
208
223
  end
209
224
  # rubocop:enable Metrics/ModuleLength
210
225
  end
211
226
  end
212
- # rubocop:enable Metrics/ModuleLength
@@ -154,24 +154,37 @@ module Hyrax
154
154
  response["nb_visits_returning"].to_i + response["nb_visits_new"].to_i
155
155
  end
156
156
 
157
+ # TODO: implement
158
+ def page_statistics(_start_date, _object)
159
+ []
160
+ end
161
+
157
162
  def results_array(response, metric)
158
163
  results = []
159
164
  response.each do |result|
160
165
  if result[1].empty?
161
166
  results.push([result[0].to_date, 0])
162
167
  elsif result[1].is_a?(Array)
163
- results.push([result[0].to_date, result[1].first[metric]])
168
+ results.push([result[0].to_date, result[1].first[metric].to_i])
164
169
  else
165
- results.push([result[0].to_date, result[1][metric].presence || 0])
170
+ results.push([result[0].to_date, result[1][metric].presence.to_i])
166
171
  end
167
172
  end
168
173
  Hyrax::Analytics::Results.new(results)
169
174
  end
170
175
 
176
+ # If Matomo detects an error it will return a reponse with the key {"result":"error"}
177
+ # instead of returning an error status code. This method checks for that key.
178
+ def contains_matomo_error?(response)
179
+ response.is_a?(Hash) && response["result"] == "error"
180
+ end
181
+
171
182
  def get(params)
172
183
  response = Faraday.get(config.base_url, params)
173
184
  return [] if response.status != 200
174
- JSON.parse(response.body)
185
+ api_response = JSON.parse(response.body)
186
+ return [] if contains_matomo_error?(api_response)
187
+ api_response
175
188
  end
176
189
 
177
190
  def api_params(method, period, date, additional_params = {})
@@ -74,6 +74,12 @@ module Hyrax
74
74
  fields = [:date, :pageviews]
75
75
  results.map { |row| fields.zip(row).to_h }
76
76
  end
77
+
78
+ def each
79
+ results.each do |result|
80
+ yield({ date: result[0], pageviews: result[1] })
81
+ end
82
+ end
77
83
  end
78
84
  end
79
85
  end
@@ -20,7 +20,7 @@ module Hyrax
20
20
  .find_inverse_references_by(resource: resource, property: :access_to)
21
21
  .find { |r| r.is_a?(Hyrax::AccessControl) } ||
22
22
  raise(Valkyrie::Persistence::ObjectNotFoundError)
23
- rescue ArgumentError # some adapters raise ArgumentError for missing resources
23
+ rescue ArgumentError, Ldp::Gone, Ldp::NotFound # some adapters raise ArgumentError for missing resources
24
24
  raise(Valkyrie::Persistence::ObjectNotFoundError)
25
25
  end
26
26
  end
@@ -25,30 +25,13 @@ module Hyrax
25
25
  # @param models [Array]
26
26
  # @param start_datetime [DateTime]
27
27
  # @param end_datetime [DateTime]
28
+ # @return [Array<Hyrax::Resource>]
28
29
  def find_by_date_range(start_datetime:, end_datetime: nil, models: nil)
29
- end_datetime = 1.second.since(Time.zone.now) if end_datetime.blank?
30
- if models.present?
31
- query_service.run_query(find_models_by_date_range_query, start_datetime.to_s, end_datetime.to_s, models)
32
- else
33
- query_service.run_query(find_by_date_range_query, start_datetime.to_s, end_datetime.to_s)
34
- end
35
- end
36
-
37
- def find_models_by_date_range_query
38
- <<-SQL
39
- SELECT * FROM orm_resources
40
- WHERE created_at >= ?
41
- AND created_at <= ?
42
- AND internal_resource IN (?);
43
- SQL
44
- end
45
-
46
- def find_by_date_range_query
47
- <<-SQL
48
- SELECT * FROM orm_resources
49
- WHERE created_at >= ?
50
- AND created_at <= ?;
51
- SQL
30
+ end_range = end_datetime.blank? ? '*' : end_datetime.utc.xmlschema
31
+ query = "system_create_dtsi:[#{start_datetime.utc.xmlschema} TO #{end_range}]"
32
+ query += " AND has_model_ssim: (#{models.map { |m| "\"#{m}\"" }.join(' OR ')})" unless models.empty?
33
+ ids = Hyrax::SolrService.query_result(query, fl: 'id')['response']['docs'].map { |doc| doc['id'] }
34
+ Hyrax.query_service.find_many_by_ids(ids: ids)
52
35
  end
53
36
  end
54
37
  end
@@ -28,9 +28,9 @@ module Hyrax
28
28
  # @param global_id [GlobalID] global id for a Hyrax::CollectionType
29
29
  #
30
30
  # @return [Enumerable<PcdmCollection>]
31
- def find_collections_by_type(global_id:)
31
+ def find_collections_by_type(global_id:, model: Hyrax.config.collection_class)
32
32
  query_service
33
- .find_all_of_model(model: Hyrax.config.collection_model.safe_constantize)
33
+ .find_all_of_model(model:)
34
34
  .select { |collection| collection.collection_type_gid == global_id }
35
35
  end
36
36
  end
@@ -25,37 +25,9 @@ module Hyrax
25
25
  # @param hash [Hash] the hash representation of the query
26
26
  def find_count_by(hash = {}, models: nil)
27
27
  return nil if models.empty? && hash.blank?
28
-
29
- internal_array = ["{ #{hash.map { |k, v| "\"#{k}\": #{v}" }.join(', ')} }"] if hash.present?
30
- if models.empty?
31
- query_service.orm_class.count_by_sql(([find_count_by_properties_query] + internal_array))
32
- elsif hash.blank?
33
- query_service.orm_class.count_by_sql([find_count_by_models_query] + [models])
34
- else
35
- query_service.orm_class.count_by_sql(([find_count_by_properties_and_models_query] + internal_array + [models]))
36
- end
37
- end
38
-
39
- def find_count_by_properties_and_models_query
40
- <<-SQL
41
- SELECT count(*) FROM orm_resources
42
- WHERE metadata @> ?
43
- AND internal_resource IN (?);
44
- SQL
45
- end
46
-
47
- def find_count_by_models_query
48
- <<-SQL
49
- SELECT count(*) FROM orm_resources
50
- WHERE internal_resource IN (?);
51
- SQL
52
- end
53
-
54
- def find_count_by_properties_query
55
- <<-SQL
56
- SELECT count(*) FROM orm_resources
57
- WHERE metadata @> ?;
58
- SQL
28
+ flat_hash = hash.map { |k, v| "#{k}: \"#{v}\"" }.join(' ')
29
+ flat_hash += " has_model_ssim: (#{models.map { |m| "\"#{m}\"" }.join(' OR ')})" unless models.empty?
30
+ Hyrax::SolrService.count(flat_hash)
59
31
  end
60
32
  end
61
33
  end
@@ -31,7 +31,7 @@ module Hyrax
31
31
  result = query_service.find_by(id: id)
32
32
  unless result.is_a? Hyrax::FileMetadata
33
33
  raise ::Valkyrie::Persistence::ObjectNotFoundError,
34
- "Result type #{result.internal_resource} for id #{id} is not a `Hyrax::FileMetadata`"
34
+ "Result type #{result&.internal_resource} for id #{id} is not a `Hyrax::FileMetadata`"
35
35
  end
36
36
  result
37
37
  end
@@ -44,7 +44,7 @@ module Hyrax
44
44
  result = query_service.find_by_alternate_identifier(alternate_identifier: alternate_identifier)
45
45
  unless result.is_a? Hyrax::FileMetadata
46
46
  raise ::Valkyrie::Persistence::ObjectNotFoundError,
47
- "Result type #{result.internal_resource} for alternate_identifier #{alternate_identifier} is not a `Hyrax::FileMetadata`"
47
+ "Result type #{result&.internal_resource} for alternate_identifier #{alternate_identifier} is not a `Hyrax::FileMetadata`"
48
48
  end
49
49
  result
50
50
  end
@@ -24,35 +24,13 @@ module Hyrax
24
24
  #
25
25
  # @param model [Class]
26
26
  # @param ids [Enumerable<#to_s>, Symbol]
27
+ # @return [Array<Hyrax::Resource>]
27
28
  #
28
29
  def find_models_by_access(mode:, models: nil, agent:, group: nil)
29
- agent = "group/#{agent}" if group.present?
30
- internal_array = "{\"permissions\": [{\"mode\": \"#{mode}\", \"agent\": \"#{agent}\"}]}"
31
- if models.present?
32
- query_service.run_query(find_models_by_access_query, internal_array, models)
33
- else
34
- query_service.run_query(find_by_access_query, internal_array)
35
- end
36
- end
37
-
38
- def find_models_by_access_query
39
- <<-SQL
40
- SELECT * FROM orm_resources
41
- WHERE id IN (
42
- SELECT uuid(metadata::json#>'{access_to,0}'->>'id') FROM orm_resources
43
- WHERE metadata @> ?
44
- ) AND internal_resource IN (?);
45
- SQL
46
- end
47
-
48
- def find_by_access_query
49
- <<-SQL
50
- SELECT * FROM orm_resources
51
- WHERE id IN (
52
- SELECT uuid(metadata::json#>'{access_to,0}'->>'id') FROM orm_resources
53
- WHERE metadata @> ?
54
- );
55
- SQL
30
+ query = "#{Hydra.config.permissions[mode.to_sym][(group ? 'group' : 'individual').to_sym]}:#{agent}"
31
+ query += " AND has_model_ssim: (#{models.map { |m| "\"#{m}\"" }.join(' OR ')})" unless models.empty?
32
+ ids = Hyrax::SolrService.query_result(query, fl: 'id')['response']['docs'].map { |doc| doc['id'] }
33
+ Hyrax.query_service.find_many_by_ids(ids: ids)
56
34
  end
57
35
  end
58
36
  end