hyrax 5.0.1 → 5.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (249) 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/batch_create_job.rb +4 -3
  80. data/app/jobs/create_work_job.rb +4 -3
  81. data/app/jobs/migrate_files_to_valkyrie_job.rb +109 -0
  82. data/app/jobs/migrate_resources_job.rb +34 -0
  83. data/app/jobs/valkyrie_create_derivatives_job.rb +2 -1
  84. data/app/models/admin_set.rb +1 -0
  85. data/app/models/concerns/hyrax/ar_resource.rb +104 -0
  86. data/app/models/concerns/hyrax/solr_document/ordered_members.rb +2 -1
  87. data/app/models/concerns/hyrax/solr_document_behavior.rb +13 -2
  88. data/app/models/concerns/hyrax/valkyrie_lazy_migration.rb +82 -0
  89. data/app/models/file_download_stat.rb +1 -1
  90. data/app/models/file_view_stat.rb +1 -1
  91. data/app/models/hyrax/collection_type.rb +12 -4
  92. data/app/models/hyrax/file_metadata.rb +19 -0
  93. data/app/models/hyrax/file_set.rb +25 -0
  94. data/app/models/hyrax/model_registry.rb +2 -3
  95. data/app/models/hyrax/resource.rb +5 -0
  96. data/app/models/hyrax/statistic.rb +12 -37
  97. data/app/presenters/hyrax/file_set_presenter.rb +2 -1
  98. data/app/presenters/hyrax/file_usage.rb +3 -3
  99. data/app/presenters/hyrax/iiif_manifest_presenter.rb +2 -1
  100. data/app/presenters/hyrax/member_presenter_factory.rb +7 -1
  101. data/app/presenters/hyrax/menu_presenter.rb +1 -1
  102. data/app/presenters/hyrax/stats_usage_presenter.rb +2 -1
  103. data/app/presenters/hyrax/work_show_presenter.rb +13 -17
  104. data/app/presenters/hyrax/work_usage.rb +5 -2
  105. data/app/search_builders/hyrax/expired_embargo_search_builder.rb +7 -1
  106. data/app/search_builders/hyrax/expired_lease_search_builder.rb +7 -1
  107. data/app/search_builders/hyrax/filter_by_type.rb +1 -3
  108. data/app/search_builders/hyrax/valkyrie_abstract_type_relation.rb +7 -2
  109. data/app/services/hyrax/access_control_list.rb +1 -1
  110. data/app/services/hyrax/admin_set_create_service.rb +16 -5
  111. data/app/services/hyrax/admin_set_service.rb +2 -1
  112. data/app/services/hyrax/analytics/ga4/base.rb +96 -0
  113. data/app/services/hyrax/analytics/ga4/events.rb +25 -0
  114. data/app/services/hyrax/analytics/ga4/events_daily.rb +36 -0
  115. data/app/services/hyrax/analytics/ga4/visits.rb +33 -0
  116. data/app/services/hyrax/analytics/ga4/visits_daily.rb +24 -0
  117. data/app/services/hyrax/analytics/ga4.rb +204 -0
  118. data/app/services/hyrax/analytics/google.rb +16 -2
  119. data/app/services/hyrax/analytics/matomo.rb +16 -3
  120. data/app/services/hyrax/analytics/results.rb +6 -0
  121. data/app/services/hyrax/custom_queries/find_access_control.rb +1 -1
  122. data/app/services/hyrax/custom_queries/find_by_date_range.rb +6 -23
  123. data/app/services/hyrax/custom_queries/find_collections_by_type.rb +2 -2
  124. data/app/services/hyrax/custom_queries/find_count_by.rb +3 -31
  125. data/app/services/hyrax/custom_queries/find_file_metadata.rb +2 -2
  126. data/app/services/hyrax/custom_queries/find_models_by_access.rb +5 -27
  127. data/app/services/hyrax/embargo_manager.rb +2 -1
  128. data/app/services/hyrax/listeners/file_listener.rb +2 -2
  129. data/app/services/hyrax/lock_manager.rb +6 -6
  130. data/app/services/hyrax/lockable.rb +4 -3
  131. data/app/services/hyrax/simple_schema_loader.rb +1 -1
  132. data/app/services/hyrax/solr_service.rb +22 -8
  133. data/app/services/hyrax/statistics/query_service.rb +1 -1
  134. data/app/services/hyrax/statistics/works/over_time.rb +1 -1
  135. data/app/services/hyrax/thumbnail_path_service.rb +2 -0
  136. data/app/services/hyrax/user_stat_importer.rb +5 -5
  137. data/app/services/hyrax/valkyrie_upload.rb +9 -7
  138. data/app/services/hyrax/versioning_service.rb +10 -2
  139. data/app/services/hyrax/work_query_service.rb +2 -2
  140. data/app/services/migrate_resource_service.rb +55 -0
  141. data/app/views/_controls.html.erb +5 -5
  142. data/app/views/_masthead.html.erb +1 -1
  143. data/app/views/catalog/_search_form.html.erb +9 -16
  144. data/app/views/catalog/_thumbnail_list_collection.html.erb +1 -1
  145. data/app/views/catalog/_thumbnail_list_default.html.erb +2 -2
  146. data/app/views/hyrax/admin/analytics/collection_reports/index.html.erb +4 -4
  147. data/app/views/hyrax/admin/analytics/work_reports/index.html.erb +1 -1
  148. data/app/views/hyrax/admin/collection_types/_form.html.erb +4 -4
  149. data/app/views/hyrax/admin/collection_types/index.html.erb +1 -1
  150. data/app/views/hyrax/admin/features/index.html.erb +1 -1
  151. data/app/views/hyrax/base/_file_manager_actions.html.erb +1 -1
  152. data/app/views/hyrax/base/_file_manager_member.html.erb +7 -4
  153. data/app/views/hyrax/base/_file_manager_thumbnail.html.erb +1 -1
  154. data/app/views/hyrax/base/_form_files.html.erb +1 -1
  155. data/app/views/hyrax/base/_form_member_of_collections.html.erb +4 -0
  156. data/app/views/hyrax/base/_show_actions.html.erb +7 -8
  157. data/app/views/hyrax/base/_work_button_row.html.erb +1 -1
  158. data/app/views/hyrax/batch_select/_add_button.html.erb +1 -1
  159. data/app/views/hyrax/content_blocks/_form.html.erb +3 -3
  160. data/app/views/hyrax/dashboard/_sidebar.html.erb +1 -1
  161. data/app/views/hyrax/dashboard/_user_activity.html.erb +2 -2
  162. data/app/views/hyrax/dashboard/collections/_form.html.erb +4 -4
  163. data/app/views/hyrax/dashboard/collections/_form_share.html.erb +6 -4
  164. data/app/views/hyrax/dashboard/collections/_list_collections.html.erb +1 -1
  165. data/app/views/hyrax/dashboard/collections/_show_document_list_row.html.erb +1 -1
  166. data/app/views/hyrax/dashboard/show_admin.html.erb +18 -19
  167. data/app/views/hyrax/dashboard/sidebar/_activity.html.erb +1 -1
  168. data/app/views/hyrax/embargoes/_list_expired_active_embargoes.html.erb +7 -7
  169. data/app/views/hyrax/file_sets/_actions.html.erb +9 -1
  170. data/app/views/hyrax/file_sets/_permission_form.html.erb +4 -2
  171. data/app/views/hyrax/file_sets/_show_actions.html.erb +1 -1
  172. data/app/views/hyrax/homepage/_featured.html.erb +1 -1
  173. data/app/views/hyrax/homepage/_recent_document.html.erb +2 -2
  174. data/app/views/hyrax/leases/_list_expired_active_leases.html.erb +6 -6
  175. data/app/views/hyrax/my/collections/_list_collections.html.erb +1 -1
  176. data/app/views/hyrax/my/collections/_tabs.html.erb +1 -1
  177. data/app/views/hyrax/pages/_form.html.erb +8 -8
  178. data/app/views/hyrax/transfers/_received.html.erb +1 -1
  179. data/app/views/hyrax/uploads/create.json.jbuilder +2 -2
  180. data/app/views/hyrax/users/_activity_log.html.erb +15 -9
  181. data/app/views/hyrax/users/_user_row.html.erb +6 -3
  182. data/app/views/hyrax/users/_vitals.html.erb +3 -2
  183. data/app/views/layouts/_head_tag_content.html.erb +2 -0
  184. data/app/views/shared/_appearance_styles.html.erb +5 -1
  185. data/app/views/shared/_ga4.html.erb +11 -0
  186. data/app/views/shared/_select_work_type_modal.html.erb +10 -1
  187. data/bin/db-migrate-seed.sh +3 -3
  188. data/bin/dev-entrypoint.sh +7 -2
  189. data/bin/{db-wait.sh → service-wait.sh} +1 -1
  190. data/bin/worker-entrypoint.sh +8 -0
  191. data/chart/hyrax/templates/deployment-worker.yaml +2 -2
  192. data/config/locales/hyrax.en.yml +4 -2
  193. data/config/metadata/basic_metadata.yaml +20 -0
  194. data/config/metadata/hyrax_internal_metadata.yaml +1 -1
  195. data/docker-compose-dassie.yml +167 -0
  196. data/docker-compose-koppie.yml +21 -36
  197. data/docker-compose-sirenia.yml +50 -44
  198. data/docker-compose.yml +2 -183
  199. data/documentation/developing-your-hyrax-based-app.md +2 -2
  200. data/hyrax.gemspec +5 -4
  201. data/lib/freyja/custom_query_container.rb +5 -0
  202. data/lib/freyja/metadata_adapter.rb +32 -0
  203. data/lib/freyja/persister.rb +42 -0
  204. data/lib/freyja/query_service.rb +20 -0
  205. data/lib/freyja/resource_factory.rb +8 -0
  206. data/lib/freyja.rb +14 -0
  207. data/lib/frigg/custom_query_container.rb +5 -0
  208. data/lib/frigg/metadata_adapter.rb +22 -0
  209. data/lib/frigg/persister.rb +33 -0
  210. data/lib/frigg/query_service.rb +15 -0
  211. data/lib/frigg.rb +13 -0
  212. data/lib/generators/hyrax/install_generator.rb +5 -0
  213. data/lib/generators/hyrax/templates/config/analytics.yml +6 -1
  214. data/lib/generators/hyrax/templates/config/initializers/1_valkyrie.rb +6 -2
  215. data/lib/generators/hyrax/templates/config/valkyrie_index.yml +1 -1
  216. data/lib/goddess/custom_query_container.rb +71 -0
  217. data/lib/goddess/metadata.rb +13 -0
  218. data/lib/goddess/query.rb +176 -0
  219. data/lib/hyrax/configuration.rb +83 -0
  220. data/lib/hyrax/engine.rb +2 -0
  221. data/lib/hyrax/form_fields.rb +1 -3
  222. data/lib/hyrax/name.rb +5 -0
  223. data/lib/hyrax/rubocop/custom_cops.rb +30 -0
  224. data/lib/hyrax/specs/capybara.rb +10 -6
  225. data/lib/hyrax/specs/shared_specs/factories/admin_sets.rb +2 -0
  226. data/lib/hyrax/specs/shared_specs/factories/hyrax_embargo.rb +4 -0
  227. data/lib/hyrax/specs/shared_specs/factories/hyrax_lease.rb +4 -0
  228. data/lib/hyrax/specs/shared_specs/factories/hyrax_work.rb +16 -2
  229. data/lib/hyrax/specs/shared_specs/hydra_works.rb +1 -1
  230. data/lib/hyrax/transactions/admin_set_destroy.rb +2 -1
  231. data/lib/hyrax/transactions/collection_destroy.rb +2 -1
  232. data/lib/hyrax/transactions/container.rb +9 -0
  233. data/lib/hyrax/transactions/steps/add_file_sets.rb +2 -1
  234. data/lib/hyrax/transactions/steps/delete_permission_template.rb +30 -0
  235. data/lib/hyrax/transactions/steps/delete_resource.rb +1 -1
  236. data/lib/hyrax/transactions/steps/save_collection_logo.rb +2 -1
  237. data/lib/hyrax/valkyrie_can_can_adapter.rb +8 -1
  238. data/lib/hyrax/version.rb +1 -1
  239. data/lib/wings/active_fedora_converter.rb +13 -5
  240. data/lib/wings/converter_value_mapper.rb +1 -0
  241. data/lib/wings/services/custom_queries/find_collections_by_type.rb +2 -1
  242. data/lib/wings/services/custom_queries/find_file_metadata.rb +2 -2
  243. data/lib/wings/setup.rb +12 -3
  244. data/lib/wings/transformer_value_mapper.rb +5 -1
  245. data/lib/wings/valkyrie/persister.rb +3 -1
  246. data/template.rb +1 -1
  247. metadata +77 -19
  248. data/.koppie/scripts/db-migrate-seed.sh +0 -9
  249. 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