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