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,148 @@
1
+ module WithSubmissionProcess
2
+ def process!(data)
3
+ json = data.deep_symbolize_keys
4
+
5
+ json[:course] = find_submission_course! json
6
+ json[:student] = find_student_from json
7
+
8
+ update_assignment json
9
+ update_guide_progress json
10
+ update_student_progress json
11
+ update_student_last_assignment json
12
+ end
13
+
14
+ def organization(json)
15
+ json[:organization]
16
+ end
17
+
18
+ def find_submission_course!(json)
19
+ student = Mumuki::Classroom::Student.last_updated_student_by(organization: organization(json), uid: uid(json))
20
+ raise ActiveRecord::RecordNotFound, "Mumuki::Classroom::Student not found" unless student
21
+ student.course
22
+ end
23
+
24
+ def find_student_from(json)
25
+ Mumuki::Classroom::Student.find_by(organization: organization(json), course: course_slug(json), uid: uid(json)).as_json
26
+ end
27
+
28
+ def update_student_progress(json)
29
+ Mumuki::Classroom::Student.find_by!(organization: organization(json), course: course_slug(json), uid: uid(json)).update_all_stats
30
+ end
31
+
32
+ def update_student_last_assignment(json)
33
+ Mumuki::Classroom::Student.find_by!(organization: organization(json), course: course_slug(json), uid: uid(json)).update_last_assignment_for
34
+ end
35
+
36
+ def update_assignment(json)
37
+ assignment = Mumuki::Classroom::Assignment
38
+ .where(assignment_query(json))
39
+ .first_or_create!(assignment_without_submission_from(json))
40
+ assignment.upsert_attributes(assignment_without_submission_from(json))
41
+ assignment.add_submission! submission_from(json)
42
+ end
43
+
44
+ def assignment_query(json)
45
+ guide_progress_query(json).merge 'exercise.eid': exercise_from(json)[:eid]
46
+ end
47
+
48
+ def guide_progress_query(json)
49
+ {'organization': organization(json),
50
+ 'course': course_slug(json),
51
+ 'guide.slug': guide_from(json)[:slug],
52
+ 'student.uid': uid(json)}
53
+ end
54
+
55
+ def update_guide_progress(json)
56
+ json[:stats] = student_stats_for json
57
+ Mumuki::Classroom::GuideProgress
58
+ .where(guide_progress_query(json))
59
+ .first_or_create!(guide_progress_from json)
60
+ .upsert_attributes(guide_progress_from json)
61
+ end
62
+
63
+ def student_stats_for(json)
64
+ Mumuki::Classroom::Assignment.stats_by guide_progress_query(json)
65
+ end
66
+
67
+ def uid(json)
68
+ json[:submitter][:uid]
69
+ end
70
+
71
+ def course_slug(json)
72
+ json[:course]
73
+ end
74
+
75
+ def guide_progress_from(json)
76
+ {guide: guide_from(json),
77
+ student: student_from(json),
78
+ stats: stats_from(json),
79
+ last_assignment: {exercise: exercise_from(json),
80
+ submission: submission_from(json)}}
81
+ end
82
+
83
+ def assignment_without_submission_from(json)
84
+ {guide: guide_from(json),
85
+ student: student_from(json),
86
+ exercise: exercise_from(json)}
87
+ end
88
+
89
+ def assignment_from(json)
90
+ assignment_without_submission_from.merge submission: submission_from(json)
91
+ end
92
+
93
+ def stats_from(json)
94
+ stats = json[:stats]
95
+
96
+ {passed: stats[:passed],
97
+ failed: stats[:failed],
98
+ passed_with_warnings: stats[:passed_with_warnings]}.compact
99
+ end
100
+
101
+ def student_from(json)
102
+ student = json[:student]
103
+
104
+ {uid: student[:uid],
105
+ name: student[:name],
106
+ email: student[:email],
107
+ image_url: student[:image_url],
108
+ social_id: student[:social_id],
109
+ last_name: student[:last_name],
110
+ first_name: student[:first_name]}.compact
111
+ end
112
+
113
+ def guide_from(json)
114
+ guide = json[:guide]
115
+
116
+ classroom_guide = {
117
+ slug: guide[:slug],
118
+ name: guide[:name],
119
+ parent: guide[:parent],
120
+ language: {
121
+ name: guide[:language][:name],
122
+ devicon: guide[:language][:devicon]
123
+ }.compact
124
+ }
125
+ classroom_guide.compact
126
+ end
127
+
128
+ def exercise_from(json)
129
+ exercise = json[:exercise]
130
+
131
+ {eid: exercise[:eid],
132
+ name: exercise[:name],
133
+ number: exercise[:number]}.compact
134
+ end
135
+
136
+ def submission_from(json)
137
+ {sid: json[:sid],
138
+ status: json[:status],
139
+ result: json[:result],
140
+ content: json[:content],
141
+ feedback: json[:feedback],
142
+ created_at: json[:created_at],
143
+ test_results: json[:test_results],
144
+ submissions_count: json[:submissions_count],
145
+ expectation_results: json[:expectation_results],
146
+ origin_ip: json[:origin_ip]}.compact
147
+ end
148
+ end
@@ -0,0 +1,12 @@
1
+ class Mumuki::Classroom::Document
2
+
3
+ def self.whitelist_attributes(json)
4
+ json.with_indifferent_access.except(:created_at, :updated_at, :_id).slice(*attribute_names)
5
+ end
6
+
7
+ def self.inherited(subclass)
8
+ super
9
+ subclass.include Mongoid::Document
10
+ subclass.store_in collection: subclass.name.demodulize.tableize
11
+ end
12
+ end
@@ -0,0 +1,7 @@
1
+ class Mumuki::Classroom::Exercise < Mumuki::Classroom::Document
2
+
3
+ field :eid, type: Integer
4
+ field :number, type: Integer
5
+ field :name, type: String
6
+
7
+ end
@@ -0,0 +1,16 @@
1
+ class Mumuki::Classroom::FailedSubmission < Mumuki::Classroom::Document
2
+
3
+ extend WithFailedSubmissionReprocess
4
+
5
+ include Mongoid::Attributes::Dynamic
6
+
7
+ field :created_at, type: Time
8
+
9
+ create_index 'organization': 1, 'submitter.uid': 1
10
+ create_index({'guide.slug': 1, 'exercise.eid': 1}, {name: 'ExBibIdIndex'})
11
+
12
+ scope :for, -> (organization) { where 'organization': organization }
13
+ scope :find_by_uid, -> (uid) { where 'submitter.uid': uid }
14
+
15
+
16
+ end
@@ -0,0 +1,19 @@
1
+ class Mumuki::Classroom::Follower < Mumuki::Classroom::Document
2
+ include Mongoid::Timestamps
3
+
4
+ field :uids, type: Array
5
+ field :email, type: String
6
+ field :course, type: String
7
+ field :organization, type: String
8
+
9
+ create_index({organization: 1, course: 1, email: 1}, {unique: true})
10
+
11
+ def add!(uid)
12
+ self.add_to_set uids: uid
13
+ end
14
+
15
+ def remove!(uid)
16
+ self.pull uids: uid
17
+ end
18
+
19
+ end
@@ -0,0 +1,57 @@
1
+ class Mumuki::Classroom::GuideProgress < Mumuki::Classroom::Document
2
+ include Mongoid::Timestamps
3
+
4
+ demodulized_model_name
5
+
6
+ field :organization, type: String
7
+ field :course, type: Mumukit::Auth::Slug
8
+ field :stats, type: Hash
9
+ field :guide, type: Hash
10
+
11
+ embeds_one :student, class_name: 'Mumuki::Classroom::Student'
12
+ embeds_one :last_assignment
13
+
14
+ create_index({'organization': 1, 'course': 1, 'student.uid': 1})
15
+ create_index({'organization': 1, 'course': 1, 'guide.slug': 1, 'student.uid': 1})
16
+ create_index({'guide.slug': 1, 'last_assignment.exercise.eid': 1}, {name: 'ExBibIdIndex'})
17
+ create_index({'student.first_name': 'text', 'student.last_name': 'text', 'student.email': 'text'})
18
+
19
+ def slug
20
+ guide[:slug]
21
+ end
22
+
23
+ class << self
24
+ def detach_all_by!(query)
25
+ where(query).set(detached: true)
26
+ end
27
+
28
+ def attach_all_by!(query)
29
+ where(query).unset(:detached)
30
+ end
31
+
32
+ def destroy_all_by!(query)
33
+ where(query).destroy
34
+ end
35
+
36
+ def transfer_all_by!(query, new_organization, new_course)
37
+ where(query).set(organization: new_organization, course: new_course)
38
+ end
39
+
40
+ def last_assignment_by(query)
41
+ where(query).order_by('last_assignment.submission.created_at': :desc).first.try do |it|
42
+ Mumuki::Classroom::LastAssignment.new(guide: it.guide,
43
+ exercise: it.last_assignment.exercise,
44
+ submission: {
45
+ sid: it.last_assignment.submission.sid,
46
+ status: it.last_assignment.submission.status,
47
+ created_at: it.last_assignment.submission.created_at,
48
+ })
49
+ end
50
+ end
51
+
52
+ def uid_field
53
+ 'student.uid'.to_sym
54
+ end
55
+ end
56
+
57
+ end
@@ -0,0 +1,7 @@
1
+ class Mumuki::Classroom::LastAssignment < Mumuki::Classroom::Document
2
+
3
+ field :guide, type: Hash
4
+ embeds_one :exercise
5
+ embeds_one :submission, class_name: 'Mumuki::Classroom::Submission'
6
+
7
+ end
@@ -0,0 +1,32 @@
1
+ class Mumuki::Classroom::Message < Mumuki::Classroom::Document
2
+ include Mongoid::Timestamps
3
+
4
+ field :sender, type: String
5
+ field :email, type: String
6
+ field :content, type: String
7
+ field :type, type: String
8
+ field :date, type: String
9
+
10
+ embedded_in :submission, class_name: 'Mumuki::Classroom::Submission'
11
+
12
+ def content
13
+ Mumukit::ContentType::Markdown.to_html(self[:content])
14
+ end
15
+
16
+ def sent_by?(user)
17
+ sender == user.uid
18
+ end
19
+
20
+ def with_full_messages(user)
21
+ self.tap do |message|
22
+ message[:is_me] = message.sent_by? user
23
+ end
24
+ end
25
+
26
+ def self.import_from_json!(json)
27
+ assignment = Mumuki::Classroom::Assignment.find_by!(organization: json[:organization], 'exercise.eid': json[:exercise][:bibliotheca_id], 'student.uid': json[:sender])
28
+ assignment.add_message!({content: json[:content], sender: json[:sender]}, json[:submission_id])
29
+ assignment
30
+ end
31
+
32
+ end
@@ -0,0 +1,52 @@
1
+ class Mumuki::Classroom::Notification < Mumuki::Classroom::Document
2
+ include Mongoid::Timestamps
3
+
4
+ field :organization, type: String
5
+ field :course, type: String
6
+ field :type, type: String
7
+ field :read, type: Mongoid::Boolean, default: false
8
+ field :sender, type: String
9
+
10
+ belongs_to :assignment
11
+
12
+ create_index({'organization': 1})
13
+ create_index({'organization': 1, 'read': 1})
14
+
15
+ def self.allowed(options, permissions)
16
+ where(options).select {|notification| permissions.has_permission? :teacher, notification.course}.map(&:with_assignment)
17
+ end
18
+
19
+ def self.page(organization, permissions, page, per_page)
20
+ where(organization: organization)
21
+ .sort(created_at: :desc)
22
+ .skip(per_page * (page - 1))
23
+ .limit(per_page)
24
+ .select {|notification| permissions.has_permission? :teacher, notification.course}
25
+ .map(&:with_assignment)
26
+ end
27
+
28
+ def self.unread(organization, permissions)
29
+ allowed({organization: organization, read: false}, permissions)
30
+ end
31
+
32
+ def self.import_from_json!(type, assignment)
33
+ Mumuki::Classroom::Notification.create! organization: assignment.organization,
34
+ course: assignment.course,
35
+ type: type,
36
+ sender: assignment.student[:uid],
37
+ assignment: assignment
38
+ end
39
+
40
+ def read!
41
+ update! read: true
42
+ end
43
+
44
+ def unread!
45
+ update! read: false
46
+ end
47
+
48
+ def with_assignment
49
+ as_json.except('assignment_id').merge(assignment: assignment.notification_preview, id: id.to_s)
50
+ end
51
+
52
+ end
@@ -0,0 +1,24 @@
1
+ module Reporting
2
+
3
+ def self.build_pipeline(collection, query, paginated_params, query_params, projection)
4
+ ordering = "#{Criteria.name}::#{paginated_params[:order_by].to_s.camelize}".constantize
5
+ sorting = "#{Sorting.name}::#{collection.name.demodulize}::By#{paginated_params[:sort_by].to_s.camelize}".constantize
6
+ searching = Searching.filter_for(collection, query_params)
7
+ pipeline query, sorting, ordering, searching, projection
8
+ end
9
+
10
+ def self.aggregate(collection, query, paginated_params, query_params, projection)
11
+ pipeline = build_pipeline(collection, query, paginated_params, query_params, projection)
12
+ collection.collection.aggregate pipeline
13
+ end
14
+
15
+ def self.pipeline(query, sorting, ordering, searching, projection)
16
+ main_pipeline = []
17
+ main_pipeline << {'$match': query}
18
+ main_pipeline.concat searching.pipeline
19
+ main_pipeline.concat sorting.pipeline
20
+ main_pipeline << {'$project': projection}
21
+ main_pipeline << {'$sort': sorting.order_by(ordering)}
22
+ end
23
+
24
+ end
@@ -0,0 +1,60 @@
1
+ module Searching
2
+ VALID_PARAMS = [:query_param, :query_operand]
3
+
4
+ class BaseFilter
5
+ include ActiveModel::Model
6
+
7
+ attr_accessor *VALID_PARAMS
8
+
9
+ def query
10
+ {}
11
+ end
12
+
13
+ def pipeline
14
+ []
15
+ end
16
+ end
17
+
18
+ class StudentFilter < BaseFilter
19
+ def query
20
+ {'$text': {'$search': query_param}}
21
+ end
22
+ end
23
+
24
+ class NumericFilter < BaseFilter
25
+ def query_param=(query_param)
26
+ @query_param = query_param.to_i
27
+ end
28
+ end
29
+
30
+ def self.default_filter
31
+ StudentFilter
32
+ end
33
+
34
+ def self.filter_for(collection, query_params)
35
+ filter_class = filter_class_for(query_params[:query_criteria], collection) || default_filter
36
+ filter_class.new(valid_params(query_params))
37
+ end
38
+
39
+ def self.valid_params(params)
40
+ params.select { |it| VALID_PARAMS.include? it }
41
+ end
42
+
43
+ def self.filter_class_for(criteria, collection)
44
+ if criteria.present?
45
+ "#{self}::#{collection.model_name}::#{criteria.camelize}".safe_constantize
46
+ end
47
+ end
48
+
49
+ module QueryOperands
50
+ def current_query_operand
51
+ send current_query_operand_method, query_param
52
+ end
53
+
54
+ def current_query_operand_method
55
+ query_operand || default_query_operand
56
+ end
57
+ end
58
+ end
59
+
60
+ require_relative './searching/guide_progress'