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
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Represents a result containing response, success, and score data.
4
+ class RailsXapi::Result < ApplicationRecord
5
+ require "active_support/duration"
6
+ require "uri"
7
+ include Serializable
8
+
9
+ belongs_to :statement, class_name: "RailsXapi::Statement", dependent: :destroy
10
+ has_many :extensions, as: :extendable, dependent: :destroy
11
+
12
+ attr_reader :duration_in_seconds
13
+
14
+ validates :score_scaled,
15
+ numericality: {
16
+ greater_than_or_equal_to: -1,
17
+ less_than_or_equal_to: 1
18
+ },
19
+ allow_nil: true
20
+ validate :completion_attribute_must_be_boolean, if: -> { completion.present? }
21
+ validate :success_attribute_must_be_boolean, if: -> { success.present? }
22
+ validate :correct_duration, if: -> { duration.present? }
23
+
24
+ before_validation :calculate_scaled
25
+
26
+ # Store the score object in the results table for convenience reasons.
27
+ #
28
+ # @param [Hash] value The result hash values.
29
+ def score=(value)
30
+ validate_score(value)
31
+
32
+ self.score_scaled = value[:scaled]
33
+ self.score_raw = value[:raw]
34
+ self.score_min = value[:min]
35
+ self.score_max = value[:max]
36
+ end
37
+
38
+ def score
39
+ {
40
+ scaled: score_scaled,
41
+ raw: score_raw,
42
+ min: score_min,
43
+ max: score_max
44
+ }.compact
45
+ end
46
+
47
+ # Transform a duration in seconds into a ISO 8601 string.
48
+ # This is an optional attribute meant to bring more convenience for some systems.
49
+ #
50
+ # @param [String|Number] value The duration in seconds.
51
+ def duration_in_seconds=(value)
52
+ self.duration =
53
+ ActiveSupport::Duration.build(value).iso8601 if value.present?
54
+ end
55
+
56
+ def extensions=(extensions_data)
57
+ unless extensions_data.is_a?(Hash)
58
+ raise RailsXapi::Errors::XapiError,
59
+ I18n.t(
60
+ "rails_xapi.errors.attribute_must_be_a_hash",
61
+ name: "extensions"
62
+ )
63
+ end
64
+
65
+ extensions_data.each do |iri, data|
66
+ extensions.build(iri: iri, value: serialized_value(data))
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ # Validations in regard to the score
73
+ # See: https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#2451-score
74
+ #
75
+ # @param [Hash] value The result's score hash values.
76
+ def validate_score(value)
77
+ if value[:scaled].present? && !value[:scaled]&.between?(-1, 1)
78
+ raise RailsXapi::Errors::XapiError,
79
+ I18n.t(
80
+ "rails_xapi.errors.invalid_score_value",
81
+ value: I18n.t("rails_xapi.validations.score.scaled")
82
+ )
83
+ end
84
+
85
+ min_value = value[:min].to_i if value[:min].present?
86
+ max_value = value[:max].to_i if value[:max].present?
87
+
88
+ if value[:raw].present? &&
89
+ !value[:raw]&.between?(
90
+ min_value || -Float::INFINITY,
91
+ max_value || Float::INFINITY
92
+ )
93
+ raise RailsXapi::Errors::XapiError,
94
+ I18n.t(
95
+ "rails_xapi.errors.invalid_score_value",
96
+ value: I18n.t("rails_xapi.validations.score.raw")
97
+ )
98
+ end
99
+
100
+ if max_value.present? && min_value && min_value >= max_value
101
+ raise RailsXapi::Errors::XapiError,
102
+ I18n.t(
103
+ "rails_xapi.errors.invalid_score_value",
104
+ value: I18n.t("rails_xapi.validations.score.min")
105
+ )
106
+ end
107
+ end
108
+
109
+ # Validates the duration accordign to the specifications.
110
+ # See: https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#46-iso-8601-durations
111
+ #
112
+ # @param [String] duration The duration string to validate.
113
+ # @return [ActiveSupport::Duration::ISO8601Parser::ParsingError] If invalid string is provided.
114
+ def correct_duration
115
+ ActiveSupport::Duration.parse(duration) if duration.present?
116
+ end
117
+
118
+ def completion_attribute_must_be_boolean
119
+ unless [true, false].include?(completion)
120
+ raise RailsXapi::Errors::XapiError,
121
+ I18n.t(
122
+ "rails_xapi.errors.invalid_score_value",
123
+ value:
124
+ I18n.t(
125
+ "rails_xapi.errors.wrong_attribute_type",
126
+ name: "completion",
127
+ value: completion
128
+ )
129
+ )
130
+ end
131
+ end
132
+
133
+ def success_attribute_must_be_boolean
134
+ unless [true, false].include?(success)
135
+ raise RailsXapi::Errors::XapiError,
136
+ I18n.t(
137
+ "rails_xapi.errors.invalid_score_value",
138
+ value:
139
+ I18n.t(
140
+ "rails_xapi.errors.wrong_attribute_type",
141
+ name: "success",
142
+ value: success
143
+ )
144
+ )
145
+ end
146
+ end
147
+
148
+ # Automatically set a score_scaled if not provided.
149
+ def calculate_scaled
150
+ return if score_scaled.present? || score_max.blank? || score_raw.blank?
151
+
152
+ self.score_scaled = (score_raw.to_f / score_max.to_f)
153
+ end
154
+ end
155
+
156
+ # == Schema Information
157
+ #
158
+ # Table name: rails_xapi_results
159
+ #
160
+ # id :integer not null, primary key
161
+ # completion :boolean default(FALSE)
162
+ # duration :string
163
+ # response :text
164
+ # score_max :integer
165
+ # score_min :integer
166
+ # score_raw :integer
167
+ # score_scaled :decimal(3, 2)
168
+ # success :boolean default(FALSE)
169
+ # statement_id :bigint not null
170
+ #
171
+ # Indexes
172
+ #
173
+ # index_rails_xapi_results_on_statement_id (statement_id)
174
+ #
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Statements are the evidence for any sort of experience or event which is to be tracked in xAPI.
4
+ # See: https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#20-statements
5
+ class RailsXapi::Statement < ApplicationRecord
6
+ belongs_to :actor, class_name: "RailsXapi::Actor"
7
+ belongs_to :verb, class_name: "RailsXapi::Verb"
8
+ belongs_to :object, class_name: "RailsXapi::Object"
9
+ has_one :result, class_name: "RailsXapi::Result", dependent: :destroy
10
+ has_one :context, class_name: "RailsXapi::Context", dependent: :destroy
11
+
12
+ validate :actor_valid
13
+ validate :verb_valid
14
+ validate :object_valid
15
+
16
+ default_scope do
17
+ includes([:actor, { actor: :account }, :verb, :object, :context, :result])
18
+ end
19
+
20
+ def as_json
21
+ {
22
+ actor: actor.as_json,
23
+ verb: verb.as_json,
24
+ object: object.as_json
25
+ }.tap do |hash|
26
+ hash[:result] = result.as_json if result.present?
27
+ hash[:context] = context.as_json if context.present?
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def actor_valid
34
+ errors.add(:actor, "is invalid") if actor && !actor.valid?
35
+ end
36
+
37
+ def verb_valid
38
+ errors.add(:verb, "is invalid") if verb && !verb.valid?
39
+ end
40
+
41
+ def object_valid
42
+ errors.add(:object, "is invalid") if object && !object.valid?
43
+ end
44
+ end
45
+
46
+ # == Schema Information
47
+ #
48
+ # Table name: rails_xapi_statements
49
+ #
50
+ # id :integer not null, primary key
51
+ # timestamp :datetime
52
+ # created_at :datetime not null
53
+ # actor_id :string not null
54
+ # object_id :string not null
55
+ # verb_id :string not null
56
+ #
57
+ # Indexes
58
+ #
59
+ # index_rails_xapi_statements_on_actor_id (actor_id)
60
+ # index_rails_xapi_statements_on_object_id (object_id)
61
+ # index_rails_xapi_statements_on_verb_id (verb_id)
62
+ #
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsXapi
4
+ module Validators
5
+ class LanguageMapValidator < ActiveModel::Validator
6
+ LANGUAGE_MAP_REGEX = /\A[a-z]{2}(-[A-Z]{2})?\z/
7
+
8
+ def validate(record)
9
+ attributes_to_validate = options[:attributes] || []
10
+
11
+ attributes_to_validate.each do |attribute|
12
+ next unless record.respond_to?(attribute)
13
+
14
+ raw_value = record.public_send(attribute)
15
+ next unless raw_value.present?
16
+
17
+ validate_language_map_keys(record, attribute, raw_value)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def validate_language_map_keys(record, attribute, data)
24
+ data_hash =
25
+ case data
26
+ when String
27
+ # If it's a string, parse:
28
+ begin
29
+ JSON.parse(data)
30
+ rescue JSON::ParserError
31
+ record.errors.add(
32
+ attribute,
33
+ I18n.t("rails_xapi.errors.invalid_json")
34
+ )
35
+ return
36
+ end
37
+ when Hash
38
+ data
39
+ else
40
+ record.errors.add(attribute, "must be a JSON string or a Hash")
41
+ return
42
+ end
43
+
44
+ unless data_hash.is_a?(Hash)
45
+ raise RailsXapi::Errors::XapiError,
46
+ I18n.t("rails_xapi.errors.expected_hash", type: data_hash.class)
47
+ end
48
+
49
+ invalid_keys =
50
+ data_hash.keys.reject { |key| key.match?(LANGUAGE_MAP_REGEX) }
51
+
52
+ if invalid_keys.any?
53
+ record.errors.add(
54
+ attribute,
55
+ I18n.t(
56
+ "rails_xapi.errors.definition_description_invalid_keys",
57
+ values: invalid_keys.join(", ")
58
+ )
59
+ )
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The Verb defines the action between an Actor and an Activity.
4
+ # The systems reading the statements must use the verb IRI to infer meaning.
5
+ # See : https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#243-verb
6
+ class RailsXapi::Verb < ApplicationRecord
7
+ has_many :statements, class_name: "RailsXapi::Statement", dependent: :nullify
8
+
9
+ before_validation :set_display
10
+
11
+ validates :id,
12
+ presence: true,
13
+ format: {
14
+ with: %r{\A\w+://\S+\z},
15
+ message: I18n.t("rails_xapi.errors.must_be_a_valid_iri")
16
+ }
17
+ validates_with RailsXapi::Validators::LanguageMapValidator,
18
+ attributes: %i[display]
19
+
20
+ # Constants representing a mapping of xAPI activity verbs.
21
+ #
22
+ # The constant maps the activity verbs from several schemas
23
+ # to their corresponding human-readable output.
24
+ #
25
+ # @example Usage:
26
+ # VERBS_LIST["http://activitystrea.ms/schema/1.0/accept"] # => "accepted"
27
+ # VERBS_LIST["http://activitystrea.ms/schema/1.0/access"] # => "accessed"
28
+ #
29
+ # @note The list is build from https://registry.tincanapi.com/#home/verbs
30
+ VERBS_LIST = {
31
+ "http://activitystrea.ms/schema/1.0/accept" => "accepted",
32
+ "http://activitystrea.ms/schema/1.0/access" => "accessed",
33
+ "http://activitystrea.ms/schema/1.0/acknowledge" => "acknowledged",
34
+ "http://activitystrea.ms/schema/1.0/add" => "added",
35
+ "http://activitystrea.ms/schema/1.0/agree" => "agreed",
36
+ "http://activitystrea.ms/schema/1.0/append" => "appended",
37
+ "http://activitystrea.ms/schema/1.0/approve" => "approved",
38
+ "http://activitystrea.ms/schema/1.0/archive" => "archived",
39
+ "http://activitystrea.ms/schema/1.0/assign" => "assigned",
40
+ "http://activitystrea.ms/schema/1.0/at" => "was at",
41
+ "http://activitystrea.ms/schema/1.0/attach" => "attached",
42
+ "http://activitystrea.ms/schema/1.0/attend" => "attended",
43
+ "http://activitystrea.ms/schema/1.0/author" => "authored",
44
+ "http://activitystrea.ms/schema/1.0/authorize" => "authorized",
45
+ "http://activitystrea.ms/schema/1.0/borrow" => "borrowed",
46
+ "http://activitystrea.ms/schema/1.0/build" => "built",
47
+ "http://activitystrea.ms/schema/1.0/cancel" => "canceled",
48
+ "http://activitystrea.ms/schema/1.0/checkin" => "checked in",
49
+ "http://activitystrea.ms/schema/1.0/close" => "closed",
50
+ "http://activitystrea.ms/schema/1.0/complete" => "completed",
51
+ "http://activitystrea.ms/schema/1.0/confirm" => "confirmed",
52
+ "http://activitystrea.ms/schema/1.0/consume" => "consumed",
53
+ "http://activitystrea.ms/schema/1.0/create" => "created",
54
+ "http://activitystrea.ms/schema/1.0/delete" => "deleted",
55
+ "http://activitystrea.ms/schema/1.0/deliver" => "delivered",
56
+ "http://activitystrea.ms/schema/1.0/deny" => "denied",
57
+ "http://activitystrea.ms/schema/1.0/disagree" => "disagreed",
58
+ "http://activitystrea.ms/schema/1.0/dislike" => "disliked",
59
+ "http://activitystrea.ms/schema/1.0/experience" => "experienced",
60
+ "http://activitystrea.ms/schema/1.0/favorite" => "favorited",
61
+ "http://activitystrea.ms/schema/1.0/find" => "found",
62
+ "http://activitystrea.ms/schema/1.0/flag-as-inappropriate" =>
63
+ "flagged as inappropriate",
64
+ "http://activitystrea.ms/schema/1.0/follow" => "followed",
65
+ "http://activitystrea.ms/schema/1.0/give" => "gave",
66
+ "http://activitystrea.ms/schema/1.0/host" => "hosted",
67
+ "http://activitystrea.ms/schema/1.0/ignore" => "ignored",
68
+ "http://activitystrea.ms/schema/1.0/insert" => "inserted",
69
+ "http://activitystrea.ms/schema/1.0/install" => "installed",
70
+ "http://activitystrea.ms/schema/1.0/interact" => "interacted",
71
+ "http://activitystrea.ms/schema/1.0/invite" => "invited",
72
+ "http://activitystrea.ms/schema/1.0/join" => "joined",
73
+ "http://activitystrea.ms/schema/1.0/leave" => "left",
74
+ "http://activitystrea.ms/schema/1.0/like" => "liked",
75
+ "http://activitystrea.ms/schema/1.0/listen" => "listened",
76
+ "http://activitystrea.ms/schema/1.0/lose" => "lost",
77
+ "http://activitystrea.ms/schema/1.0/make-friend" => "made friend",
78
+ "http://activitystrea.ms/schema/1.0/open" => "opened",
79
+ "http://activitystrea.ms/schema/1.0/play" => "played",
80
+ "http://activitystrea.ms/schema/1.0/present" => "presented",
81
+ "http://activitystrea.ms/schema/1.0/purchase" => "purchased",
82
+ "http://activitystrea.ms/schema/1.0/qualify" => "qualified",
83
+ "http://activitystrea.ms/schema/1.0/read" => "read",
84
+ "http://activitystrea.ms/schema/1.0/receive" => "received",
85
+ "http://activitystrea.ms/schema/1.0/reject" => "rejected",
86
+ "http://activitystrea.ms/schema/1.0/remove" => "removed",
87
+ "http://activitystrea.ms/schema/1.0/remove-friend" => "removed friend",
88
+ "http://activitystrea.ms/schema/1.0/replace" => "replaced",
89
+ "http://activitystrea.ms/schema/1.0/request" => "requested",
90
+ "http://activitystrea.ms/schema/1.0/request-friend" => "requested friend",
91
+ "http://activitystrea.ms/schema/1.0/resolve" => "resolved",
92
+ "http://activitystrea.ms/schema/1.0/retract" => "retracted",
93
+ "http://activitystrea.ms/schema/1.0/return" => "returned",
94
+ "http://activitystrea.ms/schema/1.0/rsvp-maybe" => "RSVPed maybe",
95
+ "http://activitystrea.ms/schema/1.0/rsvp-no" => "rsvped no",
96
+ "http://activitystrea.ms/schema/1.0/rsvp-yes" => "rsvped yes",
97
+ "http://activitystrea.ms/schema/1.0/satisfy" => "satisfied",
98
+ "http://activitystrea.ms/schema/1.0/save" => "saved",
99
+ "http://activitystrea.ms/schema/1.0/schedule" => "scheduled",
100
+ "http://activitystrea.ms/schema/1.0/search" => "searched",
101
+ "http://activitystrea.ms/schema/1.0/sell" => "sold",
102
+ "http://activitystrea.ms/schema/1.0/send" => "sent",
103
+ "http://activitystrea.ms/schema/1.0/share" => "shared",
104
+ "http://activitystrea.ms/schema/1.0/sponsor" => "sponsored",
105
+ "http://activitystrea.ms/schema/1.0/start" => "started",
106
+ "http://activitystrea.ms/schema/1.0/stop-following" => "stopped following",
107
+ "http://activitystrea.ms/schema/1.0/submit" => "submitted",
108
+ "http://activitystrea.ms/schema/1.0/tag" => "tagged",
109
+ "http://activitystrea.ms/schema/1.0/terminate" => "terminated",
110
+ "http://activitystrea.ms/schema/1.0/tie" => "tied",
111
+ "http://activitystrea.ms/schema/1.0/unfavorite" => "unfavorited",
112
+ "http://activitystrea.ms/schema/1.0/unlike" => "unliked",
113
+ "http://activitystrea.ms/schema/1.0/unsatisfy" => "unsatisfied",
114
+ "http://activitystrea.ms/schema/1.0/unsave" => "unsaved",
115
+ "http://activitystrea.ms/schema/1.0/unshare" => "unshared",
116
+ "http://activitystrea.ms/schema/1.0/update" => "updated",
117
+ "http://activitystrea.ms/schema/1.0/use" => "used",
118
+ "http://activitystrea.ms/schema/1.0/watch" => "Watched",
119
+ "http://activitystrea.ms/schema/1.0/win" => "won",
120
+ "http://adlnet.gov/expapi/verbs/answered" => "answered",
121
+ "http://adlnet.gov/expapi/verbs/asked" => "asked",
122
+ "http://adlnet.gov/expapi/verbs/attempted" => "attempted",
123
+ "http://adlnet.gov/expapi/verbs/attended" => "attended",
124
+ "http://adlnet.gov/expapi/verbs/commented" => "commented",
125
+ "http://adlnet.gov/expapi/verbs/completed" => "completed",
126
+ "http://adlnet.gov/expapi/verbs/exited" => "exited",
127
+ "http://adlnet.gov/expapi/verbs/experienced" => "experienced",
128
+ "http://adlnet.gov/expapi/verbs/failed" => "failed",
129
+ "http://adlnet.gov/expapi/verbs/imported" => "imported",
130
+ "http://adlnet.gov/expapi/verbs/initialized" => "initialized",
131
+ "http://adlnet.gov/expapi/verbs/interacted" => "interacted",
132
+ "http://adlnet.gov/expapi/verbs/launched" => "launched",
133
+ "http://adlnet.gov/expapi/verbs/mastered" => "mastered",
134
+ "http://adlnet.gov/expapi/verbs/passed" => "passed",
135
+ "http://adlnet.gov/expapi/verbs/preferred" => "preferred",
136
+ "http://adlnet.gov/expapi/verbs/progressed" => "progressed",
137
+ "http://adlnet.gov/expapi/verbs/registered" => "registered",
138
+ "http://adlnet.gov/expapi/verbs/responded" => "responded",
139
+ "http://adlnet.gov/expapi/verbs/resumed" => "resumed",
140
+ "http://adlnet.gov/expapi/verbs/scored" => "scored",
141
+ "http://adlnet.gov/expapi/verbs/shared" => "shared",
142
+ "http://adlnet.gov/expapi/verbs/suspended" => "suspended",
143
+ "http://adlnet.gov/expapi/verbs/terminated" => "terminated",
144
+ "http://adlnet.gov/expapi/verbs/voided" => "voided",
145
+ "http://curatr3.com/define/verb/edited" => "edited",
146
+ "http://curatr3.com/define/verb/voted-down" => "voted down (with reason)",
147
+ "http://curatr3.com/define/verb/voted-up" => "voted up (with reason)",
148
+ "http://future-learning.info/xAPI/verb/pressed" => "pressed",
149
+ "http://future-learning.info/xAPI/verb/released" => "released",
150
+ "http://id.tincanapi.com/verb/adjourned" => "adjourned",
151
+ "http://id.tincanapi.com/verb/applauded" => "applauded",
152
+ "http://id.tincanapi.com/verb/arranged" => "arranged",
153
+ "http://id.tincanapi.com/verb/bookmarked" => "bookmarked",
154
+ "http://id.tincanapi.com/verb/called" => "called",
155
+ "http://id.tincanapi.com/verb/closed-sale" => "closed sale",
156
+ "http://id.tincanapi.com/verb/created-opportunity" => "created opportunity",
157
+ "http://id.tincanapi.com/verb/defined" => "defined",
158
+ "http://id.tincanapi.com/verb/disabled" => "disabled",
159
+ "http://id.tincanapi.com/verb/discarded" => "discarded",
160
+ "http://id.tincanapi.com/verb/downloaded" => "downloaded",
161
+ "http://id.tincanapi.com/verb/earned" => "earned",
162
+ "http://id.tincanapi.com/verb/enabled" => "enabled",
163
+ "http://id.tincanapi.com/verb/estimated-duration" =>
164
+ "estimated the duration",
165
+ "http://id.tincanapi.com/verb/expected" => "expected",
166
+ "http://id.tincanapi.com/verb/expired" => "expired",
167
+ "http://id.tincanapi.com/verb/focused" => "focused",
168
+ "http://id.tincanapi.com/verb/frame/entered" => "entered frame",
169
+ "http://id.tincanapi.com/verb/frame/exited" => "exited frame",
170
+ "http://id.tincanapi.com/verb/hired" => "hired",
171
+ "http://id.tincanapi.com/verb/interviewed" => "interviewed",
172
+ "http://id.tincanapi.com/verb/laughed" => "laughed",
173
+ "http://id.tincanapi.com/verb/marked-unread" => "unread",
174
+ "http://id.tincanapi.com/verb/mentioned" => "mentioned",
175
+ "http://id.tincanapi.com/verb/mentored" => "mentored",
176
+ "http://id.tincanapi.com/verb/paused" => "paused",
177
+ "http://id.tincanapi.com/verb/performed-offline" => "performed",
178
+ "http://id.tincanapi.com/verb/personalized" => "personalized",
179
+ "http://id.tincanapi.com/verb/previewed" => "previewed",
180
+ "http://id.tincanapi.com/verb/promoted" => "promoted",
181
+ "http://id.tincanapi.com/verb/rated" => "rated",
182
+ "http://id.tincanapi.com/verb/replied" => "replied",
183
+ "http://id.tincanapi.com/verb/replied-to-tweet" => "replied to tweet",
184
+ "http://id.tincanapi.com/verb/requested-attention" => "requested attention",
185
+ "http://id.tincanapi.com/verb/retweeted" => "retweeted",
186
+ "http://id.tincanapi.com/verb/reviewed" => "reviewed",
187
+ "http://id.tincanapi.com/verb/secured" => "secured",
188
+ "http://id.tincanapi.com/verb/selected" => "selected",
189
+ "http://id.tincanapi.com/verb/skipped" => "skipped",
190
+ "http://id.tincanapi.com/verb/talked-with" => "talked",
191
+ "http://id.tincanapi.com/verb/terminated-employment-with" =>
192
+ "terminated employment with",
193
+ "http://id.tincanapi.com/verb/tweeted" => "tweeted",
194
+ "http://id.tincanapi.com/verb/unfocused" => "unfocused",
195
+ "http://id.tincanapi.com/verb/unregistered" => "unregistered",
196
+ "http://id.tincanapi.com/verb/viewed" => "viewed",
197
+ "http://id.tincanapi.com/verb/voted-down" => "down voted",
198
+ "http://id.tincanapi.com/verb/voted-up" => "up voted",
199
+ "http://id.tincanapi.com/verb/was-assigned-job-title" =>
200
+ "was assigned job title",
201
+ "http://id.tincanapi.com/verb/was-hired-by" => "was hired by",
202
+ "http://risc-inc.com/annotator/verbs/annotated" => "annotated",
203
+ "http://risc-inc.com/annotator/verbs/modified" => "modified annotation",
204
+ "http://specification.openbadges.org/xapi/verbs/earned" =>
205
+ "earned an Open Badge",
206
+ "http://www.digital-knowledge.co.jp/tincanapi/verbs/drew" => "drew",
207
+ "http://www.tincanapi.co.uk/pages/verbs.html#cancelled_planned_learning" =>
208
+ "cancelled planned learning",
209
+ "http://www.tincanapi.co.uk/pages/verbs.html#planned_learning" => "planned",
210
+ "http://www.tincanapi.co.uk/verbs/enrolled_onto_learning_plan" =>
211
+ "enrolled onto learning plan",
212
+ "http://www.tincanapi.co.uk/verbs/evaluated" => "evaluated",
213
+ "https://brindlewaye.com/xAPITerms/verbs/added/" => "added",
214
+ "https://brindlewaye.com/xAPITerms/verbs/loggedin/" => "log in",
215
+ "https://brindlewaye.com/xAPITerms/verbs/loggedout/" => "log out",
216
+ "https://brindlewaye.com/xAPITerms/verbs/ran/" => "ran",
217
+ "https://brindlewaye.com/xAPITerms/verbs/removed/" => "removed",
218
+ "https://brindlewaye.com/xAPITerms/verbs/reviewed/" => "reviewed",
219
+ "https://brindlewaye.com/xAPITerms/verbs/walked/" => "walked"
220
+ }
221
+
222
+ def to_locale
223
+ loc = I18n.default_locale&.to_s || "en"
224
+ parsed = JSON.parse(display)
225
+ fallback = parsed&.first&.[](-1) || ""
226
+
227
+ parsed[loc] || fallback
228
+ end
229
+
230
+ def as_json
231
+ { id: id, display: display }.compact
232
+ end
233
+
234
+ private
235
+
236
+ def set_display
237
+ if display.present?
238
+ # Parse the data as JSON to store it
239
+ self.display = JSON.parse(display.gsub("=>", ":")).to_json
240
+ elsif VERBS_LIST.key?(id)
241
+ verb_list_id = VERBS_LIST[id]
242
+ self.display = { "en-US": verb_list_id }.to_json
243
+ else
244
+ raise RailsXapi::Errors::XapiError,
245
+ I18n.t("rails_xapi.errors.missing_verb_display")
246
+ end
247
+ end
248
+ end
249
+
250
+ # == Schema Information
251
+ #
252
+ # Table name: rails_xapi_verbs
253
+ #
254
+ # id :string not null, primary key
255
+ # display :string
256
+ #
257
+ # Indexes
258
+ #
259
+ # index_rails_xapi_verbs_on_id (id) UNIQUE
260
+ #
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationService
4
+ def self.call(*, &)
5
+ new(*, &).call
6
+ end
7
+
8
+ # Set the start_date to the first day of the given month and year,
9
+ # and the end_date to the last day of the givenn month.
10
+ def self.generate_start_date_end_date(year, month)
11
+ start_date = Date.new(year, month, 1)
12
+ end_date = start_date.end_of_month
13
+
14
+ [start_date, end_date]
15
+ end
16
+ end