rails-xapi 0.1.2

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 (53) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +152 -0
  4. data/Rakefile +8 -0
  5. data/app/controllers/rails_xapi/application_controller.rb +7 -0
  6. data/app/helpers/rails_xapi/application_helper.rb +26 -0
  7. data/app/jobs/rails_xapi/application_job.rb +4 -0
  8. data/app/jobs/rails_xapi/create_statement_job.rb +11 -0
  9. data/app/mailers/rails_xapi/application_mailer.rb +6 -0
  10. data/app/models/concerns/serializable.rb +14 -0
  11. data/app/models/rails_xapi/account.rb +27 -0
  12. data/app/models/rails_xapi/activity_definition.rb +143 -0
  13. data/app/models/rails_xapi/actor.rb +227 -0
  14. data/app/models/rails_xapi/application_record.rb +5 -0
  15. data/app/models/rails_xapi/context.rb +163 -0
  16. data/app/models/rails_xapi/context_activity.rb +37 -0
  17. data/app/models/rails_xapi/errors/xapi_error.rb +4 -0
  18. data/app/models/rails_xapi/extension.rb +29 -0
  19. data/app/models/rails_xapi/group_member.rb +21 -0
  20. data/app/models/rails_xapi/interaction_activity.rb +103 -0
  21. data/app/models/rails_xapi/interaction_component.rb +34 -0
  22. data/app/models/rails_xapi/object.rb +150 -0
  23. data/app/models/rails_xapi/result.rb +174 -0
  24. data/app/models/rails_xapi/statement.rb +62 -0
  25. data/app/models/rails_xapi/validators/language_map_validator.rb +64 -0
  26. data/app/models/rails_xapi/verb.rb +260 -0
  27. data/app/services/application_service.rb +16 -0
  28. data/app/services/rails_xapi/query.rb +217 -0
  29. data/app/services/rails_xapi/statement_creator.rb +53 -0
  30. data/app/views/layouts/rails_xapi/application.html.erb +12 -0
  31. data/config/locales/rails_xapi.en.yml +43 -0
  32. data/config/locales/rails_xapi.fr.yml +233 -0
  33. data/config/routes.rb +2 -0
  34. data/db/migrate/20240716144226_create_rails_xapi_actors.rb +16 -0
  35. data/db/migrate/20240716144227_create_rails_xapi_accounts.rb +15 -0
  36. data/db/migrate/20240716144228_create_rails_xapi_verbs.rb +14 -0
  37. data/db/migrate/20240716144229_create_rails_xapi_objects.rb +16 -0
  38. data/db/migrate/20240716144230_create_rails_xapi_activity_definitions.rb +17 -0
  39. data/db/migrate/20240716144231_create_rails_xapi_extensions.rb +13 -0
  40. data/db/migrate/20240716144232_create_rails_xapi_statements.rb +19 -0
  41. data/db/migrate/20240716144233_create_rails_xapi_results.rb +21 -0
  42. data/db/migrate/20240716144234_create_rails_xapi_contexts.rb +23 -0
  43. data/db/migrate/20240716144235_create_rails_xapi_context_activities.rb +16 -0
  44. data/db/migrate/20240716144236_create_rails_xapi_group_members.rb +16 -0
  45. data/db/migrate/20250522093846_create_rails_xapi_interaction_activities.rb +15 -0
  46. data/db/migrate/20250522122830_create_rails_xapi_interaction_component.rb +16 -0
  47. data/lib/rails-xapi/configuration.rb +13 -0
  48. data/lib/rails-xapi/engine.rb +28 -0
  49. data/lib/rails-xapi/version.rb +5 -0
  50. data/lib/rails-xapi.rb +16 -0
  51. data/lib/tasks/auto_annotate_models.rake +62 -0
  52. data/lib/tasks/rails-xapi_tasks.rake +4 -0
  53. metadata +122 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6f7f793c9d2d10b489f20f41d63f0687b2d38ad974792cd12600a98846634178
4
+ data.tar.gz: cd86c11505ed01f92f714949a1c34fd1ed12de12cc170e62409189b4885b3a77
5
+ SHA512:
6
+ metadata.gz: 1b17754744f9266c1163bc1a38a1b48559b4e349de9d586216de28a097b904a4983f47c447895e4fe20040160d8779a21490dad14fe7eddde91a4b278c2d9668
7
+ data.tar.gz: d32ac593f6b50267cbfc491cd2e0a88cd79cb616f7d017f4822149ce50f851d0c74c9cf0c8a79ff4fc137c49f5741fee0e9a83c6e97d720909cf38fadec649a0
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Hipjea
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # Rails xAPI
2
+
3
+ This gem is a Rails engine that allows the validation of data from an xAPI statement. It enables the storage of xAPI statements in relational tables.
4
+
5
+ [![Actions Status](https://github.com/fondation-unit/rails-xapi/actions/workflows/ci.yml/badge.svg)](https://github.com/fondation-unit/rails-xapi/actions/workflows/ci.yml/)
6
+
7
+ > [!IMPORTANT]
8
+ > This is an ongoing development. The documentation will be provided as it becomes available.
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ ```ruby
15
+ gem "rails-xapi", git: "https://github.com/fondation-unit/rails-xapi"
16
+ ```
17
+
18
+ And then execute:
19
+
20
+ ```bash
21
+ $ bundle
22
+ ```
23
+
24
+ Create the migration files:
25
+
26
+ ```bash
27
+ $ bin/rails rails_xapi:install:migrations
28
+ ```
29
+
30
+ Mount the engine in `config/routes.rb`:
31
+
32
+ ```ruby
33
+ mount RailsXapi::Engine, at: "rails-xapi"
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ### Statement creation
39
+
40
+ Example usage of the `RailsXapi::StatementCreator` service:
41
+
42
+ ```ruby
43
+ user_name = "#{user.firstname} #{user.lastname}"
44
+
45
+ data = {
46
+ actor: {
47
+ objectType: "Agent",
48
+ name: user_name,
49
+ mbox: "mailto:#{user.email}",
50
+ account: {
51
+ homePage: "http://example.com/some_user_homepage/#{user&.id}",
52
+ name: user_name
53
+ }
54
+ },
55
+ verb: {
56
+ id: "https://brindlewaye.com/xAPITerms/verbs/loggedin/"
57
+ },
58
+ object: {
59
+ id: new_user_session_url,
60
+ definition: {
61
+ name: {
62
+ "en-US" => "log in"
63
+ },
64
+ description: {
65
+ "en-US" => "User signed in"
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ RailsXapi::StatementCreator.create(data)
72
+ ```
73
+
74
+ ### Data query
75
+
76
+ Ready-to-use queries are available in the [app/services/rails_xapi/query.rb](app/services/rails_xapi/query.rb) class.
77
+
78
+ | Query symbol | Description |
79
+ | ---------------------------------- | --------------------------------------------------------- |
80
+ | `:statement` | Retrieve a single statement by its ID |
81
+ | `:statements_by_object_and_actors` | Get statements by object IDand actor emails |
82
+ | `:verb_ids` | Get list of unique verb IDs |
83
+ | `:verb_displays` | Get list of unique verb display values |
84
+ | `:verbs` | Get hash of unique verbs with ID and display values |
85
+ | `:actor_by_email` | Find statements by actor's email |
86
+ | `:actor_by_mbox` | Find statements by actor's mbox |
87
+ | `:actor_by_account_homepage` | Find statements by actor's account homepage |
88
+ | `:actor_by_openid` | Find statements by actor's openID |
89
+ | `:actor_by_mbox_sha1sum` | Find statements by actor's mbox SHA1 sum |
90
+ | `:user_statements_per_month` | Retrieve actor's statements for a specific month/year |
91
+ | `:per_month` | Group given records by creation date for a specific month |
92
+ | `:month_graph_data` | Create date/count array of data for a month |
93
+
94
+ Example of usage:
95
+
96
+ ```ruby
97
+ def create_statement
98
+ data = {
99
+ actor: {
100
+ objectType: "Agent",
101
+ name: "John Doe",
102
+ mbox: "mailto:example@localhost.com",
103
+ account: {
104
+ homePage: "http://example.com/some_user_homepage/1",
105
+ name: "JohnDoe#1"
106
+ }
107
+ },
108
+ verb: {
109
+ id: "https://brindlewaye.com/xAPITerms/verbs/loggedin/"
110
+ },
111
+ object: {
112
+ id: "http://localhost:3000/new_user_session",
113
+ definition: {
114
+ name: {
115
+ "en-GB" => "login"
116
+ },
117
+ description: {
118
+ "en-US" => "User signed in."
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ statement = RailsXapi::StatementCreator.create(data)
125
+ redirect_to statement_show_path(id: statement[:statement][:id])
126
+ end
127
+
128
+ def statement_show
129
+ @statement = RailsXapi::Query.call(
130
+ query: :statement,
131
+ args: params[:id]
132
+ )
133
+ end
134
+
135
+ def logs_per_month(year = Date.current.year, month = Date.current.month)
136
+ RailsXapi::Query.call(
137
+ query: :user_statements_per_month,
138
+ args: [{mbox: "mailto:#{email}"}, year, month]
139
+ )
140
+ end
141
+ ```
142
+
143
+ ## Test
144
+
145
+ ```bash
146
+ bundle exec rails db:schema:load RAILS_ENV=test
147
+ bundle exec rspec spec/
148
+ ```
149
+
150
+ ## License
151
+
152
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsXapi
4
+ class ApplicationController < ActionController::Base
5
+ protect_from_forgery with: :exception
6
+ end
7
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsXapi
4
+ module ApplicationHelper
5
+ # Output the duration ISO 8601 in minutes.
6
+ def duration_to_minutes(duration)
7
+ sprintf("%.2f", ActiveSupport::Duration.parse(duration)&.in_minutes)
8
+ end
9
+
10
+ # Output the value of a JSON row in a specific locale.
11
+ def json_value_for_locale(json_str, locale = I18n.locale)
12
+ hash = JSON.parse(json_str)
13
+ result = hash.select { |key, _value| key.include?(locale.to_s) }
14
+ result.values.first.to_s || hash.first.value.to_s
15
+ rescue
16
+ json_str
17
+ end
18
+
19
+ # Output the result score as a percentage.
20
+ def result_success_rate(result)
21
+ return nil if result.score_raw.blank? || result.score_max.blank?
22
+
23
+ ((result.score_raw.to_f / result.score_max.to_f) * 100).to_i
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,4 @@
1
+ module RailsXapi
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RailsXapi::CreateStatementJob < ApplicationJob
4
+ queue_as :default
5
+
6
+ def perform(statement)
7
+ statement.save if statement.present?
8
+
9
+ { status: 200, statement: statement }
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ module RailsXapi
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Serializable
4
+ extend ActiveSupport::Concern
5
+
6
+ LATIN_LETTERS = "a-zA-ZÀ-ÖØ-öø-ÿœ"
7
+ LATIN_LETTERS_REGEX = /[^#{LATIN_LETTERS}\s-]/i
8
+
9
+ included do
10
+ def serialized_value(data)
11
+ data.is_a?(Hash) ? data.to_json : data.to_s
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Represents an account with home_page and name.
4
+ class RailsXapi::Account < ApplicationRecord
5
+ require "uri"
6
+
7
+ belongs_to :actor, class_name: "RailsXapi::Actor", dependent: :destroy
8
+
9
+ def homePage=(value)
10
+ # We need to match the camel case notation from JSON data.
11
+ self.home_page = value
12
+ end
13
+ end
14
+
15
+ # == Schema Information
16
+ #
17
+ # Table name: rails_xapi_accounts
18
+ #
19
+ # id :integer not null, primary key
20
+ # home_page :string not null
21
+ # name :string not null
22
+ # actor_id :bigint not null
23
+ #
24
+ # Indexes
25
+ #
26
+ # index_rails_xapi_accounts_on_actor_id (actor_id)
27
+ #
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The object optional activity definition.
4
+ # See: https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#activity-definition
5
+ class RailsXapi::ActivityDefinition < ApplicationRecord
6
+ include Serializable
7
+ include RailsXapi::ApplicationHelper
8
+
9
+ belongs_to :object, class_name: "RailsXapi::Object"
10
+ has_one :interaction_activity,
11
+ class_name: "RailsXapi::InteractionActivity",
12
+ dependent: :destroy
13
+ has_many :extensions, as: :extendable, dependent: :destroy
14
+
15
+ validates :activity_type,
16
+ format: {
17
+ with: %r{\A\w+://\S+\z},
18
+ message: I18n.t("rails_xapi.errors.must_be_a_valid_iri")
19
+ },
20
+ allow_blank: true
21
+
22
+ before_validation :set_name, :set_description
23
+ validates_with RailsXapi::Validators::LanguageMapValidator,
24
+ attributes: %i[name description]
25
+
26
+ def type
27
+ # Virtual attribute to bypass the Single Table Inheritance keyword.
28
+ activity_type
29
+ end
30
+
31
+ def type=(value)
32
+ # Store the `type` attribute into `activity_type` column to avoid
33
+ # reserved key-words issues.
34
+ self.activity_type = value
35
+ end
36
+
37
+ def moreInfo=(value)
38
+ # Match the camel case notation from JSON data.
39
+ self.more_info = value
40
+ end
41
+
42
+ def assign_from_json_definition(definition_hash)
43
+ return unless definition_hash.present?
44
+
45
+ normalized_hash = definition_hash.deep_stringify_keys
46
+ interaction_keys = RailsXapi::InteractionActivity::INTERACTION_KEYS
47
+
48
+ interaction_attrs = normalized_hash.slice(*interaction_keys)
49
+ core_attrs = normalized_hash.except(*interaction_keys)
50
+ # Assign base definition attributes
51
+ self.attributes = core_attrs
52
+
53
+ if normalized_hash["interactionType"].present?
54
+ # Assign InteractionActivity attributes
55
+ build_interaction_activity unless interaction_activity
56
+
57
+ interaction_activity.interaction_type =
58
+ interaction_attrs["interactionType"]
59
+ interaction_activity.correct_responses_pattern =
60
+ interaction_attrs["correctResponsesPattern"]
61
+ # Build the interaction_components association
62
+ interaction_activity.assign_interaction_components(interaction_attrs)
63
+
64
+ # Trigger the validation
65
+ unless interaction_activity.valid?
66
+ raise ActiveRecord::RecordInvalid, interaction_activity
67
+ end
68
+
69
+ interaction_activity.save!
70
+ end
71
+ end
72
+
73
+ def extensions=(extensions_data)
74
+ unless extensions_data.is_a?(Hash)
75
+ raise RailsXapi::Errors::XapiError,
76
+ I18n.t(
77
+ "rails_xapi.errors.attribute_must_be_a_hash",
78
+ name: "extensions"
79
+ )
80
+ end
81
+
82
+ # Find any existing extension for the given activity definition.
83
+ exts = extensions.where(extendable_type: self.class.to_s, extendable_id: id)
84
+
85
+ # If none, build and save the extensions.
86
+ if exts.blank?
87
+ extensions_data.each do |iri, data|
88
+ extensions.build(iri: iri, value: serialized_value(data))
89
+ end
90
+ end
91
+ end
92
+
93
+ def as_json
94
+ { name: name, description: description, type: activity_type }.tap do |hash|
95
+ hash[:extensions] = extensions.as_json if extensions.present?
96
+ hash[:moreInfo] = more_info if more_info.present?
97
+ hash.merge!(interaction_activity.as_json) if interaction_activity.present?
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ def set_json_attribute(attribute)
104
+ value = send(attribute)
105
+ return if value.blank?
106
+
107
+ begin
108
+ value = JSON.parse(value.to_s.gsub("=>", ":"))
109
+ rescue JSON::ParserError => _
110
+ raise RailsXapi::Errors::XapiError,
111
+ I18n.t(
112
+ "rails_xapi.errors.attribute_must_be_a_valid_language_map",
113
+ name: attribute
114
+ )
115
+ end
116
+
117
+ self[attribute] = value.to_json
118
+ end
119
+
120
+ def set_name
121
+ set_json_attribute(:name)
122
+ end
123
+
124
+ def set_description
125
+ set_json_attribute(:description)
126
+ end
127
+ end
128
+
129
+ # == Schema Information
130
+ #
131
+ # Table name: rails_xapi_activity_definitions
132
+ #
133
+ # id :integer not null, primary key
134
+ # activity_type :string
135
+ # description :text
136
+ # more_info :text
137
+ # name :string
138
+ # object_id :string not null
139
+ #
140
+ # Indexes
141
+ #
142
+ # index_rails_xapi_activity_definitions_on_object_id (object_id)
143
+ #
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The Actor defines who performed the action.
4
+ # See: https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#242-actor
5
+ class RailsXapi::Actor < ApplicationRecord
6
+ require "uri"
7
+ include Serializable
8
+
9
+ OBJECT_TYPES = %w[Agent Group]
10
+
11
+ attr_accessor :objectType, :member
12
+
13
+ has_one :account, class_name: "RailsXapi::Account", dependent: :destroy
14
+ has_many :statements, class_name: "RailsXapi::Statement", dependent: :nullify
15
+ has_many :members, class_name: "RailsXapi::GroupMember", dependent: :destroy
16
+
17
+ validates :object_type, presence: true
18
+ validate :validate_actor_ifi_presence,
19
+ :validate_mbox,
20
+ :validate_mbox_sha1sum,
21
+ :validate_object_type,
22
+ :validate_openid
23
+
24
+ after_initialize :set_defaults
25
+ before_validation :normalize_actor
26
+ after_commit :create_members, if: :is_group?
27
+
28
+ # Build the Actor object from the given data and user email.
29
+ #
30
+ # @param [Hash] data The data used to build the actor object, including optional nested account data.
31
+ # @return [RailsXapi::Actor] The actor object initialized with the data.
32
+ def self.build_actor_from_data(data)
33
+ data = handle_account_data(data)
34
+
35
+ conditions = data.slice(:mbox, :mbox_sha1sum, :openid).compact
36
+ find_by(conditions) || create(data)
37
+ end
38
+
39
+ # Find an Actor by its identifiers or create a new one.
40
+ #
41
+ # @param [Hash] data The data to find or create the actor.
42
+ # @return [RailsXapi::Actor] The found or created actor object.
43
+ def self.by_iri_or_create(data)
44
+ data = handle_account_data(data)
45
+
46
+ actor =
47
+ find_or_create_by(
48
+ mbox: data[:mbox],
49
+ mbox_sha1sum: data[:mbox_sha1sum],
50
+ openid: data[:openid]
51
+ ) { |a| a.attributes = data }
52
+
53
+ unless actor.valid?
54
+ raise RailsXapi::Errors::XapiError,
55
+ I18n.t("rails_xapi.errors.invalid_actor")
56
+ end
57
+
58
+ actor
59
+ end
60
+
61
+ def validate_mbox
62
+ return if mbox.blank?
63
+
64
+ mbox_valid =
65
+ mbox.strip =~ /\Amailto:([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/
66
+ unless mbox_valid
67
+ raise RailsXapi::Errors::XapiError,
68
+ I18n.t("rails_xapi.errors.malformed_mbox", name: mbox)
69
+ end
70
+
71
+ true
72
+ end
73
+
74
+ # Overrides the Hash class method to camelize object_type, according to the xAPI specification.
75
+ # See: https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#part-two-experience-api-data
76
+ #
77
+ # @return [Hash] The actor hash with the camel-case version of object_type.
78
+ def as_json
79
+ {
80
+ objectType: object_type,
81
+ name: name,
82
+ mbox: mbox,
83
+ mbox_sha1sum: mbox_sha1sum,
84
+ account: account.as_json,
85
+ openid: openid
86
+ }.compact
87
+ end
88
+
89
+ private
90
+
91
+ def set_defaults
92
+ # We need to match the camel case notation from JSON data.
93
+ self.object_type =
94
+ objectType.presence || object_type.presence || OBJECT_TYPES.first
95
+ self.member = member.presence || nil
96
+ end
97
+
98
+ def is_group?
99
+ object_type === "Group"
100
+ end
101
+
102
+ # Normalizes the actor data.
103
+ #
104
+ # @param [Hash] actor The actor data.
105
+ # @return [Hash] The normalized actor data.
106
+ def normalize_actor
107
+ self.object_type = object_type.presence || OBJECT_TYPES.first
108
+
109
+ if name.present?
110
+ self.name =
111
+ name
112
+ .gsub(Serializable::LATIN_LETTERS_REGEX, "")
113
+ .to_s
114
+ .humanize
115
+ .gsub(/\b('?[#{Serializable::LATIN_LETTERS}])/o) do
116
+ Regexp.last_match(1).capitalize
117
+ end
118
+ end
119
+
120
+ self.mbox = mbox.strip.downcase if mbox.present?
121
+ end
122
+
123
+ # Find an Account by its identifier or create a new one and set the actor's data.
124
+ #
125
+ # @param [Hash] data The data to find or create the account.
126
+ # @return [Hash] The actor's data.
127
+ private_class_method def self.handle_account_data(data)
128
+ if (account_data = data[:account]).present?
129
+ account =
130
+ RailsXapi::Account.find_or_create_by(
131
+ home_page: account_data[:homePage]
132
+ ) { |a| a.name = account_data[:name] }
133
+
134
+ data[:account] = account
135
+ data[:name] ||= account_data[:name]
136
+ end
137
+
138
+ data
139
+ end
140
+
141
+ def validate_actor_ifi_presence
142
+ unless mbox.present? || mbox_sha1sum.present? || openid.present? ||
143
+ account.present?
144
+ raise RailsXapi::Errors::XapiError,
145
+ I18n.t("rails_xapi.errors.actor_ifi_must_be_present")
146
+ end
147
+ end
148
+
149
+ def validate_mbox_sha1sum
150
+ return if mbox_sha1sum.blank?
151
+
152
+ unless is_sha1?(mbox_sha1sum)
153
+ raise RailsXapi::Errors::XapiError,
154
+ I18n.t("rails_xapi.errors.malformed_mbox_sha1sum")
155
+ end
156
+
157
+ true
158
+ end
159
+
160
+ def validate_object_type
161
+ object_type_valid = OBJECT_TYPES.include?(object_type)
162
+ unless object_type_valid
163
+ raise RailsXapi::Errors::XapiError,
164
+ I18n.t(
165
+ "rails_xapi.errors.invalid_actor_object_type",
166
+ name: object_type
167
+ )
168
+ end
169
+
170
+ true
171
+ end
172
+
173
+ def validate_openid
174
+ return if openid.blank?
175
+
176
+ uri = URI.parse(openid)
177
+ is_valid_openid_uri = uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
178
+ unless is_valid_openid_uri
179
+ raise RailsXapi::Errors::XapiError,
180
+ I18n.t("rails_xapi.errors.malformed_openid_uri", uri: openid)
181
+ end
182
+
183
+ true
184
+ end
185
+
186
+ # Produces the hex-encoded SHA1 hash of the actor mailto.
187
+ #
188
+ # @param [String] mbox The mbox clear value to be encoded.
189
+ # @return [Boolean] True if the value is matching, false otherwise.
190
+ def is_sha1?(str)
191
+ # SHA-1 hash is a 40-character hexadecimal string consisting of numbers 0-9 and letters a-f.
192
+ # We also handle the case with an optional "sha1" prefix.
193
+ !!(str =~ /^(sha1:)?[0-9a-f]{40}$/i)
194
+ end
195
+
196
+ # Create members in the case of a "Group" objectType.
197
+ def create_members
198
+ # We should end the function here when we create a group without members (ex: a team in the context object).
199
+ return if member.blank?
200
+
201
+ if id.blank?
202
+ raise RailsXapi::Errors::XapiError,
203
+ I18n.t("rails_xapi.errors.failed_to_create_group_members")
204
+ end
205
+
206
+ member.each do |m|
207
+ new_actor = RailsXapi::Actor.by_iri_or_create(m)
208
+ RailsXapi::GroupMember.create(group_id: id, actor_id: new_actor.id)
209
+ rescue => _
210
+ raise RailsXapi::Errors::XapiError,
211
+ I18n.t("rails_xapi.errors.failed_to_create_member", member: m)
212
+ end
213
+ end
214
+ end
215
+
216
+ # == Schema Information
217
+ #
218
+ # Table name: rails_xapi_actors
219
+ #
220
+ # id :integer not null, primary key
221
+ # mbox :string
222
+ # mbox_sha1sum :string
223
+ # name :string
224
+ # object_type :string
225
+ # openid :string
226
+ # created_at :datetime not null
227
+ #
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RailsXapi::ApplicationRecord < ActiveRecord::Base
4
+ self.abstract_class = true
5
+ end