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,157 @@
1
+ class Mumuki::Classroom::App < Sinatra::Application
2
+ helpers do
3
+ def allowed_courses
4
+ {courses: Course.allowed_for(current_user).as_json}
5
+ end
6
+
7
+ def guide_progress_query
8
+ with_detached_and_search with_organization_and_course('guide.slug': repo_slug), Mumuki::Classroom::GuideProgress
9
+ end
10
+
11
+ def student_query
12
+ with_organization_and_course('guide.slug': repo_slug)
13
+ end
14
+
15
+ def student_assignment_query(student)
16
+ student_info = student.slice('first_name', 'last_name', 'email').transform_keys { |it| "student.#{it}" }
17
+ student_query.merge(student_info)
18
+ end
19
+
20
+ def guide_report_projection
21
+ {
22
+ '_id': 0,
23
+ 'last_name': '$student.last_name',
24
+ 'first_name': '$student.first_name',
25
+ 'email': '$student.email',
26
+ 'passed_count': '$stats.passed',
27
+ 'passed_with_warnings_count': '$stats.passed_with_warnings',
28
+ 'failed_count': '$stats.failed',
29
+ 'items_to_review': ''
30
+ }
31
+ end
32
+
33
+ def csv_with_headers(csv, projection)
34
+ headers = projection.symbolize_keys.except(:_id).keys.join(',')
35
+ "#{headers}\n#{csv}"
36
+ end
37
+
38
+ def add_failed_tags(report_json, exercises)
39
+ report_json.each do |student|
40
+ items_to_review = Mumuki::Classroom::Assignment.items_to_review(student_assignment_query(student), exercises)
41
+ student['items_to_review'] = items_to_review.join ', '
42
+ end
43
+ end
44
+
45
+ def normalized_exercises
46
+ json_body[:exercises].map do |it|
47
+ {language: json_body[:language]}.merge(it.symbolize_keys)
48
+ end
49
+ end
50
+
51
+ def ensure_organization_existence!
52
+ Organization.locate! organization
53
+ end
54
+
55
+ # TODO: Use JSON Builder
56
+ def with_last_invitation(course)
57
+ course.as_json(except: [:created_at, :updated_at, :id], methods: [:current_invitation]).tap do |it|
58
+ it['invitation'] = it['current_invitation']
59
+ it.except! 'current_invitation'
60
+ end
61
+ end
62
+
63
+ def guide_progress_report(matcher, projection)
64
+ projection = csv_projection_for projection
65
+ aggregation = Mumuki::Classroom::GuideProgress.where(matcher).project(projection)
66
+ pipeline_with_sort_criterion = aggregation.pipeline << {'$sort': {email: 1, passed_count: -1, passed_with_warnings_count: -1, failed_count: -1, last_name: 1, first_name: 1}}
67
+ json = Mumuki::Classroom::GuideProgress.collection.aggregate(pipeline_with_sort_criterion).as_json
68
+ content_type 'application/csv'
69
+ csv_with_headers(Mumuki::Classroom::Reports::Formats.format_report('csv', json), projection)
70
+ end
71
+
72
+ def guide_progress_report_projection
73
+ {
74
+ '_id': 0,
75
+ 'last_name': '$student.last_name',
76
+ 'first_name': '$student.first_name',
77
+ 'email': '$student.email',
78
+ 'last_submission': '$last_assignment.submission.created_at',
79
+ 'detached': {'$eq': ['$detached', true]},
80
+ 'guide_slug': '$guide.slug',
81
+ 'passed_count': '$stats.passed',
82
+ 'passed_with_warnings_count': '$stats.passed_with_warnings',
83
+ 'failed_count': '$stats.failed'
84
+ }
85
+ end
86
+ end
87
+
88
+ Mumukit::Platform.map_organization_routes!(self) do
89
+ get '/courses' do
90
+ allowed_courses
91
+ end
92
+
93
+ get '/api/courses' do
94
+ allowed_courses
95
+ end
96
+
97
+ post '/courses' do
98
+ current_user.protect! :janitor, json_body[:slug]
99
+ ensure_organization_existence!
100
+ Course.create! with_current_organization(json_body)
101
+ {status: :created}
102
+ end
103
+
104
+ get '/courses/:course' do
105
+ authorize! :teacher
106
+ {course: with_last_invitation(Course.locate!(course_slug))}
107
+ end
108
+
109
+ post '/courses/:course/invitation' do
110
+ authorize! :janitor
111
+ course = Course.locate! course_slug
112
+ {invitation: course.invite!(json_body[:expiration_date])}
113
+ end
114
+
115
+ get '/courses/:course/guides/:organization/:repository' do
116
+ authorize! :teacher
117
+ count, guide_progress = Sorting.aggregate(Mumuki::Classroom::GuideProgress, guide_progress_query, paginated_params, query_params)
118
+ {
119
+ total: count,
120
+ page: page + 1,
121
+ guide_students_progress: guide_progress
122
+ }
123
+ end
124
+
125
+ post '/courses/:course/guides/:organization/:repository/report' do
126
+ authorize! :teacher
127
+ json = Reporting.aggregate(Mumuki::Classroom::GuideProgress, guide_progress_query, paginated_params, query_params, guide_report_projection).as_json
128
+ add_failed_tags json, normalized_exercises
129
+ content_type 'application/csv'
130
+ csv_with_headers(Mumuki::Classroom::Reports::Formats.format_report('csv', json), guide_report_projection)
131
+ end
132
+
133
+ get '/courses/:course/guides/:organization/:repository/:uid' do
134
+ authorize! :teacher
135
+ {exercise_student_progress: Mumuki::Classroom::Assignment.with_full_messages(with_organization_and_course(exercise_student_progress_query), current_user)}
136
+ end
137
+
138
+ get '/courses/:course/progress' do
139
+ authorize! :admin
140
+ {exercise_student_progress: Mumuki::Classroom::Assignment.where(with_organization_and_course).as_json}
141
+ end
142
+
143
+ get '/courses/:course/report' do
144
+ authorize! :teacher
145
+ group_report with_organization_and_course, group_report_projection
146
+ end
147
+
148
+ get '/courses/:course/guide_progress_report' do
149
+ authorize! :janitor
150
+ guide_progress_report with_organization_and_course, guide_progress_report_projection
151
+ end
152
+
153
+ get '/courses/:course/guides/:organization/:repository/:uid/:exercise_id' do
154
+ Mumuki::Classroom::Assignment.find_by!(with_organization_and_course exercise_student_progress_query.merge('exercise.eid': exercise_id)).as_json
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,13 @@
1
+ class Mumuki::Classroom::App < Sinatra::Application
2
+ error Mumuki::Classroom::CourseMemberExistsError do
3
+ halt 400
4
+ end
5
+
6
+ error Mongoid::Errors::DocumentNotFound do
7
+ halt 404
8
+ end
9
+
10
+ error Mongo::Error::OperationFailure do |e|
11
+ halt 422 if e.message =~ /^E11000/
12
+ end
13
+ end
@@ -0,0 +1,71 @@
1
+ class Mumuki::Classroom::App < Sinatra::Application
2
+ helpers do
3
+ def exam_id
4
+ params[:exam_id]
5
+ end
6
+
7
+ def exam_query
8
+ {classroom_id: exam_id}
9
+ end
10
+
11
+ def exam_from_classroom_json(json)
12
+ exam = json.with_indifferent_access
13
+ Exam.adapt_json_values exam
14
+ Exam.whitelist_attributes exam
15
+ end
16
+
17
+ def exam_body
18
+ exam_from_classroom_json with_current_organization_and_course(json_body)
19
+ end
20
+
21
+ def exam_as_json_response(exam)
22
+ exam.as_json
23
+ .merge(eid: exam.classroom_id, name: exam.guide.name, language: exam.guide.language.name,
24
+ slug: exam.guide.slug, uids: exam.users.map(&:uid), course: exam.course.slug,
25
+ organization: exam.organization.name, passing_criterion: exam.passing_criterion.as_json)
26
+ .except(:classroom_id, :guide_id, :course_id, :organization_id,
27
+ :passing_criterion_type, :passing_criterion_value)
28
+ end
29
+ end
30
+
31
+ Mumukit::Platform.map_organization_routes!(self) do
32
+ get '/courses/:course/exams/:exam_id' do
33
+ authorize! :teacher
34
+ exam = Exam.find_by!(exam_query)
35
+ exam_as_json_response exam
36
+ end
37
+
38
+ put '/courses/:course/exams/:exam_id' do
39
+ authorize! :teacher
40
+ exam = Exam.find_by!(exam_query)
41
+ exam.update_attributes! exam_body
42
+ {status: :updated}.merge(eid: exam_id)
43
+ end
44
+
45
+ ['/api', ''].each do |route_prefix|
46
+ get "#{route_prefix}/courses/:course/exams" do
47
+ authorize! :teacher
48
+ {exams: Exam.where(with_current_organization_and_course).map { |it| exam_as_json_response(it) }}
49
+ end
50
+
51
+ post "#{route_prefix}/courses/:course/exams" do
52
+ authorize! :teacher
53
+ exam = Exam.create! exam_body
54
+ {status: :created}.merge(eid: exam.classroom_id)
55
+ end
56
+
57
+ post "#{route_prefix}/courses/:course/exams/:exam_id/students/:uid" do
58
+ authorize! :teacher
59
+ Exam.upsert_students!(eid: exam_id, added: [uid])
60
+ {status: :updated}.merge(eid: exam_id)
61
+ end
62
+
63
+ delete "#{route_prefix}/courses/:course/exams/:exam_id/students/:uid" do
64
+ authorize! :teacher
65
+ Exam.upsert_students!(eid: exam_id, deleted: [uid])
66
+ {status: :updated}.merge(eid: exam_id)
67
+ end
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,29 @@
1
+ class Mumuki::Classroom::App < Sinatra::Application
2
+ helpers do
3
+ def follower_query
4
+ with_organization_and_course email: current_user.uid
5
+ end
6
+ end
7
+
8
+ Mumukit::Platform.map_organization_routes!(self) do
9
+ post '/courses/:course/followers' do
10
+ authorize! :teacher
11
+ Mumuki::Classroom::Follower.find_or_create_by!(follower_query).add!(json_body[:uid])
12
+ {status: :created}
13
+ end
14
+
15
+ get '/courses/:course/followers' do
16
+ {followers: Mumuki::Classroom::Follower
17
+ .where(follower_query)
18
+ .select { |it| permissions.has_permission? :teacher, it.course }
19
+ .as_json
20
+ }
21
+ end
22
+
23
+ delete '/courses/:course/followers/:uid' do
24
+ authorize! :teacher
25
+ Mumuki::Classroom::Follower.find_by!(follower_query).remove!(params[:uid])
26
+ {status: :created}
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,79 @@
1
+ class Mumuki::Classroom::App < Sinatra::Application
2
+ Mumukit::Platform.map_organization_routes!(self) do
3
+ get '/guides/:organization/:repository' do
4
+ authorize! :teacher
5
+ guide = Guide.locate!(repo_slug)
6
+ validate_usage! guide
7
+ {guide: guide_needed_fields(guide)}
8
+ end
9
+
10
+ get '/courses/:course/guides' do
11
+ get_current_guides
12
+ end
13
+
14
+ get '/api/courses/:course/guides' do
15
+ get_current_guides
16
+ end
17
+ end
18
+
19
+ helpers do
20
+ # TODO: Use JSON Builder
21
+ def get_current_guides
22
+ authorize! :teacher
23
+ ensure_organization_exists!
24
+ ensure_course_exists!
25
+ {
26
+ chapters: chapter_needed_fields(current_organization.book.chapters.includes(topic: {lessons: {guide: :language}})),
27
+ complements: guide_container_needed_fields(current_organization.book.complements.includes(guide: :language)),
28
+ exams: guide_container_needed_fields(current_organization.exams.where(course: current_course).includes(guide: :language))
29
+ }
30
+ end
31
+
32
+ def except_fields
33
+ [:id, :created_at, :updated_at, :language_id, :guide_id, :topic_id, :book_id]
34
+ end
35
+
36
+ def guide_as_json_opts
37
+ {except: except_fields, include: {language: {only: [:name, :devicon]}}}
38
+ end
39
+
40
+ def guide_container_as_json_opts
41
+ {except: except_fields, include: {guide: guide_as_json_opts}}
42
+ end
43
+
44
+ def chapter_as_json_opts
45
+ {except: except_fields, include: {lessons: guide_container_as_json_opts}, methods: :name}
46
+ end
47
+
48
+ def guide_needed_fields(guide)
49
+ guide.as_json guide_as_json_opts
50
+ end
51
+
52
+ def with_guide_progress_count(containers)
53
+ containers.each do |container|
54
+ container.tap do |it|
55
+ it['guide']['students_count'] = Mumuki::Classroom::GuideProgress
56
+ .where(with_organization_and_course 'guide.slug': it['guide']['slug'])
57
+ .count
58
+ end
59
+ end
60
+ end
61
+
62
+ def guide_container_needed_fields(containers)
63
+ with_guide_progress_count containers.as_json(guide_container_as_json_opts)
64
+ end
65
+
66
+ def chapter_needed_fields(chapters)
67
+ chapters.as_json(chapter_as_json_opts).tap do |chs|
68
+ chs.each do |chapter|
69
+ with_guide_progress_count(chapter['lessons'])
70
+ end
71
+ end
72
+ end
73
+
74
+ # TODO: Extract to domain
75
+ def validate_usage!(guide)
76
+ raise ActiveRecord::RecordNotFound, "Couldn't find #{Guide.name} with #{Guide.sync_key_id_field}: #{guide.slug}" unless guide.usage_in_organization
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,16 @@
1
+ class Mumuki::Classroom::App < Sinatra::Application
2
+ helpers do
3
+ def manual_evaluation_assignment_query
4
+ with_organization_and_course 'exercise.eid': params[:exercise_id].to_i, 'student.uid': params[:uid], 'guide.slug': repo_slug
5
+ end
6
+ end
7
+
8
+ Mumukit::Platform.map_organization_routes!(self) do
9
+ post '/courses/:course/guides/:organization/:repository/:exercise_id/student/:uid/manual_evaluation' do
10
+ assignment = Mumuki::Classroom::Assignment.find_by!(manual_evaluation_assignment_query)
11
+ assignment.evaluate_manually!(json_body[:sid], json_body[:comment], json_body[:status])
12
+ assignment.notify_manual_evaluation!(json_body[:sid])
13
+ end
14
+ end
15
+ end
16
+
@@ -0,0 +1,206 @@
1
+ class Mumuki::Classroom::App < Sinatra::Application
2
+
3
+ Mumukit::Platform.map_organization_routes!(self) do
4
+
5
+ namespace '/api/courses/:course/massive' do
6
+
7
+ before do
8
+ next if request.options?
9
+ authorize! :janitor
10
+ ensure_organization_exists!
11
+ ensure_course_exists!
12
+ end
13
+
14
+ get '/students' do
15
+ per_page = MASSIVE_BATCH_LIMIT
16
+ progress = guide_progress_at_page per_page
17
+ count = progress.count
18
+ guide_progress = progress.select(&:student).map { |it| as_guide_progress_response it }
19
+ {
20
+ page: page + 1,
21
+ total_pages: (count / per_page.to_f).ceil,
22
+ total_results: count,
23
+ total_page_results: [per_page, guide_progress.size].min,
24
+ guide_students_progress: guide_progress
25
+ }
26
+ end
27
+
28
+ post '/students' do
29
+ create_members! :student do |user|
30
+ Mumukit::Nuntius.notify! 'resubmissions', uid: user.uid, tenant: tenant
31
+ end
32
+ end
33
+
34
+ post '/teachers' do
35
+ create_members! :teacher
36
+ end
37
+
38
+ post '/students/detach' do
39
+ update_students! do |processed|
40
+ update_students_at_course! :detach, :remove, processed
41
+ end
42
+ end
43
+
44
+ post '/students/attach' do
45
+ update_students! do |processed|
46
+ update_students_at_course! :attach, :add, processed
47
+ end
48
+ end
49
+
50
+ post '/exams/:exam_id/students' do
51
+ update_students! do |processed|
52
+ Exam.upsert_students! eid: exam_id, added: processed
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ MASSIVE_BATCH_LIMIT = 100
59
+
60
+ helpers do
61
+ def with_massive_batch_limit(elements)
62
+ elements.to_a.take MASSIVE_BATCH_LIMIT
63
+ end
64
+
65
+ def uids
66
+ json_body[:uids]
67
+ end
68
+
69
+ def massive_uids
70
+ with_massive_batch_limit uids
71
+ end
72
+
73
+ def students
74
+ @students ||= json_body[:students].map do |it|
75
+ Mumuki::Classroom::Student.normalized_attributes_from_json it
76
+ end
77
+ end
78
+
79
+ def teachers
80
+ @teachers ||= json_body[:teachers].map do |it|
81
+ Mumuki::Classroom::Teacher.normalized_attributes_from_json it
82
+ end
83
+ end
84
+
85
+ def massive_students
86
+ @massive_students ||= with_massive_batch_limit students
87
+ end
88
+
89
+ def massive_teachers
90
+ @massive_teachers ||= with_massive_batch_limit teachers
91
+ end
92
+
93
+ def user_from_member_json(member_json)
94
+ User.whitelist_attributes member_json
95
+ end
96
+
97
+ def upsert_user!(role, member)
98
+ user = User.find_or_initialize_by(uid: member[:uid])
99
+ user.assign_attributes user_from_member_json(member)
100
+ user.add_permission! role, course_slug
101
+ user.verify_name!
102
+ yield user if block_given?
103
+ end
104
+
105
+ #FIXME: This method now doesn't perform a bulk update as PG doesn't support it
106
+ def upsert_users!(role, members, &block)
107
+ members.each { |it| upsert_user! role, it, &block }
108
+ end
109
+
110
+ def massive_response(processed, unprocessed, errored, errored_msg, hash = {})
111
+ add_massive_response_field(:errored_members, errored, errored_msg, hash)
112
+ add_massive_response_field(:unprocessed, unprocessed, unprocessed_msg, hash)
113
+ hash.merge(processed_count: processed.size, processed: processed)
114
+ end
115
+
116
+ def unprocessed_msg
117
+ "This endpoint process only first #{MASSIVE_BATCH_LIMIT} elements"
118
+ end
119
+
120
+ def students_does_not_belong_msg
121
+ 'Students does not belong to current course'
122
+ end
123
+
124
+ def add_massive_response_field(field, list, message, hash)
125
+ unless list.empty?
126
+ hash["#{field}_reason".to_sym] = message
127
+ hash["#{field}_count".to_sym] = list.size
128
+ hash[field.to_sym] = list
129
+ end
130
+ end
131
+
132
+ def members_for(role)
133
+ send role.to_s.pluralize
134
+ end
135
+
136
+ def massive_members_for(role)
137
+ send "massive_#{role.to_s.pluralize}"
138
+ end
139
+
140
+ def unprocessed_members_for(role)
141
+ members_for(role) - massive_members_for(role)
142
+ end
143
+
144
+ def create_members!(role, &block)
145
+ members_collection = collection_for role
146
+ massive_members = massive_members_for role
147
+ existing_members = existing_members_in_course(members_collection, massive_members)
148
+ existing_members_uids = existing_members.map { |it| it[:uid] }
149
+ processed_members = massive_members.reject { |it| existing_members_uids.include? it[:uid] }.uniq { |it| it[:uid]}
150
+ members_collection.collection.insert_many(processed_members.map { |member| with_organization_and_course member })
151
+ upsert_users! role, processed_members, &block
152
+ massive_response(processed_members, unprocessed_members_for(role), existing_members,
153
+ "#{role.to_s.pluralize.titleize} already belong to current course", status: :created)
154
+ end
155
+
156
+ def existing_members_in_course(col, massive_members)
157
+ col.where(with_organization_and_course)
158
+ .in(uid: massive_members.map { |it| it[:uid] })
159
+ .map { |it| col.normalized_attributes_from_json(it) }
160
+ end
161
+
162
+ def update_students!
163
+ processed = students_from(massive_uids).map(&:uid)
164
+ yield processed if block_given?
165
+ massive_response processed, (uids - massive_uids), (massive_uids - processed),
166
+ students_does_not_belong_msg, status: :updated
167
+ end
168
+
169
+ def update_students_at_course!(method, action, students_uids)
170
+ Mumuki::Classroom::Student.send "#{method}_all_by!", students_uids, with_organization_and_course
171
+ User.where(uid: students_uids).each do |user|
172
+ user.send "#{action}_permission!", :student, course_slug
173
+ user.save!
174
+ end
175
+ end
176
+
177
+ def students_from(uids)
178
+ Mumuki::Classroom::Student.where(with_organization_and_course).in(uid: uids)
179
+ end
180
+
181
+ def ensure_course_exists!
182
+ Course.locate!(course_slug)
183
+ end
184
+
185
+ def ensure_organization_exists!
186
+ Organization.locate!(organization).tap &:switch!
187
+ end
188
+
189
+ def guide_progress_at_page(per_page)
190
+ Mumuki::Classroom::GuideProgress
191
+ .where(with_organization_and_course)
192
+ .sort('organization': :asc, 'course': :asc, 'student.uid': :asc)
193
+ .limit(per_page)
194
+ .skip(page * per_page)
195
+ end
196
+
197
+ def as_guide_progress_response(guide_progress)
198
+ {
199
+ student: guide_progress.student.uid,
200
+ guide: guide_progress.slug,
201
+ progress: guide_progress.as_json.except(:student, :guide)
202
+ }
203
+ end
204
+ end
205
+ end
206
+