sufia 7.0.0.beta1 → 7.0.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/sufia.js +3 -4
  3. data/app/assets/stylesheets/sufia/_styles.scss +2 -1
  4. data/app/controllers/concerns/sufia/transfers_controller_behavior.rb +1 -0
  5. data/app/controllers/errors_controller.rb +1 -1
  6. data/app/indexers/sufia/work_indexer.rb +1 -0
  7. data/app/presenters/sufia/admin_stats_presenter.rb +17 -11
  8. data/app/search_builders/sufia/catalog_search_builder.rb +29 -0
  9. data/app/search_builders/sufia/my_collections_search_builder.rb +7 -0
  10. data/app/search_builders/sufia/my_highlights_search_builder.rb +9 -3
  11. data/app/search_builders/sufia/my_shares_search_builder.rb +8 -1
  12. data/app/search_builders/sufia/search_builder.rb +0 -59
  13. data/app/services/sufia/analytics.rb +25 -8
  14. data/app/services/sufia/query_service.rb +1 -1
  15. data/app/services/sufia/statistics/collections/over_time.rb +13 -0
  16. data/app/services/sufia/statistics/depositors/summary.rb +54 -0
  17. data/app/services/sufia/statistics/file_sets/by_format.rb +14 -0
  18. data/app/services/sufia/statistics/over_time.rb +11 -3
  19. data/app/services/sufia/statistics/system_stats.rb +61 -0
  20. data/app/services/sufia/statistics/term_query.rb +65 -0
  21. data/app/services/sufia/statistics/works/by_depositor.rb +13 -0
  22. data/app/services/sufia/statistics/works/by_resource_type.rb +13 -0
  23. data/app/services/sufia/statistics/works/count.rb +49 -0
  24. data/app/services/sufia/statistics/works/over_time.rb +13 -0
  25. data/app/views/admin/stats/_stats_by_date.html.erb +1 -1
  26. data/app/views/admin/stats/_top_data.html.erb +4 -4
  27. data/app/views/admin/stats/_works.html.erb +8 -0
  28. data/app/views/collections/_form.html.erb +1 -1
  29. data/app/views/curation_concerns/base/_attribute_rows.html.erb +10 -10
  30. data/app/views/curation_concerns/base/_metadata.html.erb +1 -1
  31. data/app/views/curation_concerns/base/_relationships.html.erb +1 -1
  32. data/app/views/curation_concerns/file_sets/_descriptions.html.erb +1 -1
  33. data/app/views/error/404.html.erb +8 -19
  34. data/app/views/layouts/error.html.erb +3 -3
  35. data/app/views/layouts/homepage.html.erb +1 -1
  36. data/app/views/layouts/sufia-dashboard.html.erb +1 -1
  37. data/app/views/layouts/sufia-one-column.html.erb +1 -1
  38. data/app/views/{_footer.html.erb → shared/_footer.html.erb} +1 -1
  39. data/app/views/stats/file.html.erb +0 -1
  40. data/app/views/stats/work.html.erb +0 -2
  41. data/lib/generators/sufia/install_generator.rb +6 -0
  42. data/lib/sufia/engine.rb +1 -0
  43. data/lib/sufia/version.rb +1 -1
  44. data/spec/controllers/my/shares_controller_spec.rb +6 -7
  45. data/spec/controllers/transfers_controller_spec.rb +10 -0
  46. data/spec/features/batch_edit_spec.rb +1 -1
  47. data/spec/lib/sufia/analytics_spec.rb +18 -10
  48. data/spec/presenters/sufia/admin_stats_presenter_spec.rb +21 -14
  49. data/spec/search_builder/{sufia_search_builder_spec.rb → sufia/catalog_search_builder_spec.rb} +1 -1
  50. data/spec/search_builder/sufia/my_shares_search_builder_spec.rb +18 -0
  51. data/spec/services/statistics/{collections_spec.rb → collections/over_time_spec.rb} +1 -1
  52. data/spec/services/{sufia/admin/depositor_stats_spec.rb → statistics/depositors/summary_spec.rb} +5 -7
  53. data/spec/services/statistics/file_sets/by_format_spec.rb +30 -0
  54. data/spec/services/statistics/system_stats_spec.rb +54 -0
  55. data/spec/services/statistics/works/by_depositor_spec.rb +25 -0
  56. data/spec/services/statistics/works/by_resource_type_spec.rb +21 -0
  57. data/spec/services/statistics/works/count_spec.rb +42 -0
  58. data/spec/services/statistics/{works_spec.rb → works/over_time_spec.rb} +1 -1
  59. data/spec/views/admin/stats/index.html.erb_spec.rb +3 -6
  60. data/spec/views/curation_concerns/base/_relationships.html.erb_spec.rb +4 -1
  61. data/sufia.gemspec +2 -1
  62. metadata +49 -25
  63. data/app/services/sufia/admin/depositor_stats.rb +0 -48
  64. data/app/services/sufia/statistics/collections.rb +0 -12
  65. data/app/services/sufia/statistics/works.rb +0 -12
  66. data/app/services/sufia/system_stats.rb +0 -120
  67. data/app/views/admin/stats/_files.html.erb +0 -8
  68. data/lib/generators/sufia/fulltext_generator.rb +0 -26
  69. data/spec/services/sufia/system_stats_spec.rb +0 -224
  70. data/vendor/assets/javascripts/flot/excanvas.js +0 -1428
  71. data/vendor/assets/javascripts/flot/jquery.flot.js +0 -3168
  72. data/vendor/assets/javascripts/flot/jquery.flot.selection.js +0 -360
  73. data/vendor/assets/javascripts/flot/jquery.flot.time.js +0 -432
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 117c39bea3f165e71777729d3f4118473b8a876c
4
- data.tar.gz: 8b5a85e970c5ca8dd32733fbb8723548e80d8930
3
+ metadata.gz: 8878efdf23b664d1ccc522e05f997d099dcd43f9
4
+ data.tar.gz: 8062a829e1bc496ef751aca11363c10bc5fc143c
5
5
  SHA512:
6
- metadata.gz: 8d03ab38475146b43bf223c5c9b6a9a9a20c00f82d49887acd922164d0423daee93d561101d116b36a8ffc328be5233c9bb7f84d4784a1a688df1c4d52c1c6c5
7
- data.tar.gz: e4b9d252e988795eb3888cdd4c01849f427857d2ac2db4210627b2745df4a9955528ca742492dc34009b2b60bc28f848a5b2537b9b24c1a3beb344a9bb9559ce
6
+ metadata.gz: e1457e174fcfdf5ceb54dc2c9090a37bb03af3c47882f04a046550f19b1687112cfd073ac9b75199ad4db8615ab13790b5ed47ee39772a582b3299dc3070fa82
7
+ data.tar.gz: afdcc985bc2f96201032af0c9ef78ceb2eb7387499ad0621c775fcfbd0d6a24e315b619fba59cfa2503432d626dcc209cffc754a6498f548091cac415338c9b1
@@ -21,10 +21,9 @@
21
21
  //= require select2
22
22
  //= require fixedsticky
23
23
 
24
- //= require flot/excanvas
25
- //= require flot/jquery.flot
26
- //= require flot/jquery.flot.time
27
- //= require flot/jquery.flot.selection
24
+ //= require jquery.flot
25
+ //= require jquery.flot.time
26
+ //= require jquery.flot.selection
28
27
 
29
28
  //= require batch_edit
30
29
  //
@@ -33,9 +33,10 @@
33
33
  padding-bottom: $padding-large-vertical;
34
34
  }
35
35
 
36
- #footer {
36
+ footer.navbar {
37
37
  padding-bottom: 0;
38
38
  margin-bottom: 0;
39
+ border-radius: 0;
39
40
  }
40
41
 
41
42
  label.required, span.required {
@@ -3,6 +3,7 @@ module Sufia
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
+ before_action :authenticate_user!
6
7
  before_action :load_proxy_deposit_request, only: :create
7
8
  load_and_authorize_resource :proxy_deposit_request, parent: false, except: :index
8
9
  before_action :authorize_depositor_by_id, only: [:new, :create]
@@ -7,6 +7,6 @@ class ErrorsController < ApplicationController
7
7
 
8
8
  def render_routing_error(exception)
9
9
  logger.error("Rendering 404 page due to exception: #{exception.inspect} - #{exception.backtrace if exception.respond_to? :backtrace}")
10
- render template: '/error/404', layout: "error", formats: [:html], status: 404
10
+ render '404', layout: "error", status: 404
11
11
  end
12
12
  end
@@ -7,6 +7,7 @@ module Sufia
7
7
  # the search query. While at the same time allowing us not to return Collections
8
8
  # when a work in the collection matches the query.
9
9
  solr_doc[Solrizer.solr_name('file_set_ids', :symbol)] = solr_doc[Solrizer.solr_name('member_ids', :symbol)]
10
+ solr_doc[Solrizer.solr_name('resource_type', :facetable)] = object.resource_type
10
11
  end
11
12
  end
12
13
  end
@@ -1,16 +1,22 @@
1
1
  module Sufia
2
2
  class AdminStatsPresenter
3
- attr_reader :limit, :start_date, :end_date, :stats_filters
3
+ attr_reader :limit, :stats_filters
4
4
 
5
5
  def initialize(stats_filters, limit)
6
6
  @stats_filters = stats_filters
7
- @start_date = stats_filters[:start_date]
8
- @end_date = stats_filters[:end_date]
9
7
  @limit = limit
10
8
  end
11
9
 
10
+ def start_date
11
+ @start_date ||= Time.zone.parse(stats_filters[:start_date]).beginning_of_day if stats_filters[:start_date].present?
12
+ end
13
+
14
+ def end_date
15
+ @end_date ||= Time.zone.parse(stats_filters[:end_date]).end_of_day if stats_filters[:end_date].present?
16
+ end
17
+
12
18
  def depositors
13
- @depositors ||= Sufia::Admin::DepositorStats.new(start_date, end_date).depositors
19
+ @depositors ||= Sufia::Statistics::Depositors::Summary.new(start_date, end_date).depositors
14
20
  end
15
21
 
16
22
  def recent_users
@@ -18,15 +24,15 @@ module Sufia
18
24
  end
19
25
 
20
26
  def active_users
21
- @active_users ||= stats.top_depositors
27
+ @active_users ||= Sufia::Statistics::Works::ByDepositor.new(limit).query
22
28
  end
23
29
 
24
30
  def top_formats
25
- @top_formats ||= stats.top_formats
31
+ @top_formats ||= Sufia::Statistics::FileSets::ByFormat.new(limit).query
26
32
  end
27
33
 
28
- def files_count
29
- @files_count ||= stats.document_by_permission
34
+ def works_count
35
+ @works_count ||= Sufia::Statistics::Works::Count.new(start_date, end_date).by_permission
30
36
  end
31
37
 
32
38
  def users_count
@@ -37,16 +43,16 @@ module Sufia
37
43
  if start_date.blank?
38
44
  "unfiltered"
39
45
  elsif end_date.blank?
40
- "#{start_date} to #{Date.current}"
46
+ "#{start_date.to_date.to_formatted_s(:standard)} to #{Date.current.to_formatted_s(:standard)}"
41
47
  else
42
- "#{start_date} to #{end_date}"
48
+ "#{start_date.to_date.to_formatted_s(:standard)} to #{end_date.to_date.to_formatted_s(:standard)}"
43
49
  end
44
50
  end
45
51
 
46
52
  private
47
53
 
48
54
  def stats
49
- @stats ||= SystemStats.new(limit, start_date, end_date)
55
+ @stats ||= Sufia::Statistics::SystemStats.new(limit, start_date, end_date)
50
56
  end
51
57
  end
52
58
  end
@@ -4,4 +4,33 @@ class Sufia::CatalogSearchBuilder < Sufia::SearchBuilder
4
4
  :add_advanced_parse_q_to_solr,
5
5
  :show_works_or_works_that_contain_files
6
6
  ]
7
+
8
+ # show both works that match the query and works that contain files that match the query
9
+ def show_works_or_works_that_contain_files(solr_parameters)
10
+ return if solr_parameters[:q].blank?
11
+ solr_parameters[:user_query] = solr_parameters[:q]
12
+ solr_parameters[:q] = new_query
13
+ end
14
+
15
+ private
16
+
17
+ # the {!lucene} gives us the OR syntax
18
+ def new_query
19
+ "{!lucene}#{interal_query(dismax_query)} #{interal_query(join_for_works_from_files)}"
20
+ end
21
+
22
+ # the _query_ allows for another parser (aka dismax)
23
+ def interal_query(query_value)
24
+ "_query_:\"#{query_value}\""
25
+ end
26
+
27
+ # the {!dismax} causes the query to go against the query fields
28
+ def dismax_query
29
+ "{!dismax v=$user_query}"
30
+ end
31
+
32
+ # join from file id to work relationship solrized file_set_ids_ssim
33
+ def join_for_works_from_files
34
+ "{!join from=#{ActiveFedora.id_field} to=file_set_ids_ssim}#{dismax_query}"
35
+ end
7
36
  end
@@ -6,4 +6,11 @@ class Sufia::MyCollectionsSearchBuilder < Sufia::SearchBuilder
6
6
  :show_only_resources_deposited_by_current_user,
7
7
  :show_only_collections
8
8
  ]
9
+
10
+ def show_only_collections(solr_parameters)
11
+ solr_parameters[:fq] ||= []
12
+ solr_parameters[:fq] += [
13
+ ActiveFedora::SolrQueryBuilder.construct_query_for_rel(has_model: Collection.to_class_uri)
14
+ ]
15
+ end
9
16
  end
@@ -2,7 +2,13 @@
2
2
  class Sufia::MyHighlightsSearchBuilder < Sufia::SearchBuilder
3
3
  include Sufia::MySearchBuilderBehavior
4
4
 
5
- self.default_processor_chain += [
6
- :show_only_highlighted_works
7
- ]
5
+ self.default_processor_chain += [:show_only_highlighted_works]
6
+
7
+ def show_only_highlighted_works(solr_parameters)
8
+ ids = scope.current_user.trophies.pluck(:work_id)
9
+ solr_parameters[:fq] ||= []
10
+ solr_parameters[:fq] += [
11
+ ActiveFedora::SolrQueryBuilder.construct_query_for_ids(ids)
12
+ ]
13
+ end
8
14
  end
@@ -2,5 +2,12 @@
2
2
  class Sufia::MySharesSearchBuilder < Sufia::SearchBuilder
3
3
  include Sufia::MySearchBuilderBehavior
4
4
 
5
- self.default_processor_chain = default_processor_chain - [:filter_models] + [:show_only_shared_files]
5
+ self.default_processor_chain += [:show_only_shared_files]
6
+
7
+ def show_only_shared_files(solr_parameters)
8
+ solr_parameters[:fq] ||= []
9
+ solr_parameters[:fq] += [
10
+ "-" + ActiveFedora::SolrQueryBuilder.construct_query_for_rel(depositor: scope.current_user.user_key)
11
+ ]
12
+ end
6
13
  end
@@ -4,13 +4,6 @@ class Sufia::SearchBuilder < Blacklight::SearchBuilder
4
4
  include Hydra::AccessControlsEnforcement
5
5
  include CurationConcerns::SearchFilters
6
6
 
7
- def show_only_collections(solr_parameters)
8
- solr_parameters[:fq] ||= []
9
- solr_parameters[:fq] += [
10
- ActiveFedora::SolrQueryBuilder.construct_query_for_rel(has_model: Collection.to_class_uri)
11
- ]
12
- end
13
-
14
7
  def show_only_resources_deposited_by_current_user(solr_parameters)
15
8
  solr_parameters[:fq] ||= []
16
9
  solr_parameters[:fq] += [
@@ -24,56 +17,4 @@ class Sufia::SearchBuilder < Blacklight::SearchBuilder
24
17
  ActiveFedora::SolrQueryBuilder.construct_query_for_rel(has_model: ::GenericWork.to_class_uri)
25
18
  ]
26
19
  end
27
-
28
- def show_only_shared_files(solr_parameters)
29
- solr_parameters[:fq] ||= []
30
- solr_parameters[:fq] += [
31
- "-" + ActiveFedora::SolrQueryBuilder.construct_query_for_rel(depositor: scope.current_user.user_key)
32
- ]
33
- end
34
-
35
- def show_only_highlighted_works(solr_parameters)
36
- ids = scope.current_user.trophies.pluck(:work_id)
37
- solr_parameters[:fq] ||= []
38
- solr_parameters[:fq] += [
39
- ActiveFedora::SolrQueryBuilder.construct_query_for_ids(ids)
40
- ]
41
- end
42
-
43
- # Limits search results just to GenericWorks and collections
44
- # @param solr_parameters the current solr parameters
45
- # @param user_parameters the current user-submitted parameters
46
- def only_works_and_collections(solr_parameters)
47
- solr_parameters[:fq] ||= []
48
- solr_parameters[:fq] << "#{Solrizer.solr_name('has_model', :symbol)}:(\"GenericWork\" \"Collection\")"
49
- end
50
-
51
- # show both works that match the query and works that contain files that match the query
52
- def show_works_or_works_that_contain_files(solr_parameters)
53
- return if solr_parameters[:q].blank?
54
- solr_parameters[:user_query] = solr_parameters[:q]
55
- solr_parameters[:q] = new_query
56
- end
57
-
58
- protected
59
-
60
- # the {!lucene} gives us the OR syntax
61
- def new_query
62
- "{!lucene}#{interal_query(dismax_query)} #{interal_query(join_for_works_from_files)}"
63
- end
64
-
65
- # the _query_ allows for another parser (aka dismax)
66
- def interal_query(query_value)
67
- "_query_:\"#{query_value}\""
68
- end
69
-
70
- # the {!dismax} causes the query to go against the query fields
71
- def dismax_query
72
- "{!dismax v=$user_query}"
73
- end
74
-
75
- # join from file id to work relationship solrized file_set_ids_ssim
76
- def join_for_works_from_files
77
- "{!join from=#{ActiveFedora.id_field} to=file_set_ids_ssim}#{dismax_query}"
78
- end
79
20
  end
@@ -13,30 +13,47 @@ module Sufia
13
13
  # ` client_email: GOOGLE_OAUTH_CLIENT_EMAIL`
14
14
  # @return [Hash] A hash containing five keys: 'app_name', 'app_version', 'client_email', 'privkey_path', 'privkey_secret'
15
15
  def self.config
16
- @config ||= YAML.load(File.read(File.join(Rails.root, 'config', 'analytics.yml')))['analytics']
16
+ @config ||= load_config
17
17
  end
18
+ private_class_method :config
19
+
20
+ def self.load_config
21
+ filename = File.join(Rails.root, 'config', 'analytics.yml')
22
+ yaml = YAML.load(File.read(filename))
23
+ unless yaml
24
+ Rails.logger.error("Unable to fetch any keys from #{filename}.")
25
+ return
26
+ end
27
+ yaml.fetch('analytics')
28
+ end
29
+ private_class_method :load_config
18
30
 
19
31
  # Generate an OAuth2 token for Google Analytics
20
32
  # @return [OAuth2::AccessToken] An OAuth2 access token for GA
21
33
  def self.token
22
34
  scope = 'https://www.googleapis.com/auth/analytics.readonly'
23
- client = Google::APIClient.new(application_name: config['app_name'],
24
- application_version: config['app_version'])
25
- key = Google::APIClient::PKCS12.load_key(config['privkey_path'],
26
- config['privkey_secret'])
27
- service_account = Google::APIClient::JWTAsserter.new(config['client_email'], scope,
35
+ client = Google::APIClient.new(application_name: config.fetch('app_name'),
36
+ application_version: config.fetch('app_version'))
37
+ key = Google::APIClient::PKCS12.load_key(config.fetch('privkey_path'),
38
+ config.fetch('privkey_secret'))
39
+ service_account = Google::APIClient::JWTAsserter.new(config.fetch('client_email'),
40
+ scope,
28
41
  key)
29
42
  client.authorization = service_account.authorize
30
- oauth_client = OAuth2::Client.new('', '', authorize_url: 'https://accounts.google.com/o/oauth2/auth',
31
- token_url: 'https://accounts.google.com/o/oauth2/token')
43
+ oauth_client = OAuth2::Client.new('',
44
+ '',
45
+ authorize_url: 'https://accounts.google.com/o/oauth2/auth',
46
+ token_url: 'https://accounts.google.com/o/oauth2/token')
32
47
  OAuth2::AccessToken.new(oauth_client, client.authorization.access_token)
33
48
  end
49
+ private_class_method :token
34
50
 
35
51
  # Return a user object linked to a Google Analytics account
36
52
  # @return [Legato::User] A user account wit GA access
37
53
  def self.user
38
54
  Legato::User.new(token)
39
55
  end
56
+ private_class_method :user
40
57
 
41
58
  # Return a Google Analytics profile matching specified ID
42
59
  # @ return [Legato::Management::Profile] A user profile associated with GA
@@ -1,6 +1,6 @@
1
1
  module Sufia
2
2
  class QueryService
3
- # query to find generic files created during the time range
3
+ # query to find works created during the time range
4
4
  # @param [DateTime] start_datetime starting date time for range query
5
5
  # @param [DateTime] end_datetime ending date time for range query
6
6
  def find_by_date_created(start_datetime, end_datetime = nil)
@@ -0,0 +1,13 @@
1
+ module Sufia
2
+ module Statistics
3
+ module Collections
4
+ class OverTime < Statistics::OverTime
5
+ private
6
+
7
+ def relation
8
+ Collection
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,54 @@
1
+ # Gather information about the depositors who have contributed to the repository
2
+ module Sufia
3
+ module Statistics
4
+ module Depositors
5
+ class Summary
6
+ include Blacklight::SearchHelper
7
+
8
+ # @param [Time] start_date optionally specify the start date to gather the stats from
9
+ # @param [Time] end_date optionally specify the end date to gather the stats from
10
+ def initialize(start_date, end_date)
11
+ @start_dt = start_date
12
+ @end_dt = end_date
13
+ end
14
+
15
+ attr_accessor :start_dt, :end_dt
16
+
17
+ def depositors
18
+ # step through the array by twos to get each pair
19
+ results.map do |key, deposits|
20
+ user = ::User.find_by_user_key(key)
21
+ { key: key, deposits: deposits, user: user }
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ delegate :blacklight_config, to: CatalogController
28
+ delegate :depositor_field, to: DepositSearchBuilder
29
+
30
+ # results come from Solr in an array where the first item is the user and
31
+ # the second item is the count
32
+ # [ abc123, 55, ccczzz, 205 ]
33
+ # @return [#each] an enumerable object of tuples (user and count)
34
+ def results
35
+ facet_results = repository.search(query)
36
+ facet_results.facet_fields[depositor_field].each_slice(2)
37
+ end
38
+
39
+ def search_builder
40
+ DepositSearchBuilder.new([:include_depositor_facet], self)
41
+ end
42
+
43
+ # TODO: This can probably be pushed into the DepositSearchBuilder
44
+ def query
45
+ search_builder.merge(q: date_query).query
46
+ end
47
+
48
+ def date_query
49
+ Sufia::QueryService.new.build_date_query(start_dt, end_dt) unless start_dt.blank?
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,14 @@
1
+ module Sufia
2
+ module Statistics
3
+ module FileSets
4
+ class ByFormat < Statistics::TermQuery
5
+ private
6
+
7
+ # Returns 'file_format_sim'
8
+ def index_key
9
+ Solrizer.solr_name('file_format', :facetable)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -1,7 +1,7 @@
1
1
  module Sufia
2
2
  module Statistics
3
3
  # An abstract class for generating cumulative graphs
4
- # you must provide a `point` method in the concrete class
4
+ # you must provide a `relation` method in the concrete class
5
5
  class OverTime
6
6
  # @param [Fixnum] delta_x change in x (in days)
7
7
  # @param [Time] x_min minimum date
@@ -24,12 +24,20 @@ module Sufia
24
24
 
25
25
  private
26
26
 
27
+ def point(min, max)
28
+ relation.where(query(min, max)).count
29
+ end
30
+
31
+ def query(min, max)
32
+ QueryService.new.build_date_query(min, max)
33
+ end
34
+
27
35
  def size
28
36
  ((@x_max - @x_min) / @delta_x.days.to_i).ceil
29
37
  end
30
38
 
31
- def point
32
- raise NotImplementedError, "Implement the point method in a base class"
39
+ def relation
40
+ raise NotImplementedError, "Implement the relation method in a concrete class"
33
41
  end
34
42
  end
35
43
  end