mumuki-classroom 8.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +661 -0
- data/README.md +371 -0
- data/Rakefile +29 -0
- data/lib/mumuki/classroom.rb +56 -0
- data/lib/mumuki/classroom/collection.rb +10 -0
- data/lib/mumuki/classroom/engine.rb +8 -0
- data/lib/mumuki/classroom/event.rb +4 -0
- data/lib/mumuki/classroom/event/user_changed.rb +54 -0
- data/lib/mumuki/classroom/locales/en.yml +3 -0
- data/lib/mumuki/classroom/locales/es-CL.yml +3 -0
- data/lib/mumuki/classroom/locales/es.yml +3 -0
- data/lib/mumuki/classroom/locales/pt.yml +3 -0
- data/lib/mumuki/classroom/models.rb +22 -0
- data/lib/mumuki/classroom/models/assignment.rb +148 -0
- data/lib/mumuki/classroom/models/concerns/course_member.rb +53 -0
- data/lib/mumuki/classroom/models/concerns/extensions.rb +90 -0
- data/lib/mumuki/classroom/models/concerns/with_failed_submission_reprocess.rb +74 -0
- data/lib/mumuki/classroom/models/concerns/with_submission_process.rb +148 -0
- data/lib/mumuki/classroom/models/document.rb +12 -0
- data/lib/mumuki/classroom/models/exercise.rb +7 -0
- data/lib/mumuki/classroom/models/failed_submission.rb +16 -0
- data/lib/mumuki/classroom/models/follower.rb +19 -0
- data/lib/mumuki/classroom/models/guide_progress.rb +57 -0
- data/lib/mumuki/classroom/models/last_assignment.rb +7 -0
- data/lib/mumuki/classroom/models/message.rb +32 -0
- data/lib/mumuki/classroom/models/notification.rb +52 -0
- data/lib/mumuki/classroom/models/reporting.rb +24 -0
- data/lib/mumuki/classroom/models/searching.rb +60 -0
- data/lib/mumuki/classroom/models/searching/guide_progress.rb +90 -0
- data/lib/mumuki/classroom/models/sorting.rb +92 -0
- data/lib/mumuki/classroom/models/sorting/guide_progress.rb +147 -0
- data/lib/mumuki/classroom/models/sorting/student.rb +45 -0
- data/lib/mumuki/classroom/models/sorting/total_stats_sort_by.rb +5 -0
- data/lib/mumuki/classroom/models/student.rb +88 -0
- data/lib/mumuki/classroom/models/submission.rb +62 -0
- data/lib/mumuki/classroom/models/suggestion.rb +35 -0
- data/lib/mumuki/classroom/models/teacher.rb +3 -0
- data/lib/mumuki/classroom/permissions_diff.rb +83 -0
- data/lib/mumuki/classroom/reports.rb +4 -0
- data/lib/mumuki/classroom/reports/formats.rb +35 -0
- data/lib/mumuki/classroom/sinatra.rb +301 -0
- data/lib/mumuki/classroom/sinatra/courses.rb +157 -0
- data/lib/mumuki/classroom/sinatra/errors.rb +13 -0
- data/lib/mumuki/classroom/sinatra/exams.rb +71 -0
- data/lib/mumuki/classroom/sinatra/followers.rb +29 -0
- data/lib/mumuki/classroom/sinatra/guides.rb +79 -0
- data/lib/mumuki/classroom/sinatra/manual_evaluation.rb +16 -0
- data/lib/mumuki/classroom/sinatra/massive.rb +206 -0
- data/lib/mumuki/classroom/sinatra/messages.rb +52 -0
- data/lib/mumuki/classroom/sinatra/notifications.rb +29 -0
- data/lib/mumuki/classroom/sinatra/organization.rb +7 -0
- data/lib/mumuki/classroom/sinatra/pagination.rb +13 -0
- data/lib/mumuki/classroom/sinatra/permissions.rb +9 -0
- data/lib/mumuki/classroom/sinatra/ping.rb +7 -0
- data/lib/mumuki/classroom/sinatra/searching.rb +27 -0
- data/lib/mumuki/classroom/sinatra/students.rb +111 -0
- data/lib/mumuki/classroom/sinatra/suggestions.rb +17 -0
- data/lib/mumuki/classroom/sinatra/teachers.rb +14 -0
- data/lib/mumuki/classroom/version.rb +5 -0
- data/lib/mumuki/profile.rb +17 -0
- data/lib/mumuki/views/threads.html.erb +43 -0
- data/lib/tasks/mumuki/messages.rake +20 -0
- data/lib/tasks/mumuki/resubmissions.rake +15 -0
- data/lib/tasks/mumuki/students.rake +31 -0
- data/lib/tasks/mumuki/submissions.rake +17 -0
- data/lib/tasks/mumuki/user_permissions.rake +17 -0
- metadata +291 -0
@@ -0,0 +1,90 @@
|
|
1
|
+
module Searching
|
2
|
+
module GuideProgress
|
3
|
+
module QueryOperands
|
4
|
+
include Searching::QueryOperands
|
5
|
+
|
6
|
+
def more_than(value)
|
7
|
+
{'$gte': value}
|
8
|
+
end
|
9
|
+
|
10
|
+
def less_than(value)
|
11
|
+
{'$lte': value}
|
12
|
+
end
|
13
|
+
|
14
|
+
def close_to(value)
|
15
|
+
more_than(value - 1).merge(less_than value + 1)
|
16
|
+
end
|
17
|
+
|
18
|
+
def default_query_operand
|
19
|
+
:more_than
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class NotFailedAssignments < NumericFilter
|
24
|
+
include Searching::GuideProgress::QueryOperands
|
25
|
+
|
26
|
+
def pipeline
|
27
|
+
[
|
28
|
+
{
|
29
|
+
'$addFields':
|
30
|
+
{'stats.not_failed': {'$sum': %w($stats.passed $stats.passed_with_warnings)}}
|
31
|
+
},
|
32
|
+
{
|
33
|
+
'$match':
|
34
|
+
{'stats.not_failed': current_query_operand }
|
35
|
+
}
|
36
|
+
]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class PassedAssignments < NumericFilter
|
41
|
+
include Searching::GuideProgress::QueryOperands
|
42
|
+
|
43
|
+
def query
|
44
|
+
{'stats.passed': current_query_operand}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class TotalAssignments < NumericFilter
|
49
|
+
include Searching::GuideProgress::QueryOperands
|
50
|
+
|
51
|
+
def pipeline
|
52
|
+
[
|
53
|
+
{
|
54
|
+
'$addFields': {'stats.total': {'$sum': %w($stats.passed $stats.passed_with_warnings $stats.failed)}}
|
55
|
+
},
|
56
|
+
{
|
57
|
+
'$match': {'stats.total': current_query_operand }
|
58
|
+
}
|
59
|
+
]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class SolvedAssignmentsPercentage < NumericFilter
|
64
|
+
include Searching::GuideProgress::QueryOperands
|
65
|
+
|
66
|
+
def pipeline
|
67
|
+
[
|
68
|
+
{
|
69
|
+
'$addFields': {
|
70
|
+
'stats.solved_percentage': {
|
71
|
+
'$multiply': [
|
72
|
+
{
|
73
|
+
'$divide': [
|
74
|
+
{'$sum': %w($stats.passed $stats.passed_with_warnings)},
|
75
|
+
{'$sum': %w($stats.passed $stats.passed_with_warnings $stats.failed)}
|
76
|
+
]
|
77
|
+
},
|
78
|
+
100
|
79
|
+
]
|
80
|
+
}
|
81
|
+
}
|
82
|
+
},
|
83
|
+
{
|
84
|
+
'$match': {'stats.solved_percentage': current_query_operand }
|
85
|
+
}
|
86
|
+
]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Sorting
|
2
|
+
|
3
|
+
def self.aggregate(collection, query, paginated_params, query_params)
|
4
|
+
reporting_pipeline = Reporting.build_pipeline(collection, query, paginated_params, query_params, projection)
|
5
|
+
query = collection.collection.aggregate(pipeline paginated_params, reporting_pipeline).first
|
6
|
+
query_results(query)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.query_results(query)
|
10
|
+
total = query[:total].first
|
11
|
+
[total.blank? ? 0 : total[:count], query[:results]]
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.pipeline(params, pipeline)
|
15
|
+
paging_pipeline = []
|
16
|
+
paging_pipeline << {'$skip': params[:page] * params[:per_page]}
|
17
|
+
paging_pipeline << {'$limit': params[:per_page]}
|
18
|
+
pipeline << {'$facet': {
|
19
|
+
results: paging_pipeline,
|
20
|
+
total: [
|
21
|
+
{
|
22
|
+
'$count': 'count'
|
23
|
+
}
|
24
|
+
]
|
25
|
+
}}
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.projection
|
29
|
+
{
|
30
|
+
'_id': 0,
|
31
|
+
'assignments': 0,
|
32
|
+
'notifications': 0,
|
33
|
+
'guide._id': 0,
|
34
|
+
'student._id': 0,
|
35
|
+
'last_assignment._id': 0,
|
36
|
+
'last_assignment.guide._id': 0,
|
37
|
+
'last_assignment.exercise._id': 0,
|
38
|
+
'last_assignment.submission._id': 0,
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
class SortBy
|
43
|
+
def self.pipeline
|
44
|
+
[]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
module Criteria
|
51
|
+
module Base
|
52
|
+
def bson_type
|
53
|
+
value.bson_type
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_bson(*args)
|
57
|
+
value.to_bson(*args)
|
58
|
+
end
|
59
|
+
|
60
|
+
def !
|
61
|
+
negated
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
module Asc
|
66
|
+
extend Criteria::Base
|
67
|
+
|
68
|
+
def self.value
|
69
|
+
1
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.negated
|
73
|
+
Desc
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
module Desc
|
78
|
+
extend Criteria::Base
|
79
|
+
|
80
|
+
def self.value
|
81
|
+
-1
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.negated
|
85
|
+
Asc
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
require_relative './sorting/total_stats_sort_by'
|
91
|
+
require_relative './sorting/student'
|
92
|
+
require_relative './sorting/guide_progress'
|
@@ -0,0 +1,147 @@
|
|
1
|
+
module Sorting
|
2
|
+
module GuideProgress
|
3
|
+
class ByMessages < SortBy
|
4
|
+
def self.pipeline
|
5
|
+
[
|
6
|
+
{'$lookup': lookup_notifications},
|
7
|
+
{'$addFields': filter_unread_notifications},
|
8
|
+
{'$unwind': unwind_notifications},
|
9
|
+
{'$lookup': lookup_assignments},
|
10
|
+
{'$addFields': filter_guide_assignments},
|
11
|
+
{'$group': group_by_students_uid},
|
12
|
+
{'$addFields': generate_guide_progress},
|
13
|
+
{'$addFields': add_progress_count},
|
14
|
+
{'$replaceRoot': progress_to_document_root},
|
15
|
+
]
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.progress_to_document_root
|
19
|
+
{'newRoot': '$progresses'}
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.add_progress_count
|
23
|
+
{
|
24
|
+
'progresses.unread': '$count'
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.generate_guide_progress
|
29
|
+
{
|
30
|
+
'progresses': {'$arrayElemAt': ['$progresses', 0]},
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.group_by_students_uid
|
35
|
+
{
|
36
|
+
'_id': '$student.uid',
|
37
|
+
'progresses': {'$push': '$$ROOT'},
|
38
|
+
'count': {
|
39
|
+
'$sum': {
|
40
|
+
'$cond': {
|
41
|
+
'if': {'$anyElementTrue': ['$assignments']},
|
42
|
+
'then': 1,
|
43
|
+
'else': 0
|
44
|
+
}
|
45
|
+
}
|
46
|
+
}
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.filter_guide_assignments
|
51
|
+
{
|
52
|
+
'assignments': {
|
53
|
+
'$filter': {
|
54
|
+
'as': 'assignment',
|
55
|
+
'input': '$assignments',
|
56
|
+
'cond': {
|
57
|
+
'$eq': %w($$assignment.guide.slug $guide.slug),
|
58
|
+
}
|
59
|
+
}
|
60
|
+
}
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.lookup_assignments
|
65
|
+
{
|
66
|
+
'from': 'assignments',
|
67
|
+
'localField': 'notifications.assignment_id',
|
68
|
+
'foreignField': '_id',
|
69
|
+
'as': 'assignments'
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.unwind_notifications
|
74
|
+
{
|
75
|
+
'path': '$notifications',
|
76
|
+
'preserveNullAndEmptyArrays': true
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.filter_unread_notifications
|
81
|
+
{
|
82
|
+
'notifications': {
|
83
|
+
'$filter': {
|
84
|
+
'as': 'notification',
|
85
|
+
'input': '$notifications',
|
86
|
+
'cond': {
|
87
|
+
'$and': [
|
88
|
+
{'$eq': %w($$notification.organization $organization)},
|
89
|
+
{'$eq': %w($$notification.sender $student.uid)},
|
90
|
+
{'$eq': %w($$notification.course $course)},
|
91
|
+
{'$eq': ['$$notification.read', false]}
|
92
|
+
]
|
93
|
+
}
|
94
|
+
}
|
95
|
+
}
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.lookup_notifications
|
100
|
+
{
|
101
|
+
'from': 'notifications',
|
102
|
+
'localField': 'organization',
|
103
|
+
'foreignField': 'organization',
|
104
|
+
'as': 'notifications'
|
105
|
+
}
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.order_by(ordering)
|
109
|
+
{'unread': ordering,
|
110
|
+
'student.last_name': ordering,
|
111
|
+
'student.first_name': ordering}
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class ByName < SortBy
|
116
|
+
def self.pipeline
|
117
|
+
[{'$addFields': {
|
118
|
+
'student.last_name': {'$toLower': '$student.last_name'},
|
119
|
+
'student.first_name': {'$toLower': '$student.first_name'}
|
120
|
+
}}]
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.order_by(ordering)
|
124
|
+
{'student.last_name': ordering,
|
125
|
+
'student.first_name': ordering}
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
class ByProgress < SortBy
|
130
|
+
def self.order_by(ordering)
|
131
|
+
{'stats.passed': ordering,
|
132
|
+
'stats.passed_with_warnings': ordering,
|
133
|
+
'stats.failed': ordering,
|
134
|
+
'student.last_name': ordering,
|
135
|
+
'student.first_name': ordering}
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
class ByLastSubmissionDate < SortBy
|
140
|
+
def self.order_by(ordering)
|
141
|
+
{'last_assignment.submission.created_at': !ordering,
|
142
|
+
'last_name': ordering,
|
143
|
+
'first_name': ordering}
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Sorting
|
2
|
+
module Student
|
3
|
+
class ByName < SortBy
|
4
|
+
def self.pipeline
|
5
|
+
[{'$addFields': {
|
6
|
+
'last_name': {'$toLower': '$last_name'},
|
7
|
+
'first_name': {'$toLower': '$first_name'}
|
8
|
+
}}]
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.order_by(ordering)
|
12
|
+
{'last_name': ordering,
|
13
|
+
'first_name': ordering}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class ByProgress < TotalStatsSortBy
|
18
|
+
|
19
|
+
def self.order_by(ordering)
|
20
|
+
{'stats.total': ordering,
|
21
|
+
'stats.failed': !ordering,
|
22
|
+
'stats.passed_with_warnings': !ordering,
|
23
|
+
'stats.passed': !ordering,
|
24
|
+
'last_name': ordering,
|
25
|
+
'first_name': ordering}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class BySignupDate < SortBy
|
30
|
+
def self.order_by(ordering)
|
31
|
+
{'created_at': !ordering,
|
32
|
+
'last_name': ordering,
|
33
|
+
'first_name': ordering}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class ByLastSubmissionDate < SortBy
|
38
|
+
def self.order_by(ordering)
|
39
|
+
{'last_assignment.submission.created_at': !ordering,
|
40
|
+
'last_name': ordering,
|
41
|
+
'first_name': ordering}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
class Mumuki::Classroom::Student < Mumuki::Classroom::Document
|
2
|
+
include CourseMember
|
3
|
+
|
4
|
+
field :personal_id, type: String
|
5
|
+
field :stats, type: Hash
|
6
|
+
field :detached, type: Mongoid::Boolean
|
7
|
+
field :detached_at, type: Time
|
8
|
+
embeds_one :last_assignment, class_name: 'Mumuki::Classroom::LastAssignment'
|
9
|
+
|
10
|
+
create_index({organization: 1, uid: 1})
|
11
|
+
create_index({'last_assignment.guide.slug': 1, 'last_assignment.exercise.eid': 1}, {name: 'ExBibIdIndex'})
|
12
|
+
create_index({first_name: 'text', last_name: 'text', email: 'text', personal_id: 'text'})
|
13
|
+
|
14
|
+
def course_name
|
15
|
+
course.to_mumukit_slug.course
|
16
|
+
end
|
17
|
+
|
18
|
+
def destroy_cascade!
|
19
|
+
Mumuki::Classroom::GuideProgress.destroy_all_by!(sub_student_query uid)
|
20
|
+
Mumuki::Classroom::Assignment.destroy_all_by!(sub_student_query uid)
|
21
|
+
destroy!
|
22
|
+
end
|
23
|
+
|
24
|
+
def update_all_stats
|
25
|
+
all_stats = Mumuki::Classroom::Assignment.stats_by(sub_student_query uid)
|
26
|
+
update_attributes!(stats: all_stats)
|
27
|
+
end
|
28
|
+
|
29
|
+
def sub_student_query(uid)
|
30
|
+
{'organization': organization, 'course': course, 'student.uid': uid}
|
31
|
+
end
|
32
|
+
|
33
|
+
def detach!
|
34
|
+
update_attributes! detached: true, detached_at: Time.now
|
35
|
+
Mumuki::Classroom::Assignment.detach_all_by! sub_student_query(uid)
|
36
|
+
Mumuki::Classroom::GuideProgress.detach_all_by! sub_student_query(uid)
|
37
|
+
end
|
38
|
+
|
39
|
+
def attach!
|
40
|
+
unset :detached, :detached_at
|
41
|
+
Mumuki::Classroom::Assignment.attach_all_by! sub_student_query(uid)
|
42
|
+
Mumuki::Classroom::GuideProgress.attach_all_by! sub_student_query(uid)
|
43
|
+
end
|
44
|
+
|
45
|
+
def transfer_to!(organization, course)
|
46
|
+
Mumuki::Classroom::Assignment.transfer_all_by! sub_student_query(uid), organization, course
|
47
|
+
Mumuki::Classroom::GuideProgress.transfer_all_by! sub_student_query(uid), organization, course
|
48
|
+
update_attributes! organization: organization, course: course
|
49
|
+
end
|
50
|
+
|
51
|
+
def update_last_assignment_for
|
52
|
+
update_attributes!(last_assignment: Mumuki::Classroom::GuideProgress.last_assignment_by(sub_student_query uid))
|
53
|
+
end
|
54
|
+
|
55
|
+
class << self
|
56
|
+
def report(criteria, &block)
|
57
|
+
where(criteria).select(&block).as_json(only: [:first_name, :last_name, :email, :created_at, :detached_at])
|
58
|
+
end
|
59
|
+
|
60
|
+
def update_all_stats(options)
|
61
|
+
where(options).each(&:update_all_stats)
|
62
|
+
end
|
63
|
+
|
64
|
+
def last_updated_student_by(query)
|
65
|
+
where(query).ne(detached: true).order_by(updated_at: :desc).first
|
66
|
+
end
|
67
|
+
|
68
|
+
def detach_all_by!(uids, query)
|
69
|
+
where(query).in(uid: uids).update_all(detached: true, detached_at: Time.now)
|
70
|
+
criteria = query.merge('student.uid': {'$in': uids})
|
71
|
+
Mumuki::Classroom::Assignment.detach_all_by! criteria
|
72
|
+
Mumuki::Classroom::GuideProgress.detach_all_by! criteria
|
73
|
+
end
|
74
|
+
|
75
|
+
def attach_all_by!(uids, query)
|
76
|
+
where(query).in(uid: uids).unset(:detached, :detached_at)
|
77
|
+
criteria = query.merge('student.uid': {'$in': uids})
|
78
|
+
Mumuki::Classroom::Assignment.attach_all_by! criteria
|
79
|
+
Mumuki::Classroom::GuideProgress.attach_all_by! criteria
|
80
|
+
end
|
81
|
+
|
82
|
+
def uid_field
|
83
|
+
:uid
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|