mumuki-classroom 8.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +661 -0
  3. data/README.md +371 -0
  4. data/Rakefile +29 -0
  5. data/lib/mumuki/classroom.rb +56 -0
  6. data/lib/mumuki/classroom/collection.rb +10 -0
  7. data/lib/mumuki/classroom/engine.rb +8 -0
  8. data/lib/mumuki/classroom/event.rb +4 -0
  9. data/lib/mumuki/classroom/event/user_changed.rb +54 -0
  10. data/lib/mumuki/classroom/locales/en.yml +3 -0
  11. data/lib/mumuki/classroom/locales/es-CL.yml +3 -0
  12. data/lib/mumuki/classroom/locales/es.yml +3 -0
  13. data/lib/mumuki/classroom/locales/pt.yml +3 -0
  14. data/lib/mumuki/classroom/models.rb +22 -0
  15. data/lib/mumuki/classroom/models/assignment.rb +148 -0
  16. data/lib/mumuki/classroom/models/concerns/course_member.rb +53 -0
  17. data/lib/mumuki/classroom/models/concerns/extensions.rb +90 -0
  18. data/lib/mumuki/classroom/models/concerns/with_failed_submission_reprocess.rb +74 -0
  19. data/lib/mumuki/classroom/models/concerns/with_submission_process.rb +148 -0
  20. data/lib/mumuki/classroom/models/document.rb +12 -0
  21. data/lib/mumuki/classroom/models/exercise.rb +7 -0
  22. data/lib/mumuki/classroom/models/failed_submission.rb +16 -0
  23. data/lib/mumuki/classroom/models/follower.rb +19 -0
  24. data/lib/mumuki/classroom/models/guide_progress.rb +57 -0
  25. data/lib/mumuki/classroom/models/last_assignment.rb +7 -0
  26. data/lib/mumuki/classroom/models/message.rb +32 -0
  27. data/lib/mumuki/classroom/models/notification.rb +52 -0
  28. data/lib/mumuki/classroom/models/reporting.rb +24 -0
  29. data/lib/mumuki/classroom/models/searching.rb +60 -0
  30. data/lib/mumuki/classroom/models/searching/guide_progress.rb +90 -0
  31. data/lib/mumuki/classroom/models/sorting.rb +92 -0
  32. data/lib/mumuki/classroom/models/sorting/guide_progress.rb +147 -0
  33. data/lib/mumuki/classroom/models/sorting/student.rb +45 -0
  34. data/lib/mumuki/classroom/models/sorting/total_stats_sort_by.rb +5 -0
  35. data/lib/mumuki/classroom/models/student.rb +88 -0
  36. data/lib/mumuki/classroom/models/submission.rb +62 -0
  37. data/lib/mumuki/classroom/models/suggestion.rb +35 -0
  38. data/lib/mumuki/classroom/models/teacher.rb +3 -0
  39. data/lib/mumuki/classroom/permissions_diff.rb +83 -0
  40. data/lib/mumuki/classroom/reports.rb +4 -0
  41. data/lib/mumuki/classroom/reports/formats.rb +35 -0
  42. data/lib/mumuki/classroom/sinatra.rb +301 -0
  43. data/lib/mumuki/classroom/sinatra/courses.rb +157 -0
  44. data/lib/mumuki/classroom/sinatra/errors.rb +13 -0
  45. data/lib/mumuki/classroom/sinatra/exams.rb +71 -0
  46. data/lib/mumuki/classroom/sinatra/followers.rb +29 -0
  47. data/lib/mumuki/classroom/sinatra/guides.rb +79 -0
  48. data/lib/mumuki/classroom/sinatra/manual_evaluation.rb +16 -0
  49. data/lib/mumuki/classroom/sinatra/massive.rb +206 -0
  50. data/lib/mumuki/classroom/sinatra/messages.rb +52 -0
  51. data/lib/mumuki/classroom/sinatra/notifications.rb +29 -0
  52. data/lib/mumuki/classroom/sinatra/organization.rb +7 -0
  53. data/lib/mumuki/classroom/sinatra/pagination.rb +13 -0
  54. data/lib/mumuki/classroom/sinatra/permissions.rb +9 -0
  55. data/lib/mumuki/classroom/sinatra/ping.rb +7 -0
  56. data/lib/mumuki/classroom/sinatra/searching.rb +27 -0
  57. data/lib/mumuki/classroom/sinatra/students.rb +111 -0
  58. data/lib/mumuki/classroom/sinatra/suggestions.rb +17 -0
  59. data/lib/mumuki/classroom/sinatra/teachers.rb +14 -0
  60. data/lib/mumuki/classroom/version.rb +5 -0
  61. data/lib/mumuki/profile.rb +17 -0
  62. data/lib/mumuki/views/threads.html.erb +43 -0
  63. data/lib/tasks/mumuki/messages.rake +20 -0
  64. data/lib/tasks/mumuki/resubmissions.rake +15 -0
  65. data/lib/tasks/mumuki/students.rake +31 -0
  66. data/lib/tasks/mumuki/submissions.rake +17 -0
  67. data/lib/tasks/mumuki/user_permissions.rake +17 -0
  68. 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,5 @@
1
+ class TotalStatsSortBy
2
+ def self.pipeline
3
+ [{'$addFields': {'stats.total': {'$sum': %w($stats.passed $stats.passed_with_warnings $stats.failed)}}}]
4
+ end
5
+ 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
+