mumuki-domain 7.6.1 → 7.7.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.
- checksums.yaml +4 -4
- data/app/models/concerns/submittable/solvable.rb +8 -2
- data/app/models/concerns/with_discussions.rb +8 -5
- data/app/models/concerns/with_layout.rb +5 -1
- data/app/models/concerns/with_scoped_queries.rb +9 -9
- data/app/models/concerns/with_scoped_queries/page.rb +1 -1
- data/app/models/concerns/with_scoped_queries/sort.rb +6 -1
- data/app/models/course.rb +13 -7
- data/app/models/discussion.rb +47 -9
- data/app/models/exam.rb +53 -23
- data/app/models/exam/passing_criterion.rb +53 -0
- data/app/models/exercise/challenge.rb +1 -1
- data/app/models/exercise/problem.rb +6 -0
- data/app/models/exercise/reading.rb +4 -0
- data/app/models/invitation.rb +7 -1
- data/app/models/message.rb +28 -4
- data/app/models/user.rb +4 -0
- data/db/migrate/20200601203033_add_course_to_exam.rb +5 -0
- data/db/migrate/20200605161350_add_passing_criterions_to_exam.rb +6 -0
- data/db/migrate/20200702165503_add_messages_count_to_discussion.rb +6 -0
- data/db/migrate/20200728162727_add_not_actually_a_question_field_to_messages.rb +5 -0
- data/db/migrate/20200728163038_add_requires_moderator_response_to_discussions.rb +5 -0
- data/db/migrate/20200731081757_add_last_moderator_access_fields_to_discussion.rb +6 -0
- data/lib/mumuki/domain/factories/course_factory.rb +2 -0
- data/lib/mumuki/domain/factories/exam_factory.rb +1 -0
- data/lib/mumuki/domain/organization/settings.rb +2 -1
- data/lib/mumuki/domain/status/discussion/opened.rb +0 -8
- data/lib/mumuki/domain/submission/base.rb +6 -0
- data/lib/mumuki/domain/submission/solution.rb +3 -1
- data/lib/mumuki/domain/version.rb +1 -1
- metadata +10 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9983a4a1b2d29d329606c4ccfe991bd979a5717a8562678316e143638857622b
|
|
4
|
+
data.tar.gz: 3115a2b010be3a7536bb835e36751595d510cfbc009093d6ac77c08e973de5e8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 46cb4964c07e81cb2568d10a96cc83ca3ab29417abc3012ea7898dfd0712e375ec4a37f7852bd827725e73a94dcc153766948c06cc91e2f1b8f3d91629fcb2d4
|
|
7
|
+
data.tar.gz: 97bc43d01513919f1a3ec4b053261c7f43590399710f3ba0c7ce026daa243547e0bda2a17508372d5ef5a33fa1b36b9c06e90f31a2fe5adb2faed791a22b2933
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module Solvable
|
|
2
|
-
def submit_solution!(user,
|
|
3
|
-
assignment, _ = find_assignment_and_submit! user,
|
|
2
|
+
def submit_solution!(user, submission_attributes={})
|
|
3
|
+
assignment, _ = find_assignment_and_submit! user, solution_for(submission_attributes)
|
|
4
4
|
try_solve_discussions(user) if assignment.solved?
|
|
5
5
|
assignment
|
|
6
6
|
end
|
|
@@ -8,6 +8,12 @@ module Solvable
|
|
|
8
8
|
def run_tests!(params)
|
|
9
9
|
language.run_tests!(params.merge(locale: locale, expectations: expectations, custom_expectations: custom_expectations))
|
|
10
10
|
end
|
|
11
|
+
|
|
12
|
+
def solution_for(submission_attributes)
|
|
13
|
+
submission_attributes[:content]
|
|
14
|
+
.to_mumuki_solution(language)
|
|
15
|
+
.with_client_result(submission_attributes[:client_result])
|
|
16
|
+
end
|
|
11
17
|
end
|
|
12
18
|
|
|
13
19
|
class NilClass
|
|
@@ -7,11 +7,7 @@ module WithDiscussions
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def discuss!(user, discussion, organization = Organization.current)
|
|
10
|
-
|
|
11
|
-
discussion.merge!(submission: submission_for(user)) if submission_for(user).present?
|
|
12
|
-
created_discussion = discussions.create discussion
|
|
13
|
-
user.subscribe_to! created_discussion
|
|
14
|
-
created_discussion
|
|
10
|
+
new_discussion_for(user, discussion, organization).tap &:save!
|
|
15
11
|
end
|
|
16
12
|
|
|
17
13
|
def submission_for(_)
|
|
@@ -21,4 +17,11 @@ module WithDiscussions
|
|
|
21
17
|
def try_solve_discussions(user)
|
|
22
18
|
discussions.where(initiator: user).map(&:try_solve!)
|
|
23
19
|
end
|
|
20
|
+
|
|
21
|
+
def new_discussion_for(user, discussion_h = {}, organization = Organization.current)
|
|
22
|
+
discussion_h.merge!(initiator_id: user.id, organization: organization)
|
|
23
|
+
discussion_h.merge!(submission: submission_for(user)) if submission_for(user).present?
|
|
24
|
+
discussions.new discussion_h
|
|
25
|
+
end
|
|
26
|
+
|
|
24
27
|
end
|
|
@@ -2,6 +2,10 @@ module WithLayout
|
|
|
2
2
|
extend ActiveSupport::Concern
|
|
3
3
|
|
|
4
4
|
included do
|
|
5
|
-
enum layout: [:input_right, :input_bottom, :
|
|
5
|
+
enum layout: [:input_right, :input_bottom, :input_primary, :input_kindergarten]
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def input_kids?
|
|
9
|
+
input_primary? || input_kindergarten?
|
|
6
10
|
end
|
|
7
11
|
end
|
|
@@ -16,8 +16,8 @@ module WithScopedQueries
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
class_methods do
|
|
19
|
-
def query_methods
|
|
20
|
-
queriable_attributes.keys
|
|
19
|
+
def query_methods(excluded_methods)
|
|
20
|
+
queriable_attributes.keys - excluded_methods.to_a
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def scoped_query_module(method)
|
|
@@ -28,19 +28,19 @@ module WithScopedQueries
|
|
|
28
28
|
queriable_attributes.values.flatten
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
def actual_params(params,
|
|
32
|
-
params.
|
|
31
|
+
def actual_params(params, excluded_params)
|
|
32
|
+
params.except(*excluded_params)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
def scoped_query_by(params,
|
|
36
|
-
query_methods.inject(all) do |scope, method|
|
|
37
|
-
valid_params = valid_params_for(method, params,
|
|
35
|
+
def scoped_query_by(params, **options)
|
|
36
|
+
query_methods(options[:excluded_methods]).inject(all) do |scope, method|
|
|
37
|
+
valid_params = valid_params_for(method, params, options[:excluded_params])
|
|
38
38
|
scoped_query_module(method).query_by valid_params, scope, self
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
def valid_params_for(method, params,
|
|
43
|
-
actual_params = actual_params(params,
|
|
42
|
+
def valid_params_for(method, params, excluded_params)
|
|
43
|
+
actual_params = actual_params(params, excluded_params)
|
|
44
44
|
actual_params.permit queriable_attributes[method]
|
|
45
45
|
end
|
|
46
46
|
end
|
|
@@ -34,13 +34,18 @@ module WithScopedQueries::Sort
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
class_methods do
|
|
37
|
+
def opposite(direction)
|
|
38
|
+
dir = direction.to_s.downcase.to_sym
|
|
39
|
+
[:asc, :desc].find { |it| it != dir }
|
|
40
|
+
end
|
|
41
|
+
|
|
37
42
|
def sorting_filters
|
|
38
43
|
sorting_fields.product([:asc, :desc]).map do |it|
|
|
39
44
|
"#{it.first}_#{it.second}"
|
|
40
45
|
end
|
|
41
46
|
end
|
|
42
47
|
|
|
43
|
-
def sorting_params_allowed?(sort_param, direction_param=nil)
|
|
48
|
+
def sorting_params_allowed?(sort_param, direction_param = nil)
|
|
44
49
|
sorting_fields.include?(sort_param.to_sym) && [:asc, :desc].include?(direction_param&.to_sym)
|
|
45
50
|
end
|
|
46
51
|
end
|
data/app/models/course.rb
CHANGED
|
@@ -14,7 +14,7 @@ class Course < ApplicationRecord
|
|
|
14
14
|
resource_fields :slug, :shifts, :code, :days, :period, :description
|
|
15
15
|
|
|
16
16
|
def current_invitation
|
|
17
|
-
invitations.where('expiration_date > ?', Time.now).
|
|
17
|
+
invitations.where('expiration_date > ?', Time.now).first
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def import_from_resource_h!(resource_h)
|
|
@@ -22,10 +22,8 @@ class Course < ApplicationRecord
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def slug=(slug)
|
|
25
|
-
s =
|
|
26
|
-
|
|
27
|
-
self[:slug] = slug
|
|
28
|
-
self[:code] = s.course
|
|
25
|
+
s = slug.to_mumukit_slug
|
|
26
|
+
self[:slug] = slug.to_s
|
|
29
27
|
self[:organization_id] = Organization.locate!(s.organization).id
|
|
30
28
|
end
|
|
31
29
|
|
|
@@ -42,8 +40,8 @@ class Course < ApplicationRecord
|
|
|
42
40
|
end
|
|
43
41
|
|
|
44
42
|
def generate_invitation!(expiration_date)
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
invitations.create expiration_date: expiration_date, course: self
|
|
44
|
+
current_invitation
|
|
47
45
|
end
|
|
48
46
|
|
|
49
47
|
def self.sync_key_id_field
|
|
@@ -53,4 +51,12 @@ class Course < ApplicationRecord
|
|
|
53
51
|
def to_organization
|
|
54
52
|
organization
|
|
55
53
|
end
|
|
54
|
+
|
|
55
|
+
def to_s
|
|
56
|
+
slug.to_s
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.allowed_for(user, organization = Organization.current)
|
|
60
|
+
where(organization: organization).select { |course| user.teacher_of? course.slug }
|
|
61
|
+
end
|
|
56
62
|
end
|
data/app/models/discussion.rb
CHANGED
|
@@ -4,25 +4,29 @@ class Discussion < ApplicationRecord
|
|
|
4
4
|
belongs_to :item, polymorphic: true
|
|
5
5
|
has_many :messages, -> { order(:created_at) }, dependent: :destroy
|
|
6
6
|
belongs_to :initiator, class_name: 'User'
|
|
7
|
+
belongs_to :last_moderator_access_by, class_name: 'User', optional: true
|
|
7
8
|
belongs_to :exercise, foreign_type: :exercise, foreign_key: 'item_id'
|
|
8
9
|
belongs_to :organization
|
|
9
10
|
has_many :subscriptions
|
|
10
11
|
has_many :upvotes
|
|
11
12
|
|
|
12
13
|
scope :by_language, -> (language) { includes(:exercise).joins(exercise: :language).where(languages: {name: language}) }
|
|
14
|
+
scope :order_by_responses_count, -> (direction) { reorder(validated_messages_count: direction, messages_count: opposite(direction)) }
|
|
15
|
+
scope :by_requires_moderator_response, -> (boolean) { where(requires_moderator_response: boolean.to_boolean) }
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
validates_presence_of :title
|
|
17
|
+
after_create :subscribe_initiator!
|
|
16
18
|
|
|
17
19
|
markdown_on :description
|
|
18
20
|
|
|
19
|
-
sortable :
|
|
20
|
-
filterable :status, :language
|
|
21
|
+
sortable :responses_count, :upvotes_count, :created_at, default: :created_at_desc
|
|
22
|
+
filterable :status, :language, :requires_moderator_response
|
|
21
23
|
pageable
|
|
22
24
|
|
|
23
25
|
delegate :language, to: :item
|
|
24
26
|
delegate :to_discussion_status, to: :status
|
|
25
27
|
|
|
28
|
+
MODERATOR_REVIEW_AVERAGE_TIME = 10.minutes
|
|
29
|
+
|
|
26
30
|
scope :for_user, -> (user) do
|
|
27
31
|
if user.try(:moderator_here?)
|
|
28
32
|
all
|
|
@@ -37,10 +41,6 @@ class Discussion < ApplicationRecord
|
|
|
37
41
|
end
|
|
38
42
|
end
|
|
39
43
|
|
|
40
|
-
def capitalize_title
|
|
41
|
-
title.capitalize!
|
|
42
|
-
end
|
|
43
|
-
|
|
44
44
|
def used_in?(organization)
|
|
45
45
|
organization == self.organization
|
|
46
46
|
end
|
|
@@ -66,7 +66,7 @@ class Discussion < ApplicationRecord
|
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
def friendly
|
|
69
|
-
|
|
69
|
+
initiator.name
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
def subscription_for(user)
|
|
@@ -125,14 +125,52 @@ class Discussion < ApplicationRecord
|
|
|
125
125
|
responses_count > 0
|
|
126
126
|
end
|
|
127
127
|
|
|
128
|
+
def subscribe_initiator!
|
|
129
|
+
initiator.subscribe_to! self
|
|
130
|
+
end
|
|
131
|
+
|
|
128
132
|
def extra_preview_html
|
|
129
133
|
# FIXME this is buggy, because the extra
|
|
130
134
|
# may have changed since the submission of this discussion
|
|
131
135
|
exercise.assignment_for(initiator).extra_preview_html
|
|
132
136
|
end
|
|
133
137
|
|
|
138
|
+
def update_counters!
|
|
139
|
+
messages_query = messages_by_updated_at
|
|
140
|
+
validated_messages = messages_query.select &:validated?
|
|
141
|
+
requires_moderator_response = messages_query.find { |it| it.validated? || it.question? }&.from_initiator?
|
|
142
|
+
update! messages_count: messages_query.count,
|
|
143
|
+
validated_messages_count: validated_messages.count,
|
|
144
|
+
requires_moderator_response: requires_moderator_response
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def update_last_moderator_access!(user)
|
|
148
|
+
unless last_moderator_access_visible_for?(user)
|
|
149
|
+
update! last_moderator_access_at: Time.now,
|
|
150
|
+
last_moderator_access_by: user
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def being_accessed_by_moderator?
|
|
155
|
+
last_moderator_access_at.present? && last_moderator_access_at > Time.now - MODERATOR_REVIEW_AVERAGE_TIME
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def last_moderator_access_visible_for?(user)
|
|
159
|
+
show_last_moderator_access_for?(user) && being_accessed_by_moderator?
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def show_last_moderator_access_for?(user)
|
|
163
|
+
user&.moderator_here? && last_moderator_access_by != user
|
|
164
|
+
end
|
|
165
|
+
|
|
134
166
|
def self.debatable_for(klazz, params)
|
|
135
167
|
debatable_id = params[:"#{klazz.underscore}_id"]
|
|
136
168
|
klazz.constantize.find(debatable_id)
|
|
137
169
|
end
|
|
170
|
+
|
|
171
|
+
private
|
|
172
|
+
|
|
173
|
+
def messages_by_updated_at(direction = :desc)
|
|
174
|
+
messages.reorder(updated_at: direction)
|
|
175
|
+
end
|
|
138
176
|
end
|
data/app/models/exam.rb
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
class Exam < ApplicationRecord
|
|
2
|
+
|
|
2
3
|
include GuideContainer
|
|
3
4
|
include FriendlyName
|
|
4
|
-
|
|
5
|
-
validates_presence_of :start_time, :end_time
|
|
5
|
+
include TerminalNavigation
|
|
6
6
|
|
|
7
7
|
belongs_to :organization
|
|
8
|
+
belongs_to :course, optional: true
|
|
8
9
|
|
|
9
10
|
has_many :authorizations, class_name: 'ExamAuthorization', dependent: :destroy
|
|
10
11
|
has_many :users, through: :authorizations
|
|
11
12
|
|
|
13
|
+
enum passing_criterion_type: [:none, :percentage, :passed_exercises], _prefix: :passing_criterion
|
|
14
|
+
|
|
15
|
+
validates_presence_of :start_time, :end_time
|
|
16
|
+
validates_numericality_of :max_problem_submissions, :max_choice_submissions, greater_than_or_equal_to: 1, allow_nil: true
|
|
17
|
+
|
|
18
|
+
before_save :set_default_criterion_type!
|
|
19
|
+
before_save :ensure_valid_passing_criterion!
|
|
20
|
+
|
|
21
|
+
before_create :set_classroom_id!
|
|
22
|
+
|
|
12
23
|
after_destroy { |record| Usage.destroy_usages_for record }
|
|
13
24
|
after_create :reindex_usages!
|
|
14
25
|
|
|
15
|
-
include TerminalNavigation
|
|
16
|
-
|
|
17
26
|
def used_in?(organization)
|
|
18
27
|
organization == self.organization
|
|
19
28
|
end
|
|
@@ -116,10 +125,41 @@ class Exam < ApplicationRecord
|
|
|
116
125
|
index_usage! organization
|
|
117
126
|
end
|
|
118
127
|
|
|
128
|
+
def attempts_left_for(assignment)
|
|
129
|
+
max_attempts_for(assignment.exercise) - (assignment.attempts_count || 0)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def limited_for?(exercise)
|
|
133
|
+
max_attempts_for(exercise).present?
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def results_hidden_for?(exercise)
|
|
137
|
+
exercise.choice? && results_hidden_for_choices?
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def resettable?
|
|
141
|
+
false
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def set_classroom_id!
|
|
145
|
+
self.classroom_id ||= SecureRandom.hex(8)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def passing_criterion
|
|
149
|
+
@passing_criterion ||= Exam::PassingCriterion.parse(passing_criterion_type, passing_criterion_value)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def ensure_valid_passing_criterion!
|
|
153
|
+
passing_criterion.ensure_valid!
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def set_default_criterion_type!
|
|
157
|
+
self.passing_criterion_type ||= :none
|
|
158
|
+
end
|
|
159
|
+
|
|
119
160
|
def self.import_from_resource_h!(json)
|
|
120
161
|
exam_data = json.with_indifferent_access
|
|
121
|
-
|
|
122
|
-
organization.switch!
|
|
162
|
+
Organization.locate!(exam_data[:organization].to_s).switch!
|
|
123
163
|
adapt_json_values exam_data
|
|
124
164
|
remove_previous_version exam_data[:eid], exam_data[:guide_id]
|
|
125
165
|
exam = where(classroom_id: exam_data[:eid]).update_or_create!(whitelist_attributes(exam_data))
|
|
@@ -141,8 +181,13 @@ class Exam < ApplicationRecord
|
|
|
141
181
|
def self.adapt_json_values(exam)
|
|
142
182
|
exam[:guide_id] = Guide.locate!(exam[:slug]).id
|
|
143
183
|
exam[:organization_id] = Organization.current.id
|
|
184
|
+
exam[:course_id] = Course.locate!(exam[:course].to_s).id
|
|
144
185
|
exam[:users] = User.where(uid: exam[:uids])
|
|
145
|
-
[:start_time
|
|
186
|
+
exam[:start_time] = exam[:start_time].to_time
|
|
187
|
+
exam[:end_time] = exam[:end_time].to_time
|
|
188
|
+
exam[:classroom_id] = exam[:eid] if exam[:eid].present?
|
|
189
|
+
exam[:passing_criterion_type] = exam.dig(:passing_criterion, :type)
|
|
190
|
+
exam[:passing_criterion_value] = exam.dig(:passing_criterion, :value)
|
|
146
191
|
end
|
|
147
192
|
|
|
148
193
|
def self.remove_previous_version(eid, guide_id)
|
|
@@ -153,25 +198,10 @@ class Exam < ApplicationRecord
|
|
|
153
198
|
end
|
|
154
199
|
end
|
|
155
200
|
|
|
156
|
-
def attempts_left_for(assignment)
|
|
157
|
-
max_attempts_for(assignment.exercise) - (assignment.attempts_count || 0)
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def limited_for?(exercise)
|
|
161
|
-
max_attempts_for(exercise).present?
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def results_hidden_for?(exercise)
|
|
165
|
-
exercise.choice? && results_hidden_for_choices?
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def resettable?
|
|
169
|
-
false
|
|
170
|
-
end
|
|
171
|
-
|
|
172
201
|
private
|
|
173
202
|
|
|
174
203
|
def max_attempts_for(exercise)
|
|
175
204
|
exercise.choice? ? max_choice_submissions : max_problem_submissions
|
|
176
205
|
end
|
|
206
|
+
|
|
177
207
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
class Exam::PassingCriterion
|
|
2
|
+
|
|
3
|
+
attr_reader :value
|
|
4
|
+
|
|
5
|
+
def initialize(value)
|
|
6
|
+
@value = value
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def type
|
|
10
|
+
self.class.name.demodulize.underscore
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def as_json
|
|
14
|
+
{type: type, value: value}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def ensure_valid!
|
|
18
|
+
raise "Invalid criterion value #{value} for #{type}" unless valid_passing_grade?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.parse(type, value)
|
|
22
|
+
parse_criterion_type(type, value)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.parse_criterion_type(type, value)
|
|
26
|
+
"Exam::PassingCriterion::#{type.camelize}".constantize.new(value)
|
|
27
|
+
rescue
|
|
28
|
+
raise "Invalid criterion type #{type}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class Exam::PassingCriterion::None < Exam::PassingCriterion
|
|
34
|
+
def initialize(_)
|
|
35
|
+
@value = nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def valid_passing_grade?
|
|
39
|
+
!value
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class Exam::PassingCriterion::Percentage < Exam::PassingCriterion
|
|
44
|
+
def valid_passing_grade?
|
|
45
|
+
value.between? 0, 100
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class Exam::PassingCriterion::PassedExercises < Exam::PassingCriterion
|
|
50
|
+
def valid_passing_grade?
|
|
51
|
+
value >= 0
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -45,6 +45,12 @@ class Problem < QueriableChallenge
|
|
|
45
45
|
own_expectations.present? || own_custom_expectations.present?
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
# Sets the layout. This method accepts input_kids as a synonym of input_primary
|
|
49
|
+
# for historical reasons
|
|
50
|
+
def layout=(layout)
|
|
51
|
+
self[:layout] = layout.like?(:input_kids) ? :input_primary : layout
|
|
52
|
+
end
|
|
53
|
+
|
|
48
54
|
private
|
|
49
55
|
|
|
50
56
|
def ensure_evaluation_criteria
|
data/app/models/invitation.rb
CHANGED
|
@@ -2,12 +2,18 @@ class Invitation < ApplicationRecord
|
|
|
2
2
|
include Mumuki::Domain::Syncable
|
|
3
3
|
|
|
4
4
|
belongs_to :course
|
|
5
|
+
|
|
6
|
+
validate :ensure_not_expired, on: :create
|
|
5
7
|
validates_uniqueness_of :code
|
|
6
8
|
|
|
7
9
|
defaults do
|
|
8
10
|
self.code ||= self.class.generate_code
|
|
9
11
|
end
|
|
10
12
|
|
|
13
|
+
def ensure_not_expired
|
|
14
|
+
errors.add(:base, :invitation_expired) if expired?
|
|
15
|
+
end
|
|
16
|
+
|
|
11
17
|
def import_from_resource_h!(json)
|
|
12
18
|
update! json.merge(course: Course.locate!(json[:course]))
|
|
13
19
|
end
|
|
@@ -25,7 +31,7 @@ class Invitation < ApplicationRecord
|
|
|
25
31
|
end
|
|
26
32
|
|
|
27
33
|
def to_resource_h
|
|
28
|
-
{
|
|
34
|
+
{code: code, course: course_slug, expiration_date: expiration_date}
|
|
29
35
|
end
|
|
30
36
|
|
|
31
37
|
def navigation_end?
|
data/app/models/message.rb
CHANGED
|
@@ -6,6 +6,10 @@ class Message < ApplicationRecord
|
|
|
6
6
|
|
|
7
7
|
validates_presence_of :content, :sender
|
|
8
8
|
validates_presence_of :submission_id, :unless => :discussion_id?
|
|
9
|
+
|
|
10
|
+
after_save :update_counters_cache!
|
|
11
|
+
after_destroy :update_counters_cache!
|
|
12
|
+
|
|
9
13
|
markdown_on :content
|
|
10
14
|
|
|
11
15
|
def notify!
|
|
@@ -16,6 +20,10 @@ class Message < ApplicationRecord
|
|
|
16
20
|
sender_user == discussion&.initiator
|
|
17
21
|
end
|
|
18
22
|
|
|
23
|
+
def from_moderator?
|
|
24
|
+
sender_user.moderator_here?
|
|
25
|
+
end
|
|
26
|
+
|
|
19
27
|
def from_user?(user)
|
|
20
28
|
sender_user == user
|
|
21
29
|
end
|
|
@@ -33,9 +41,9 @@ class Message < ApplicationRecord
|
|
|
33
41
|
end
|
|
34
42
|
|
|
35
43
|
def to_resource_h
|
|
36
|
-
as_json(except: [:id, :type, :discussion_id, :approved],
|
|
44
|
+
as_json(except: [:id, :type, :discussion_id, :approved, :not_actually_a_question],
|
|
37
45
|
include: {exercise: {only: [:bibliotheca_id]}})
|
|
38
|
-
|
|
46
|
+
.merge(organization: Organization.current.name)
|
|
39
47
|
end
|
|
40
48
|
|
|
41
49
|
def read!
|
|
@@ -46,11 +54,27 @@ class Message < ApplicationRecord
|
|
|
46
54
|
toggle! :approved
|
|
47
55
|
end
|
|
48
56
|
|
|
57
|
+
def toggle_not_actually_a_question!
|
|
58
|
+
toggle! :not_actually_a_question
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def validated?
|
|
62
|
+
approved? || from_moderator?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def update_counters_cache!
|
|
66
|
+
discussion&.update_counters!
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def question?
|
|
70
|
+
from_initiator? && !not_actually_a_question?
|
|
71
|
+
end
|
|
72
|
+
|
|
49
73
|
def self.parse_json(json)
|
|
50
74
|
message = json.delete 'message'
|
|
51
75
|
json
|
|
52
|
-
|
|
53
|
-
|
|
76
|
+
.except('uid', 'exercise_id')
|
|
77
|
+
.merge(message)
|
|
54
78
|
end
|
|
55
79
|
|
|
56
80
|
def self.read_all!
|
data/app/models/user.rb
CHANGED
|
@@ -2,8 +2,10 @@ FactoryBot.define do
|
|
|
2
2
|
factory :course do
|
|
3
3
|
period { '2016' }
|
|
4
4
|
shifts { %w(morning) }
|
|
5
|
+
code { "k1234-#{SecureRandom.uuid}" }
|
|
5
6
|
days { %w(monday wednesday) }
|
|
6
7
|
description { 'test' }
|
|
7
8
|
organization_id { 1 }
|
|
9
|
+
slug { "#{Organization.current.name}/#{period}-#{code}" }
|
|
8
10
|
end
|
|
9
11
|
end
|
|
@@ -5,14 +5,6 @@ module Mumuki::Domain::Status::Discussion::Opened
|
|
|
5
5
|
true
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
-
def self.reachable_statuses_for_initiator(discussion)
|
|
9
|
-
if discussion.has_responses?
|
|
10
|
-
[Mumuki::Domain::Status::Discussion::PendingReview]
|
|
11
|
-
else
|
|
12
|
-
[Mumuki::Domain::Status::Discussion::Closed]
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
|
|
16
8
|
def self.reachable_statuses_for_moderator(discussion)
|
|
17
9
|
if discussion.has_responses?
|
|
18
10
|
[Mumuki::Domain::Status::Discussion::Closed, Mumuki::Domain::Status::Discussion::Solved]
|
|
@@ -9,6 +9,7 @@ class Mumuki::Domain::Submission::Base
|
|
|
9
9
|
:submission_id, :queries, :query_results, :manual_evaluation_comment]
|
|
10
10
|
|
|
11
11
|
attr_accessor *ATTRIBUTES
|
|
12
|
+
attr_accessor :client_result
|
|
12
13
|
|
|
13
14
|
def self.from_attributes(*args)
|
|
14
15
|
new ATTRIBUTES.zip(args).to_h
|
|
@@ -26,6 +27,11 @@ class Mumuki::Domain::Submission::Base
|
|
|
26
27
|
results
|
|
27
28
|
end
|
|
28
29
|
|
|
30
|
+
def with_client_result(result)
|
|
31
|
+
self.client_result = result if result.present?
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
29
35
|
def evaluate!(assignment)
|
|
30
36
|
try_evaluate! assignment
|
|
31
37
|
rescue => e
|
|
@@ -2,6 +2,8 @@ class Mumuki::Domain::Submission::Solution < Mumuki::Domain::Submission::Persist
|
|
|
2
2
|
attr_accessor :content
|
|
3
3
|
|
|
4
4
|
def try_evaluate!(assignment)
|
|
5
|
-
assignment
|
|
5
|
+
assignment
|
|
6
|
+
.run_tests!({client_result: client_result}.compact.merge(content: content))
|
|
7
|
+
.except(:response_type)
|
|
6
8
|
end
|
|
7
9
|
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: 7.
|
|
4
|
+
version: 7.7.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: 2020-
|
|
11
|
+
date: 2020-07-31 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -299,6 +299,7 @@ files:
|
|
|
299
299
|
- app/models/discussion.rb
|
|
300
300
|
- app/models/event.rb
|
|
301
301
|
- app/models/exam.rb
|
|
302
|
+
- app/models/exam/passing_criterion.rb
|
|
302
303
|
- app/models/exam_authorization.rb
|
|
303
304
|
- app/models/exercise.rb
|
|
304
305
|
- app/models/exercise/challenge.rb
|
|
@@ -604,7 +605,13 @@ files:
|
|
|
604
605
|
- db/migrate/20200508191543_create_avatars.rb
|
|
605
606
|
- db/migrate/20200518135658_add_avatar_to_users.rb
|
|
606
607
|
- db/migrate/20200527180729_add_disabled_at_to_users.rb
|
|
608
|
+
- db/migrate/20200601203033_add_course_to_exam.rb
|
|
609
|
+
- db/migrate/20200605161350_add_passing_criterions_to_exam.rb
|
|
607
610
|
- db/migrate/20200608132959_add_progressive_display_lookahead_to_organizations.rb
|
|
611
|
+
- db/migrate/20200702165503_add_messages_count_to_discussion.rb
|
|
612
|
+
- db/migrate/20200728162727_add_not_actually_a_question_field_to_messages.rb
|
|
613
|
+
- db/migrate/20200728163038_add_requires_moderator_response_to_discussions.rb
|
|
614
|
+
- db/migrate/20200731081757_add_last_moderator_access_fields_to_discussion.rb
|
|
608
615
|
- lib/mumuki/domain.rb
|
|
609
616
|
- lib/mumuki/domain/area.rb
|
|
610
617
|
- lib/mumuki/domain/engine.rb
|
|
@@ -714,8 +721,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
714
721
|
- !ruby/object:Gem::Version
|
|
715
722
|
version: '0'
|
|
716
723
|
requirements: []
|
|
717
|
-
|
|
718
|
-
rubygems_version: 2.7.7
|
|
724
|
+
rubygems_version: 3.0.8
|
|
719
725
|
signing_key:
|
|
720
726
|
specification_version: 4
|
|
721
727
|
summary: Mumuki Platform's Domain Model
|