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,52 @@
1
+ class Mumuki::Classroom::App < Sinatra::Application
2
+ helpers do
3
+ def message
4
+ message = json_body[:message]
5
+ message[:sender] = current_user.uid
6
+ message
7
+ end
8
+
9
+ def render_threads(course)
10
+ authorize! :student
11
+ query = with_organization exercise_student_progress_query.merge(course: course, 'exercise.eid': exercise_id)
12
+ threads = Mumuki::Classroom::Assignment.find_by!(query).threads(params[:language])
13
+ erb :'threads.html', locals: {threads: threads, user: current_user}
14
+ end
15
+
16
+ def assignment_query
17
+ with_organization_and_course 'exercise.eid': json_body[:exercise_id], 'student.uid': json_body[:uid], 'guide.slug': json_body[:guide_slug]
18
+ end
19
+
20
+ def find_or_create_suggestion(assignment)
21
+ suggestion_id ? Mumuki::Classroom::Suggestion.find(suggestion_id) : Mumuki::Classroom::Suggestion.create_from(message, assignment)
22
+ end
23
+
24
+ def submission_id
25
+ json_body[:submission_id]
26
+ end
27
+
28
+ def suggestion_id
29
+ json_body[:suggestion_id]
30
+ end
31
+ end
32
+
33
+ Mumukit::Platform.map_organization_routes!(self) do
34
+ post '/courses/:course/messages' do
35
+ authorize! :teacher
36
+ assignment = Mumuki::Classroom::Assignment.find_by!(assignment_query)
37
+ submission = assignment.add_message_to_submission!(message, submission_id)
38
+ find_or_create_suggestion(assignment).add_submission!(submission)
39
+
40
+ {status: :created, message: Mumuki::Classroom::Message.new(message)}
41
+ end
42
+
43
+ get '/courses/:course/guides/:organization/:repository/:exercise_id/student/:uid/messages' do
44
+ render_threads(course_slug)
45
+ end
46
+
47
+ get '/api/guides/:organization/:repository/:exercise_id/student/:uid/messages' do
48
+ render_threads Mumuki::Classroom::Student.last_updated_student_by(with_organization uid: uid).course
49
+ end
50
+ end
51
+ end
52
+
@@ -0,0 +1,29 @@
1
+ class Mumuki::Classroom::App < Sinatra::Application
2
+ Mumukit::Platform.map_organization_routes!(self) do
3
+ get '/notifications/unread' do
4
+ authorize! :teacher
5
+ { notifications: Mumuki::Classroom::Notification.unread(organization, current_user.permissions) }
6
+ end
7
+
8
+ get '/notifications' do
9
+ authorize! :teacher
10
+ page = params[:page].to_i
11
+ per_page = params[:per_page].to_i
12
+ {total: Mumuki::Classroom::Notification.count,
13
+ page: page,
14
+ notifications: Mumuki::Classroom::Notification.page(organization, current_user.permissions, page, per_page)}
15
+ end
16
+
17
+ put '/notifications/:notificationId/read' do
18
+ authorize! :teacher
19
+ Mumuki::Classroom::Notification.find(params[:notificationId]).read!
20
+ {status: :updated}
21
+ end
22
+
23
+ put '/notifications/:notificationId/unread' do
24
+ authorize! :teacher
25
+ Mumuki::Classroom::Notification.find(params[:notificationId]).unread!
26
+ {status: :updated}
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,7 @@
1
+ class Mumuki::Classroom::App < Sinatra::Application
2
+ Mumukit::Platform.map_organization_routes!(self) do
3
+ get '/organization' do
4
+ organization_json
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ class Mumuki::Classroom::App < Sinatra::Application
2
+ helpers do
3
+ def paginated_params
4
+ {
5
+ page: page,
6
+ sort_by: sort_by,
7
+ order_by: order_by,
8
+ per_page: per_page,
9
+ with_detached: with_detached
10
+ }
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ class Mumuki::Classroom::App < Sinatra::Application
2
+ Mumukit::Platform.map_organization_routes!(self) do
3
+ get '/permissions' do
4
+ authorize! :teacher
5
+
6
+ {permissions: permissions}
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ class Mumuki::Classroom::App < Sinatra::Application
2
+ Mumukit::Platform.map_organization_routes!(self) do
3
+ get '/ping' do
4
+ {message: 'pong!', organization: tenant}
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,27 @@
1
+ class Mumuki::Classroom::App < Sinatra::Application
2
+ helpers do
3
+ def with_detached_and_search(params_hash, collection)
4
+ params_hash
5
+ .merge('detached': {'$exists': with_detached})
6
+ .merge_if(params[:students] == 'follow', followers_criteria(collection))
7
+ .merge_unless(query_params[:query_param].empty?, query_criteria_class_for(collection).query)
8
+ end
9
+
10
+ def query_criteria_class_for(collection)
11
+ Searching.filter_for(collection, query_params)
12
+ end
13
+
14
+ def query_params
15
+ {
16
+ query_param: query,
17
+ query_criteria: query_criteria,
18
+ query_operand: query_operand
19
+ }
20
+ end
21
+
22
+ def followers_criteria(collection)
23
+ uids = Mumuki::Classroom::Follower.find_by(with_organization_and_course email: current_user_uid)&.uids.to_a
24
+ {collection.uid_field.to_sym => {'$in': uids}}
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,111 @@
1
+ class Mumuki::Classroom::App < Sinatra::Application
2
+ helpers do
3
+ def normalize_student!
4
+ json_body[:email] = json_body[:email]&.downcase
5
+ json_body[:last_name] = json_body[:last_name]&.downcase&.titleize
6
+ json_body[:first_name] = json_body[:first_name]&.downcase&.titleize
7
+ end
8
+
9
+ def list_students(matcher)
10
+ authorize! :teacher
11
+ count, students = Sorting.aggregate(Mumuki::Classroom::Student, with_detached_and_search(matcher, Mumuki::Classroom::Student), paginated_params, query_params)
12
+ {page: page + 1, total: count, students: students}
13
+ end
14
+
15
+ end
16
+
17
+ Mumukit::Platform.map_organization_routes!(self) do
18
+
19
+ get '/courses/:course/students' do
20
+ list_students with_organization_and_course
21
+ end
22
+
23
+ get '/api/courses/:course/students' do
24
+ authorize! :teacher
25
+ query_params = params.slice('uid', 'personal_id')
26
+ {students: Mumuki::Classroom::Student.where(with_organization_and_course.merge query_params)}
27
+ end
28
+
29
+ get '/students' do
30
+ list_students with_organization
31
+ end
32
+
33
+ get '/students/report' do
34
+ authorize! :janitor
35
+ group_report with_organization, group_report_projection.merge(course: '$course')
36
+ end
37
+
38
+ # Retrieves the progress for a student in an specific course
39
+ get '/api/courses/:course/students/:uid' do
40
+ authorize! :teacher
41
+ {guide_students_progress: Mumuki::Classroom::GuideProgress.where(with_organization_and_course 'student.uid': uid).sort(created_at: :asc).as_json}
42
+ end
43
+
44
+ # Tries to resubmit all failed_submissions of a student to a specific tenant
45
+ post '/courses/:course/students/:uid' do
46
+ authorize! :janitor
47
+ Mumukit::Nuntius.notify! 'resubmissions', uid: uid, tenant: tenant
48
+ {status: :created}
49
+ end
50
+
51
+ # Detaches a student of a course
52
+ post '/courses/:course/students/:uid/detach' do
53
+ authorize! :janitor
54
+ Mumuki::Classroom::Student.find_by!(with_organization_and_course uid: uid).detach!
55
+ update_user_permissions!(uid, 'remove', course_slug)
56
+ {status: :updated}
57
+ end
58
+
59
+ # Attaches a student to a course
60
+ post '/courses/:course/students/:uid/attach' do
61
+ authorize! :janitor
62
+ Mumuki::Classroom::Student.find_by!(with_organization_and_course uid: uid).attach!
63
+ update_user_permissions!(uid, 'add', course_slug)
64
+ {status: :updated}
65
+ end
66
+
67
+ # Transfers a student to another course
68
+ post '/courses/:course/students/:uid/transfer' do
69
+ authorize! :admin
70
+
71
+ destination = Mumukit::Auth::Slug.join organization, json_body[:destination]
72
+
73
+ Mumuki::Classroom::Student.find_by!(with_organization_and_course uid: uid).transfer_to! organization, destination.to_s
74
+
75
+ update_user_permissions!(uid, 'update', course_slug, destination.to_s)
76
+ {status: :updated}
77
+ end
78
+
79
+ # Retrieves info for a particular student
80
+ get '/courses/:course/student/:uid' do
81
+ authorize! :teacher
82
+
83
+ Mumuki::Classroom::Student.find_by!(with_organization_and_course uid: uid).as_json
84
+ end
85
+
86
+ # Creates student and tries to resubmit all failed submissions to that student
87
+ post '/courses/:course/students' do
88
+ authorize! :janitor
89
+ ensure_course_existence!
90
+
91
+ student_json = create_course_member! :student
92
+
93
+ Mumukit::Nuntius.notify! 'resubmissions', uid: student_json[:uid], tenant: tenant
94
+
95
+ {status: :created}
96
+ end
97
+
98
+ # Updates student information
99
+ put '/courses/:course/students/:uid' do
100
+ authorize! :janitor
101
+ ensure_course_existence!
102
+
103
+ student = Mumuki::Classroom::Student.find_by!(with_organization_and_course uid: uid)
104
+ student.update! Mumuki::Classroom::Student.normalized_attributes_from_json(json_body).except(:uid)
105
+
106
+ upsert_user! :student, student.as_user
107
+
108
+ {status: :updated}
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,17 @@
1
+ class Mumuki::Classroom::App < Sinatra::Application
2
+ helpers do
3
+ def exercise_id
4
+ params[:exercise_id].to_i
5
+ end
6
+ end
7
+
8
+ Mumukit::Platform.map_organization_routes!(self) do
9
+ get '/suggestions/:organization/:repository/:exercise_id' do
10
+ authorize! :teacher
11
+ { suggestions: Mumuki::Classroom::Suggestion
12
+ .where(guide_slug: repo_slug, 'exercise.eid': exercise_id)
13
+ .sort(updated_at: :desc)
14
+ .map { |s| s.as_json(methods: :content_html).merge(id: s.id) } }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,14 @@
1
+ class Mumuki::Classroom::App < Sinatra::Application
2
+ Mumukit::Platform.map_organization_routes!(self) do
3
+ get '/courses/:course/teachers' do
4
+ authorize! :headmaster
5
+ {teachers: Mumuki::Classroom::Teacher.where(with_organization_and_course).as_json}
6
+ end
7
+
8
+ post '/courses/:course/teachers' do
9
+ authorize! :headmaster
10
+
11
+ create_course_member! :teacher
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ module Mumuki
2
+ module Classroom
3
+ VERSION = '8.0.0'
4
+ end
5
+ end
@@ -0,0 +1,17 @@
1
+ class Mumukit::Auth::Profile
2
+ attr_accessor :attributes
3
+
4
+ FIELDS = [:uid, :social_id, :email, :name, :first_name, :last_name, :image_url]
5
+
6
+ def initialize(attributes)
7
+ @attributes = attributes
8
+ end
9
+
10
+ def self.extract(profile_like)
11
+ new profile_like.as_json(only: FIELDS).with_indifferent_access
12
+ end
13
+
14
+ def ==(other)
15
+ other.class == self.class && other.attributes == self.attributes
16
+ end
17
+ end
@@ -0,0 +1,43 @@
1
+ <div class="mu-messages">
2
+ <ol class="mu-chat">
3
+ <% threads.each_with_index do |thread, index| %>
4
+ <div class="thread">
5
+ <li class="solution self">
6
+ <a onclick="toggleCollapse('toggle-<%= index %>')">
7
+ <i class="fa fa-<%= threads.last == thread ? 'minus' : 'plus' %>-square-o toggle-<%= index %>"></i>
8
+ <span>&nbsp;</span>
9
+ <span> <%= I18n.t(:view_solution) %> </span>
10
+ </a>
11
+ <div id="toggle-<%= index %>" class="<%= thread[:status] %> message visible">
12
+ <%= thread[:content] %>
13
+ <time style="font-weight: bold;"><%= thread[:created_at] %></time>
14
+ </div>
15
+ </li>
16
+ <% thread[:messages].each do |message| %>
17
+ <li class="<%= message.sent_by?(user) ? 'self' : 'other' %>">
18
+ <div class="message">
19
+ <p> <%= message.content %></p>
20
+ <div class="sender"><%= message.sender %></div>
21
+ <time><%= message.created_at %></time>
22
+ </div>
23
+ </li>
24
+ <% end %>
25
+ </div>
26
+ <% end %>
27
+ </ol>
28
+ </div>
29
+
30
+
31
+ <script>
32
+ function toggleCollapse(target) {
33
+ var $target = $('#' + target);
34
+ var $icon = $('i.' + target);
35
+ $icon.toggleClass('fa-minus-square-o');
36
+ $icon.toggleClass('fa-plus-square-o');
37
+ $target.toggleClass('hidden');
38
+ }
39
+
40
+ $('.mu-view-messages time').each(function (i, e) {
41
+ return e.innerText = moment(e.innerText).fromNow();
42
+ });
43
+ </script>
@@ -0,0 +1,20 @@
1
+ namespace :classroom do
2
+ namespace :messages do
3
+ task listen: :environment do
4
+ Mumukit::Nuntius::Logger.info 'Listening to student messages'
5
+
6
+ Mumukit::Nuntius::Consumer.negligent_start! 'student-messages' do |body|
7
+ begin
8
+ Mumukit::Nuntius::Logger.info "Processing message #{body}"
9
+
10
+ Mumuki::Classroom::Message.import_from_json!(body).try do |assignment|
11
+ Mumuki::Classroom::Notification.import_from_json! 'Mumuki::Classroom::Message', assignment
12
+ end
13
+
14
+ rescue => e
15
+ Mumukit::Nuntius::Logger.warn "Mumuki::Classroom::Message failed #{e}. body was: #{body}"
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ namespace :classroom do
2
+ namespace :resubmissions do
3
+ task listen: :environment do
4
+ Mumukit::Nuntius::Logger.info 'Listening to resubmissions'
5
+
6
+ Mumukit::Nuntius::Consumer.negligent_start! 'resubmissions' do |body|
7
+ destination = body['tenant']
8
+ uid = body['uid']
9
+
10
+ Mumukit::Nuntius::Logger.info "Processing resubmission #{uid}"
11
+ Mumuki::Classroom::FailedSubmission.reprocess! uid, destination
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,31 @@
1
+ namespace :classroom do
2
+ namespace :students do
3
+ namespace :reports do
4
+ task :registered, [:organization, :course, :from, :to, :format] => [:environment] do |_t, args|
5
+ args.with_defaults(format: 'table')
6
+
7
+ from = Date.parse(args[:from])
8
+ to = args[:to].try { |it| Date.parse(it) } || 1.day.since
9
+ format = args[:format]
10
+
11
+ stats = Mumuki::Classroom::Student.report(organzation: args[:organization], course: args[:course]).select do |user|
12
+ Date.parse(user[:created_at]) >= from && Date.parse(user[:created_at]) < to
13
+ end
14
+ puts Mumuki::Classroom::Reports::Formats.format_report(format, stats)
15
+ end
16
+
17
+ task :active, [:organization, :course, :from, :to, :format] => [:environment] do |_t, args|
18
+ args.with_defaults(format: 'table')
19
+
20
+ from = Date.parse(args[:from])
21
+ to = args[:to].try { |it| Date.parse(it) } || 1.day.since
22
+ format = args[:format]
23
+
24
+ stats = Mumuki::Classroom::Student.report(organization: args[:organization], course: args[:course]).select do |user|
25
+ (user[:detached_at].blank? || Date.parse(user[:detached_at]) >= from) && Date.parse(user[:created_at]) < to
26
+ end
27
+ puts Mumuki::Classroom::Reports::Formats.format_report(format, stats)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,17 @@
1
+ namespace :classroom do
2
+ namespace :submissions do
3
+ task listen: :environment do
4
+ Mumukit::Nuntius::Logger.info 'Listening to submissions'
5
+
6
+ Mumukit::Nuntius::Consumer.negligent_start! 'submissions' do |body|
7
+ begin
8
+ Mumukit::Nuntius::Logger.info "Processing submission #{body['uid']}"
9
+ Mumuki::Classroom::Submission.process! body
10
+ rescue => e
11
+ Mumukit::Nuntius::Logger.warn "Mumuki::Classroom::Submission failed #{e}. body was: #{body.except('test_results')}"
12
+ Mumuki::Classroom::FailedSubmission.create! body
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end