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,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
|
+
|