mumuki-domain 6.1.5 → 6.2.0

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 (43) hide show
  1. checksums.yaml +5 -5
  2. data/Rakefile +35 -0
  3. data/app/models/assignment.rb +2 -5
  4. data/app/models/book.rb +6 -2
  5. data/app/models/concerns/syncable.rb +94 -0
  6. data/app/models/concerns/with_slug.rb +7 -7
  7. data/app/models/content.rb +1 -0
  8. data/app/models/course.rb +33 -4
  9. data/app/models/exam.rb +5 -1
  10. data/app/models/exercise.rb +8 -1
  11. data/app/models/guide.rb +3 -3
  12. data/app/models/invitation.rb +36 -11
  13. data/app/models/language.rb +5 -4
  14. data/app/models/message.rb +1 -1
  15. data/app/models/organization.rb +10 -5
  16. data/app/models/topic.rb +6 -1
  17. data/app/models/user.rb +17 -12
  18. data/db/migrate/20160701195105_add_login_methods_to_organization.rb +1 -1
  19. data/db/migrate/20181210131824_convert_course_invitation_into_fk.rb +6 -0
  20. data/lib/mumuki/domain/factories.rb +17 -0
  21. data/lib/mumuki/domain/factories/api_client_factory.rb +18 -0
  22. data/lib/mumuki/domain/factories/assignments_factory.rb +7 -0
  23. data/lib/mumuki/domain/factories/book_factory.rb +7 -0
  24. data/lib/mumuki/domain/factories/chapter_factory.rb +16 -0
  25. data/lib/mumuki/domain/factories/complement_factory.rb +6 -0
  26. data/lib/mumuki/domain/factories/course_factory.rb +9 -0
  27. data/lib/mumuki/domain/factories/discussion_factory.rb +8 -0
  28. data/lib/mumuki/domain/factories/exam_factory.rb +9 -0
  29. data/lib/mumuki/domain/factories/exercise_factory.rb +73 -0
  30. data/lib/mumuki/domain/factories/guide_factory.rb +30 -0
  31. data/lib/mumuki/domain/factories/invitation_factory.rb +7 -0
  32. data/lib/mumuki/domain/factories/lesson_factory.rb +7 -0
  33. data/lib/mumuki/domain/factories/login_settings_factory.rb +5 -0
  34. data/lib/mumuki/domain/factories/message_factory.rb +11 -0
  35. data/lib/mumuki/domain/factories/organization_factory.rb +26 -0
  36. data/lib/mumuki/domain/factories/topic_factory.rb +9 -0
  37. data/lib/mumuki/domain/factories/user_factory.rb +8 -0
  38. data/lib/mumuki/domain/locales/submission.en.yml +4 -0
  39. data/lib/mumuki/domain/locales/submission.es.yml +4 -0
  40. data/lib/mumuki/domain/locales/submission.pt.yml +4 -0
  41. data/lib/mumuki/domain/submission/console_submission.rb +1 -1
  42. data/lib/mumuki/domain/version.rb +1 -1
  43. metadata +61 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: cc25c9836058c10703a5f9a3479642ea20132c54
4
- data.tar.gz: 5586e6c506b9d28f27a866542cf91bea5e5a4aca
2
+ SHA256:
3
+ metadata.gz: ee36809b582014389c467d826cf8b882328db5ca64148dc86c61934de774cbb2
4
+ data.tar.gz: a015be5b71a6cb6be0b3b7f40704e5c7c0ff8391fc8e45d08db51420c71d9522
5
5
  SHA512:
6
- metadata.gz: dbe65fff7b93368ebfe18e4db33f585a21cbd886bc68d720bb5d54fd909636a5f2c01ce1b914bc4becd61bef75b4ca0d1d59cd7125f82da600979abab01c0fc2
7
- data.tar.gz: cb02d111698a9af4766a8bba94d9e3c0f3b42ffbebb7a6ae1fb59c56a0edcb729e7f2b17eedc635636af2bccd85f2c3aed72541c156d1d11581a0fe8ba3399ae
6
+ metadata.gz: 9281ce11cfab651b7a7c2be4f4d528a7ac29278c19eff71116adcb376c21ec6059bb874412599398da47cbe4702f2ec0a8a55865cbe80decf21df85402ca442b
7
+ data.tar.gz: 1183e2aeed8d70f425f1cc9751f95f987bfd7a8d88d0268bc6fe83983725f4f45b8ef03095b1d666b773e8964ad234355b7de4ef11eb4ca4d229764028b20164
@@ -0,0 +1,35 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Mumuki::Domain'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
18
+
19
+ load 'rails/tasks/engine.rake'
20
+ load 'rails/tasks/statistics.rake'
21
+
22
+ require 'bundler/gem_tasks'
23
+
24
+ require 'rspec/core'
25
+ require 'rspec/core/rake_task'
26
+
27
+ desc "Run all specs in spec directory (excluding plugin specs)"
28
+ RSpec::Core::RakeTask.new(:spec => 'app:db:test:prepare')
29
+
30
+ task default: :spec
31
+
32
+
33
+
34
+
35
+
@@ -49,7 +49,7 @@ class Assignment < ApplicationRecord
49
49
  end
50
50
 
51
51
  def notify!
52
- Mumukit::Nuntius.notify! 'submissions', to_resource_h unless Organization.current.silent?
52
+ Mumukit::Nuntius.notify! 'submissions', to_resource_h unless Organization.silenced?
53
53
  end
54
54
 
55
55
  def notify_to_accessible_organizations!
@@ -177,10 +177,7 @@ class Assignment < ApplicationRecord
177
177
  end
178
178
 
179
179
  def files
180
- language
181
- .directives_sections
182
- .split_sections(current_content)
183
- .map { |name, content| Mumuki::Domain::File.new name, content }
180
+ exercise.files_for(current_content)
184
181
  end
185
182
 
186
183
  private
@@ -22,14 +22,14 @@ class Book < Content
22
22
  user.try(:last_lesson)|| first_lesson
23
23
  end
24
24
 
25
+ after_save :reindex_usages!
26
+
25
27
  def import_from_resource_h!(resource_h)
26
28
  self.assign_attributes resource_h.except(:chapters, :complements, :id, :description)
27
29
  self.description = resource_h[:description]&.squeeze(' ')
28
30
 
29
31
  rebuild! resource_h[:chapters].map { |it| Topic.find_by!(slug: it).as_chapter_of(self) }
30
32
  rebuild_complements! resource_h[:complements].to_a.map { |it| Guide.find_by(slug: it)&.as_complement_of(self) }.compact
31
-
32
- Organization.where(book: self).each { |org| org.reindex_usages! }
33
33
  end
34
34
 
35
35
  def to_resource_h
@@ -51,6 +51,10 @@ class Book < Content
51
51
  [chapters, complements].flatten.each { |item| item.index_usage! organization }
52
52
  end
53
53
 
54
+ def reindex_usages!
55
+ Organization.where(book: self).each &:reindex_usages!
56
+ end
57
+
54
58
  ## Forking
55
59
 
56
60
  def fork_to!(organization, syncer)
@@ -0,0 +1,94 @@
1
+ # Mixin for objects that may
2
+ # be treated as `Mumukit::Sync` resources
3
+ #
4
+ # `Syncable` objects also get `Mumukit::Platform::Notifiable` by free
5
+ module Syncable
6
+ extend ActiveSupport::Concern
7
+ include Mumukit::Platform::Notifiable
8
+
9
+ # Generates a sync key
10
+ #
11
+ # :warning: This method is required by `Mumukit::Sync::Syncer#import!`
12
+ def sync_key
13
+ Mumukit::Sync.key sync_key_kind, sync_key_id
14
+ end
15
+
16
+ # Updates this object with the provided
17
+ # resource-hash. Fields not present on it are not modified
18
+ #
19
+ # :warning: This method is required by
20
+ #
21
+ # * `Mumukit::Sync::Syncer#import!`
22
+ # * `Mumukit::Sync::Syncer#import_all!`
23
+ # * `Mumukit::Sync::Syncer#locate_and_import!`
24
+ required :import_from_resource_h!
25
+
26
+ # Generates a resource-hash representation
27
+ #
28
+ # :warning: This method is required by
29
+ #
30
+ # * `Mumukit::Sync::Syncer#export!`
31
+ # * `Mumukit::Sync::Syncer#locate_and_export!`
32
+ required :to_resource_h
33
+
34
+ # :warning: This method is required by `Mumukit::Platform::Notifiable#notify!`
35
+ def platform_class_name
36
+ sync_key_kind.as_module_name
37
+ end
38
+
39
+ private
40
+
41
+ def sync_key_kind
42
+ self.class
43
+ end
44
+
45
+ def sync_key_id
46
+ self[self.class.sync_key_id_field]
47
+ end
48
+
49
+ class_methods do
50
+ # The name of the field used as id with `Mumukit::Sync#key`
51
+ required :sync_key_id_field
52
+
53
+ # :warning: This method is required by
54
+ #
55
+ # * `Mumukit::Sync::Syncer#import_all!`
56
+ # * `Mumukit::Sync::Syncer#locate_and_import!`
57
+ # * `Mumukit::Sync::Syncer#locate_and_export!`
58
+ def locate_resource(sync_key_id)
59
+ find_or_initialize_by sync_key_id_field => sync_key_id
60
+ end
61
+
62
+ # `locate_resource` is a helpful method that can be used
63
+ # outside the `Mumukit::Sync` context, thus this mixin provide
64
+ # a shorter alias
65
+ alias_method :locate, :locate_resource
66
+
67
+ # `locate!` is similar to `locate`, but fails instead of creating a
68
+ # a new object when not found
69
+ #
70
+ # This method is not required by `Mumukit::Sync`
71
+ def locate!(sync_key_id)
72
+ find_by! sync_key_id_field => sync_key_id
73
+ rescue ActiveRecord::RecordNotFound
74
+ raise ActiveRecord::RecordNotFound, "Coudn't find #{self.name} with #{sync_key_id_field}: #{sync_key_id}"
75
+ end
76
+
77
+ # Locates and imports a resource, extracting
78
+ # its key directly from the resource-hash.
79
+ #
80
+ # This class-message is not required by `Mumukit::Sync` but it is exposed
81
+ # for convenience when needing to import resources for whom we already
82
+ # have a resource-hash representation and we don't need to fetch
83
+ # them from a `Mumukit::Sync::Store`.
84
+ def import_from_resource_h!(resource_h)
85
+ locate_resource(extract_sync_key_id(resource_h)).tap do |it|
86
+ it.import_from_resource_h! resource_h
87
+ end
88
+ end
89
+
90
+ def extract_sync_key_id(resource_h)
91
+ resource_h[sync_key_id_field]
92
+ end
93
+ end
94
+ end
@@ -12,12 +12,6 @@ module WithSlug
12
12
  {organization: org, repository: repo}
13
13
  end
14
14
 
15
- ## Resource Protocol
16
-
17
- def sync_key
18
- Mumukit::Sync.key self.class.name.underscore.to_sym, slug
19
- end
20
-
21
15
  ## Copy and Rebase
22
16
 
23
17
  def rebase!(organization)
@@ -30,7 +24,7 @@ module WithSlug
30
24
 
31
25
  ## Filtering
32
26
 
33
- module ClassMethods
27
+ class_methods do
34
28
 
35
29
  def allowed(permissions)
36
30
  all.select { |it| permissions&.writer?(it.slug) }
@@ -45,6 +39,12 @@ module WithSlug
45
39
  find_by!(slug: "#{args[:organization]}/#{args[:repository]}")
46
40
  end
47
41
 
42
+ ## Resource Protocol
43
+
44
+ def sync_key_id_field
45
+ :slug
46
+ end
47
+
48
48
  def locate_resource(key)
49
49
  find_or_initialize_by(slug: key)
50
50
  end
@@ -1,6 +1,7 @@
1
1
  class Content < ApplicationRecord
2
2
  self.abstract_class = true
3
3
 
4
+ include Syncable
4
5
  include WithDescription
5
6
  include WithLocale
6
7
  include WithSlug
@@ -1,13 +1,21 @@
1
1
  class Course < ApplicationRecord
2
+ include Syncable
2
3
  include Mumukit::Platform::Course::Helpers
3
4
 
4
5
  validates_presence_of :slug, :shifts, :code, :days, :period, :description, :organization_id
5
6
  validates_uniqueness_of :slug
6
7
  belongs_to :organization
7
8
 
8
- def self.import_from_resource_h!(resource_h)
9
- json = Mumukit::Platform::Course::Helpers.slice_platform_json resource_h
10
- where(slug: json[:slug]).update_or_create!(json)
9
+ has_many :invitations
10
+
11
+ alias_attribute :name, :code
12
+
13
+ def current_invitation
14
+ invitations.where('expiration_date > ?', Time.now).take
15
+ end
16
+
17
+ def import_from_resource_h!(resource_h)
18
+ update! Mumukit::Platform::Course::Helpers.slice_platform_json(resource_h)
11
19
  end
12
20
 
13
21
  def slug=(slug)
@@ -15,6 +23,27 @@ class Course < ApplicationRecord
15
23
 
16
24
  self[:slug] = slug
17
25
  self[:code] = s.course
18
- self[:organization_id] = Organization.find_by!(name: s.organization).id
26
+ self[:organization_id] = Organization.locate!(s.organization).id
27
+ end
28
+
29
+ def invite!(expiration_date)
30
+ if closed?
31
+ generate_invitation! expiration_date
32
+ else
33
+ current_invitation
34
+ end
35
+ end
36
+
37
+ def closed?
38
+ current_invitation.blank? || current_invitation.expired?
39
+ end
40
+
41
+ def generate_invitation!(expiration_date)
42
+ invitation = invitations.build expiration_date: expiration_date, course: self
43
+ invitation.save_and_notify!
44
+ end
45
+
46
+ def self.sync_key_id_field
47
+ :slug
19
48
  end
20
49
  end
@@ -11,6 +11,7 @@ class Exam < ApplicationRecord
11
11
  has_many :users, through: :authorizations
12
12
 
13
13
  after_destroy { |record| Usage.destroy_usages_for record }
14
+ after_save :reindex_usages!
14
15
 
15
16
  include TerminalNavigation
16
17
 
@@ -100,6 +101,10 @@ class Exam < ApplicationRecord
100
101
  authorizations.all_except(authorizations_for(users)).destroy_all
101
102
  end
102
103
 
104
+ def reindex_usages!
105
+ index_usage! organization
106
+ end
107
+
103
108
  def self.import_from_resource_h!(json)
104
109
  exam_data = json.with_indifferent_access
105
110
  organization = Organization.find_by!(name: exam_data[:organization])
@@ -108,7 +113,6 @@ class Exam < ApplicationRecord
108
113
  remove_previous_version exam_data[:eid], exam_data[:guide_id]
109
114
  exam = where(classroom_id: exam_data[:eid]).update_or_create!(whitelist_attributes(exam_data))
110
115
  exam.process_users exam_data[:users]
111
- exam.index_usage! organization
112
116
  exam
113
117
  end
114
118
 
@@ -132,7 +132,7 @@ class Exercise < ApplicationRecord
132
132
  end
133
133
 
134
134
  def reclassify!(type)
135
- update!(type: Mumukit::Sync.classify(type))
135
+ update!(type: type)
136
136
  Exercise.find(id)
137
137
  end
138
138
 
@@ -183,6 +183,13 @@ class Exercise < ApplicationRecord
183
183
  navigable_parent.limited_for?(self)
184
184
  end
185
185
 
186
+ def files_for(current_content)
187
+ language
188
+ .directives_sections
189
+ .split_sections(current_content)
190
+ .map { |name, content| Mumuki::Domain::File.new name, content }
191
+ end
192
+
186
193
  private
187
194
 
188
195
  def evaluation_class
@@ -58,7 +58,7 @@ class Guide < Content
58
58
  end
59
59
 
60
60
  def import_from_resource_h!(resource_h)
61
- self.assign_attributes whitelist_attributes(resource_h, except: [:id])
61
+ self.assign_attributes whitelist_attributes(resource_h)
62
62
  self.language = Language.for_name(resource_h.dig(:language, :name))
63
63
  self.save!
64
64
 
@@ -67,8 +67,8 @@ class Guide < Content
67
67
  exercise_type = e[:type] || 'problem'
68
68
 
69
69
  exercise = exercise ?
70
- exercise.ensure_type!(exercise_type) :
71
- Mumukit::Sync.constantize(exercise_type).new(guide_id: self.id, bibliotheca_id: e[:id])
70
+ exercise.ensure_type!(exercise_type.as_module_name) :
71
+ exercise_type.as_module.new(guide_id: self.id, bibliotheca_id: e[:id])
72
72
 
73
73
  exercise.import_from_resource_h! (i+1), e
74
74
  end
@@ -1,25 +1,33 @@
1
1
  class Invitation < ApplicationRecord
2
- def self.find_by_code(code)
3
- Invitation
4
- .where(code: code)
5
- .where(Invitation.arel_table[:expiration_date].gt(Time.now))
6
- .take!
7
- rescue RangeError
8
- raise RecordNotFound.new('The invitation does not exist or it has expired')
2
+ include Syncable
3
+
4
+ belongs_to :course
5
+ validates_uniqueness_of :code
6
+
7
+ defaults do
8
+ self.code ||= self.class.generate_code
9
9
  end
10
10
 
11
- def self.import_from_resource_h!(json)
12
- Invitation.create! json
11
+ def import_from_resource_h!(json)
12
+ update! json.merge(course: Course.locate!(json[:course]))
13
13
  end
14
14
 
15
15
  def organization
16
- Organization.find_by! name: course.to_mumukit_slug.organization
16
+ course.organization
17
+ end
18
+
19
+ def course_slug
20
+ course.slug
17
21
  end
18
22
 
19
23
  def navigable_name
20
24
  I18n.t(:invitation_for, course: course_name)
21
25
  end
22
26
 
27
+ def to_resource_h
28
+ { code: code, course: course_slug, expiration_date: expiration_date }
29
+ end
30
+
23
31
  def navigation_end?
24
32
  true
25
33
  end
@@ -28,9 +36,26 @@ class Invitation < ApplicationRecord
28
36
  code
29
37
  end
30
38
 
39
+ def expired?
40
+ Time.now > expiration_date
41
+ end
42
+
43
+ def unexpired
44
+ raise RecordNotFound, "This invitation has already expired" if expired?
45
+ self
46
+ end
47
+
48
+ def self.generate_code
49
+ SecureRandom.urlsafe_base64 4
50
+ end
51
+
31
52
  private
32
53
 
33
54
  def course_name
34
- course.to_mumukit_slug.course
55
+ course.name
56
+ end
57
+
58
+ def self.sync_key_id_field
59
+ :code
35
60
  end
36
61
  end
@@ -1,5 +1,6 @@
1
1
  class Language < ApplicationRecord
2
2
  include WithCaseInsensitiveSearch
3
+ include Syncable
3
4
 
4
5
  enum output_content_type: [:plain, :html, :markdown]
5
6
 
@@ -59,10 +60,6 @@ class Language < ApplicationRecord
59
60
  interpolate(field, assignment.submitter.interpolations, lambda { |content| replace_content_reference(assignment, content) })
60
61
  end
61
62
 
62
- def self.locate_resource(runner_url)
63
- find_or_initialize_by(runner_url: runner_url).tap { |it| it.save(validate: false) }
64
- end
65
-
66
63
  def to_embedded_resource_h
67
64
  as_json(only: [:name, :extension, :test_extension]).symbolize_keys
68
65
  end
@@ -101,4 +98,8 @@ class Language < ApplicationRecord
101
98
  def directives_comment_type
102
99
  Mumukit::Directives::CommentType.parse comment_type
103
100
  end
101
+
102
+ def self.sync_key_id_field
103
+ :runner_url
104
+ end
104
105
  end
@@ -9,7 +9,7 @@ class Message < ApplicationRecord
9
9
  markdown_on :content
10
10
 
11
11
  def notify!
12
- Mumukit::Nuntius.notify! 'student-messages', to_resource_h
12
+ Mumukit::Nuntius.notify! 'student-messages', to_resource_h unless Organization.silenced?
13
13
  end
14
14
 
15
15
  def from_initiator?
@@ -1,4 +1,5 @@
1
1
  class Organization < ApplicationRecord
2
+ include Syncable
2
3
  include Mumukit::Platform::Organization::Helpers
3
4
 
4
5
  serialize :profile, Mumukit::Platform::Organization::Profile
@@ -96,13 +97,9 @@ class Organization < ApplicationRecord
96
97
  report_issue_enabled? || community_link.present? || forum_enabled?
97
98
  end
98
99
 
99
- def self.import_from_resource_h!(resource_h)
100
- find_or_initialize_by(name: resource_h[:name]).tap { |it| it.import_from_resource_h! resource_h }
101
- end
102
-
103
100
  def import_from_resource_h!(resource_h)
104
101
  attrs = Mumukit::Platform::Organization::Helpers.slice_platform_json resource_h
105
- attrs[:book] = Book.locate_resource attrs[:book]
102
+ attrs[:book] = Book.locate! attrs[:book]
106
103
  update! attrs
107
104
  end
108
105
 
@@ -124,5 +121,13 @@ class Organization < ApplicationRecord
124
121
  def base
125
122
  find_by name: 'base'
126
123
  end
124
+
125
+ def silenced?
126
+ !Mumukit::Platform::Organization.current? || current.silent?
127
+ end
128
+
129
+ def sync_key_id_field
130
+ :name
131
+ end
127
132
  end
128
133
  end
@@ -9,6 +9,8 @@ class Topic < Content
9
9
 
10
10
  markdown_on :appendix
11
11
 
12
+ after_save :reindex_usages!
13
+
12
14
  def pending_lessons(user)
13
15
  guides.
14
16
  joins('left join public.exercises exercises
@@ -29,7 +31,6 @@ class Topic < Content
29
31
  self.assign_attributes resource_h.except(:lessons, :description)
30
32
  self.description = resource_h[:description].squeeze(' ')
31
33
  rebuild! resource_h[:lessons].to_a.map { |it| lesson_for(it) }
32
- Organization.all.each { |org| org.reindex_usages! }
33
34
  end
34
35
 
35
36
  def to_resource_h
@@ -40,6 +41,10 @@ class Topic < Content
40
41
  book.chapters.find_by(topic_id: id) || Chapter.new(topic: self, book: book)
41
42
  end
42
43
 
44
+ def reindex_usages!
45
+ Chapter.where(topic: self).map(&:book).each(&:reindex_usages!)
46
+ end
47
+
43
48
  ## Forking
44
49
 
45
50
  def fork_to!(organization, syncer)
@@ -1,4 +1,5 @@
1
1
  class User < ApplicationRecord
2
+ include Syncable
2
3
  include WithProfile,
3
4
  WithUserNavigation,
4
5
  WithReminders,
@@ -80,7 +81,7 @@ class User < ApplicationRecord
80
81
  end
81
82
 
82
83
  def accept_invitation!(invitation)
83
- make_student_of! invitation.course
84
+ make_student_of! invitation.course_slug
84
85
  end
85
86
 
86
87
  def copy_progress_to!(another)
@@ -95,18 +96,14 @@ class User < ApplicationRecord
95
96
  reload
96
97
  end
97
98
 
98
- def self.import_from_resource_h!(json)
99
- json = Mumukit::Platform::User::Helpers.slice_platform_json json
100
- User.where(uid: json[:uid]).update_or_create!(json)
99
+ def import_from_resource_h!(json)
100
+ update! Mumukit::Platform::User::Helpers.slice_platform_json json
101
101
  end
102
102
 
103
103
  def unsubscribe_from_reminders!
104
104
  update! accepts_reminders: false
105
105
  end
106
106
 
107
- def self.unsubscription_verifier
108
- Rails.application.message_verifier(:unsubscribe)
109
- end
110
107
 
111
108
  def attach!(role, course)
112
109
  add_permission! role, course.slug
@@ -118,11 +115,6 @@ class User < ApplicationRecord
118
115
  save_and_notify!
119
116
  end
120
117
 
121
- def self.create_if_necessary(user)
122
- user[:uid] ||= user[:email]
123
- where(uid: user[:uid]).first_or_create(user)
124
- end
125
-
126
118
  def interpolations
127
119
  {
128
120
  'user_email' => email,
@@ -150,4 +142,17 @@ class User < ApplicationRecord
150
142
  def init
151
143
  self.image_url ||= "user_shape.png"
152
144
  end
145
+
146
+ def self.sync_key_id_field
147
+ :uid
148
+ end
149
+
150
+ def self.unsubscription_verifier
151
+ Rails.application.message_verifier(:unsubscribe)
152
+ end
153
+
154
+ def self.create_if_necessary(user)
155
+ user[:uid] ||= user[:email]
156
+ where(uid: user[:uid]).first_or_create(user)
157
+ end
153
158
  end
@@ -1,5 +1,5 @@
1
1
  class AddLoginMethodsToOrganization < ActiveRecord::Migration[4.2]
2
2
  def change
3
- add_column :organizations, :login_methods, :string, array: true, default: Mumukit::Login::Settings.login_methods, null: false
3
+ add_column :organizations, :login_methods, :string, array: true, default: [], null: false
4
4
  end
5
5
  end
@@ -0,0 +1,6 @@
1
+ class ConvertCourseInvitationIntoFk < ActiveRecord::Migration[5.1]
2
+ def change
3
+ remove_column :invitations, :course
4
+ add_reference :invitations, :course, index: true
5
+ end
6
+ end
@@ -0,0 +1,17 @@
1
+ require_relative './factories/api_client_factory'
2
+ require_relative './factories/assignments_factory'
3
+ require_relative './factories/book_factory'
4
+ require_relative './factories/chapter_factory'
5
+ require_relative './factories/complement_factory'
6
+ require_relative './factories/course_factory'
7
+ require_relative './factories/discussion_factory'
8
+ require_relative './factories/exam_factory'
9
+ require_relative './factories/exercise_factory'
10
+ require_relative './factories/guide_factory'
11
+ require_relative './factories/invitation_factory'
12
+ require_relative './factories/lesson_factory'
13
+ require_relative './factories/login_settings_factory'
14
+ require_relative './factories/message_factory'
15
+ require_relative './factories/organization_factory'
16
+ require_relative './factories/topic_factory'
17
+ require_relative './factories/user_factory'
@@ -0,0 +1,18 @@
1
+ FactoryBot.define do
2
+ factory :api_client do |t|
3
+ transient do
4
+ role { :janitor }
5
+ grant { 'test/*' }
6
+ end
7
+
8
+ description { "foo" }
9
+ user {
10
+ create :user,
11
+ first_name: 'foo',
12
+ last_name: 'bar',
13
+ email: 'foo+1@bar.com',
14
+ uid: 'foo+1@bar.com',
15
+ permissions: { role => grant }
16
+ }
17
+ end
18
+ end
@@ -0,0 +1,7 @@
1
+ FactoryBot.define do
2
+ factory :assignment do
3
+ status { :pending }
4
+ exercise
5
+ submitter { create(:user) }
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ FactoryBot.define do
2
+ factory :book do
3
+ name { Faker::Lorem.sentence(3) }
4
+ description { Faker::Lorem.sentence(30) }
5
+ slug { "mumuki/mumuki-test-book-#{SecureRandom.uuid}" }
6
+ end
7
+ end
@@ -0,0 +1,16 @@
1
+ FactoryBot.define do
2
+
3
+ factory :chapter do
4
+ number { Faker::Number.between(1, 40) }
5
+ book { Organization.current.book rescue nil }
6
+
7
+ transient do
8
+ lessons { [] }
9
+ name { Faker::Lorem.sentence(3) }
10
+ end
11
+
12
+ after(:build) do |chapter, evaluator|
13
+ chapter.topic = build(:topic, name: evaluator.name, lessons: evaluator.lessons) unless evaluator.topic
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,6 @@
1
+ FactoryBot.define do
2
+
3
+ factory :complement, traits: [:guide_container] do
4
+ book { Organization.current.book rescue nil }
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+ FactoryBot.define do
2
+ factory :course do
3
+ period { '2016' }
4
+ shifts { %w(morning) }
5
+ days { %w(monday wednesday) }
6
+ description { 'test' }
7
+ organization_id { 1 }
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ FactoryBot.define do
2
+ factory :discussion do
3
+ title { 'A discussion' }
4
+ description { 'A discussion description' }
5
+ initiator { create(:user) }
6
+ item { create(:exercise) }
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ FactoryBot.define do
2
+
3
+ factory :exam, traits: [:guide_container] do
4
+ duration { Faker::Number.between(10, 60).minutes }
5
+ organization { Organization.current }
6
+ start_time { 5.minutes.ago }
7
+ end_time { 10.minutes.since }
8
+ end
9
+ end
@@ -0,0 +1,73 @@
1
+ FactoryBot.define do
2
+
3
+ factory :language do
4
+ sequence(:name) { |n| "lang#{n}" }
5
+
6
+ runner_url { Faker::Internet.url }
7
+ queriable { true }
8
+ end
9
+
10
+ factory :haskell, parent: :language do
11
+ name { 'haskell' }
12
+ end
13
+
14
+ factory :bash, parent: :language do
15
+ name { 'bash' }
16
+ triable { true }
17
+ stateful_console { true }
18
+ end
19
+
20
+ factory :text_language, parent: :language do
21
+ name { 'text' }
22
+ end
23
+
24
+ factory :gobstones, parent: :language do
25
+ name { 'gobstones' }
26
+ extension { 'gbs' }
27
+ queriable { false }
28
+ end
29
+
30
+ factory :exercise_base do
31
+ language { guide ? guide.language : create(:language) }
32
+ sequence(:bibliotheca_id) { |n| n }
33
+ sequence(:number) { |n| n }
34
+
35
+ locale { :en }
36
+ guide
37
+ end
38
+
39
+ factory :challenge, parent: :exercise_base do
40
+ layout { 'input_right' }
41
+ end
42
+
43
+ factory :reading, class: Reading, parent: :exercise_base do
44
+ name { 'A reading' }
45
+ description { 'Simple reading' }
46
+ end
47
+
48
+ factory :problem, class: Problem, parent: :challenge do
49
+ name { 'A problem' }
50
+ description { 'Simple problem' }
51
+ test { 'dont care' }
52
+ end
53
+
54
+ factory :interactive, class: Interactive, parent: :challenge do
55
+ name { 'An interactive problem' }
56
+ description { 'Simple interactive problem' }
57
+ goal { :query_passes }
58
+ language { create(:bash) }
59
+ end
60
+
61
+ factory :playground, class: Playground, parent: :challenge do
62
+ name { 'A Playground' }
63
+ description { 'Simple playground' }
64
+ end
65
+
66
+ factory :exercise, parent: :problem
67
+
68
+ factory :x_equal_5_exercise, parent: :exercise do
69
+ test { 'describe "x" $ do
70
+ it "should be equal 5" $ do
71
+ x `shouldBe` 5' }
72
+ end
73
+ end
@@ -0,0 +1,30 @@
1
+ FactoryBot.define do
2
+
3
+ factory :guide do
4
+ sequence(:name) { |n| "guide#{n}" }
5
+ locale { 'en' }
6
+ description { 'A Guide' }
7
+ slug { "flbulgarelli/mumuki-sample-guide-#{SecureRandom.uuid}" }
8
+ language
9
+ end
10
+
11
+ trait :guide_container do
12
+ transient do
13
+ exercises { [] }
14
+ name { Faker::Lorem.sentence(3) }
15
+ description { Faker::Lorem.sentence(10) }
16
+ language { create(:language) }
17
+ slug { "mumuki/mumuki-test-lesson-#{SecureRandom.uuid}" }
18
+ end
19
+
20
+ after(:build) do |lesson, evaluator|
21
+ lesson.guide = build(:guide,
22
+ name: evaluator.name,
23
+ slug: evaluator.slug,
24
+ exercises: evaluator.exercises,
25
+ description: evaluator.description,
26
+ language: evaluator.language) unless evaluator.guide
27
+ end
28
+ end
29
+
30
+ end
@@ -0,0 +1,7 @@
1
+ FactoryBot.define do
2
+ factory :invitation do
3
+ code { Faker::Lorem.sentence(6) }
4
+ course { "foo/bar" }
5
+ expiration_date { 5.minutes.since }
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ FactoryBot.define do
2
+
3
+ factory :lesson, traits: [:guide_container] do
4
+ sequence(:number)
5
+ topic
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ FactoryBot.define do
2
+ factory :all_login_settings, class: Mumukit::Login::Settings do
3
+ login_methods { Mumukit::Login::Settings.login_methods }
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ FactoryBot.define do
2
+
3
+ factory :message do
4
+ exercise_id { Faker::Internet.number(2) }
5
+ assignment
6
+ submission_id { assignment.id }
7
+ sender { Faker::Internet.email }
8
+ type { 'success' }
9
+ content { Faker::Lorem.sentence(3) }
10
+ end
11
+ end
@@ -0,0 +1,26 @@
1
+ FactoryBot.define do
2
+
3
+ factory :organization do
4
+ contact_email { Faker::Internet.email }
5
+ description { 'a great org' }
6
+ locale { 'en' }
7
+ settings {}
8
+ book
9
+ end
10
+
11
+ factory :base, parent: :organization do
12
+ public { true }
13
+ name { 'base' }
14
+ login_methods { Mumukit::Login::Settings.login_methods }
15
+ end
16
+
17
+ factory :public_organization, parent: :organization do
18
+ public { true }
19
+ name { 'the-public-org' }
20
+ login_methods { Mumukit::Login::Settings.login_methods }
21
+ end
22
+
23
+ factory :private_organization, parent: :organization do
24
+ name { 'the-private-org' }
25
+ end
26
+ end
@@ -0,0 +1,9 @@
1
+ FactoryBot.define do
2
+
3
+ factory :topic do
4
+ name { Faker::Lorem::sentence(3) }
5
+ description { Faker::Lorem.paragraph(2) }
6
+ slug { "mumuki/mumuki-sample-topic-#{SecureRandom.uuid}" }
7
+ locale { :en }
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ FactoryBot.define do
2
+ factory :user do
3
+ email { Faker::Internet.email }
4
+ uid { email }
5
+ first_name { Faker::Name.first_name }
6
+ last_name { Faker::Name.last_name }
7
+ end
8
+ end
@@ -0,0 +1,4 @@
1
+ en:
2
+ submission:
3
+ try_again: Oops, something went wrong in Mumuki. Restart the exercise, wait a few seconds and try again.
4
+
@@ -0,0 +1,4 @@
1
+ es:
2
+ submission:
3
+ try_again: ¡Ups! Algo salió mal en mumuki. Reiniciá el ejercicio, esperá unos segundos y volvé a intentar.
4
+
@@ -0,0 +1,4 @@
1
+ pt:
2
+ submission:
3
+ try_again: Opa, algo deu errado em Mumuki. Reinicie o exercício, aguarde alguns segundos e tente novamente.
4
+
@@ -6,7 +6,7 @@ class Mumuki::Domain::Submission::ConsoleSubmission < Mumuki::Domain::Submission
6
6
  end
7
7
 
8
8
  def format_query_result!(results)
9
- results[:result] = I18n.t(:try_again) if results[:status] == :aborted
9
+ results[:result] = I18n.t('submission.try_again') if results[:status].aborted?
10
10
  results[:status] = results[:status].to_submission_status
11
11
  results
12
12
  end
@@ -1,5 +1,5 @@
1
1
  module Mumuki
2
2
  module Domain
3
- VERSION = '6.1.5'
3
+ VERSION = '6.2.0'
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mumuki-domain
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.1.5
4
+ version: 6.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Franco Leonardo Bulgarelli
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-11-30 00:00:00.000000000 Z
11
+ date: 2018-12-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -86,14 +86,14 @@ dependencies:
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '1.11'
89
+ version: '1.13'
90
90
  type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '1.11'
96
+ version: '1.13'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: mumukit-directives
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -114,14 +114,14 @@ dependencies:
114
114
  requirements:
115
115
  - - "~>"
116
116
  - !ruby/object:Gem::Version
117
- version: '3.5'
117
+ version: '3.6'
118
118
  type: :runtime
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
- version: '3.5'
124
+ version: '3.6'
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: mumukit-randomizer
127
127
  requirement: !ruby/object:Gem::Requirement
@@ -156,14 +156,42 @@ dependencies:
156
156
  requirements:
157
157
  - - "~>"
158
158
  - !ruby/object:Gem::Version
159
- version: '0.1'
159
+ version: '0.3'
160
160
  type: :runtime
161
161
  prerelease: false
162
162
  version_requirements: !ruby/object:Gem::Requirement
163
163
  requirements:
164
164
  - - "~>"
165
165
  - !ruby/object:Gem::Version
166
- version: '0.1'
166
+ version: '0.3'
167
+ - !ruby/object:Gem::Dependency
168
+ name: pg
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: 0.18.0
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: 0.18.0
181
+ - !ruby/object:Gem::Dependency
182
+ name: mumukit-login
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - "~>"
186
+ - !ruby/object:Gem::Version
187
+ version: '6.1'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - "~>"
193
+ - !ruby/object:Gem::Version
194
+ version: '6.1'
167
195
  description: Mumuki Platform's Domain Model
168
196
  email:
169
197
  - franco@mumuki.org
@@ -173,6 +201,7 @@ extra_rdoc_files: []
173
201
  files:
174
202
  - LICENSE
175
203
  - README.md
204
+ - Rakefile
176
205
  - app/models/api_client.rb
177
206
  - app/models/application_record.rb
178
207
  - app/models/assignment.rb
@@ -192,6 +221,7 @@ files:
192
221
  - app/models/concerns/submittable/solvable.rb
193
222
  - app/models/concerns/submittable/submittable.rb
194
223
  - app/models/concerns/submittable/triable.rb
224
+ - app/models/concerns/syncable.rb
195
225
  - app/models/concerns/with_assignments.rb
196
226
  - app/models/concerns/with_case_insensitive_search.rb
197
227
  - app/models/concerns/with_description.rb
@@ -501,6 +531,7 @@ files:
501
531
  - db/migrate/20181114201620_add_test_template_to_languages.rb
502
532
  - db/migrate/20181117190241_add_feedback_to_language.rb
503
533
  - db/migrate/20181121165956_rename_choices_column.rb
534
+ - db/migrate/20181210131824_convert_course_invitation_into_fk.rb
504
535
  - lib/mumuki/domain.rb
505
536
  - lib/mumuki/domain/engine.rb
506
537
  - lib/mumuki/domain/evaluation.rb
@@ -515,12 +546,33 @@ files:
515
546
  - lib/mumuki/domain/extensions.rb
516
547
  - lib/mumuki/domain/extensions/array.rb
517
548
  - lib/mumuki/domain/extensions/string.rb
549
+ - lib/mumuki/domain/factories.rb
550
+ - lib/mumuki/domain/factories/api_client_factory.rb
551
+ - lib/mumuki/domain/factories/assignments_factory.rb
552
+ - lib/mumuki/domain/factories/book_factory.rb
553
+ - lib/mumuki/domain/factories/chapter_factory.rb
554
+ - lib/mumuki/domain/factories/complement_factory.rb
555
+ - lib/mumuki/domain/factories/course_factory.rb
556
+ - lib/mumuki/domain/factories/discussion_factory.rb
557
+ - lib/mumuki/domain/factories/exam_factory.rb
558
+ - lib/mumuki/domain/factories/exercise_factory.rb
559
+ - lib/mumuki/domain/factories/guide_factory.rb
560
+ - lib/mumuki/domain/factories/invitation_factory.rb
561
+ - lib/mumuki/domain/factories/lesson_factory.rb
562
+ - lib/mumuki/domain/factories/login_settings_factory.rb
563
+ - lib/mumuki/domain/factories/message_factory.rb
564
+ - lib/mumuki/domain/factories/organization_factory.rb
565
+ - lib/mumuki/domain/factories/topic_factory.rb
566
+ - lib/mumuki/domain/factories/user_factory.rb
518
567
  - lib/mumuki/domain/file.rb
519
568
  - lib/mumuki/domain/locales/activerecord.en.yml
520
569
  - lib/mumuki/domain/locales/activerecord.es.yml
521
570
  - lib/mumuki/domain/locales/narrator.en.yml
522
571
  - lib/mumuki/domain/locales/narrator.es.yml
523
572
  - lib/mumuki/domain/locales/narrator.pt.yml
573
+ - lib/mumuki/domain/locales/submission.en.yml
574
+ - lib/mumuki/domain/locales/submission.es.yml
575
+ - lib/mumuki/domain/locales/submission.pt.yml
524
576
  - lib/mumuki/domain/seed.rb
525
577
  - lib/mumuki/domain/status.rb
526
578
  - lib/mumuki/domain/status/discussion/closed.rb
@@ -566,8 +618,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
566
618
  - !ruby/object:Gem::Version
567
619
  version: '0'
568
620
  requirements: []
569
- rubyforge_project:
570
- rubygems_version: 2.5.1
621
+ rubygems_version: 3.0.0
571
622
  signing_key:
572
623
  specification_version: 4
573
624
  summary: Mumuki Platform's Domain Model