Skip to content

Commit b60423a

Browse files
feat: add scholaistic integration
1 parent 6e333e2 commit b60423a

File tree

81 files changed

+2305
-7
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

81 files changed

+2305
-7
lines changed

app/controllers/components/course/scholaistic_component.rb

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,60 @@ def self.enabled_by_default?
1212
end
1313

1414
def sidebar_items
15-
settings_sidebar_items
15+
main_sidebar_items + settings_sidebar_items
1616
end
1717

1818
private
1919

20+
def main_sidebar_items
21+
return [] unless scholaistic_course_linked?
22+
23+
student_sidebar_items + admin_sidebar_items
24+
end
25+
26+
def student_sidebar_items
27+
[
28+
{
29+
key: :scholaistic_assessments,
30+
icon: :chatbot,
31+
title: settings.assessments_title || I18n.t('course.scholaistic.assessments'),
32+
weight: 4,
33+
path: course_scholaistic_assessments_path(current_course)
34+
}
35+
] + assistant_sidebar_items
36+
end
37+
38+
def assistant_sidebar_items
39+
ScholaisticApiService.assistants!(current_course).map do |assistant|
40+
{
41+
key: "scholaistic_assistant_#{assistant[:id]}",
42+
icon: :chatbot,
43+
title: assistant[:sidebar_title] || assistant[:title],
44+
weight: 4.5,
45+
path: course_scholaistic_assistant_path(current_course, assistant[:id])
46+
}
47+
end
48+
rescue StandardError => e
49+
Rails.logger.error("Failed to load Scholaistic assistants: #{e.message}")
50+
raise e unless Rails.env.production?
51+
52+
[]
53+
end
54+
55+
def admin_sidebar_items
56+
[
57+
{
58+
key: :scholaistic_assistants,
59+
type: :admin,
60+
icon: :chatbot,
61+
title: I18n.t('components.scholaistic.manage_assistants'),
62+
weight: 9,
63+
path: course_scholaistic_assistants_path(current_course),
64+
exact: true
65+
}
66+
]
67+
end
68+
2069
def settings_sidebar_items
2170
[
2271
{
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# frozen_string_literal: true
2+
class Course::Achievement::Condition::ScholaisticAssessmentsController <
3+
Course::Condition::ScholaisticAssessmentsController
4+
include Course::AchievementConditionalConcern
5+
end

app/controllers/course/admin/scholaistic_settings_controller.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def confirm_link_course
1515
key = ScholaisticApiService.parse_link_course_callback_request(request, params)
1616
head :bad_request and return if key.blank?
1717

18-
@settings.update(integration_key: key) && current_course.save
18+
@settings.update(integration_key: key, last_synced_at: nil) && current_course.save
1919
end
2020

2121
def link_course
@@ -35,7 +35,7 @@ def unlink_course
3535

3636
ScholaisticApiService.unlink_course!(@settings.integration_key)
3737

38-
update_settings_and_render(integration_key: nil)
38+
update_settings_and_render(integration_key: nil, last_synced_at: nil)
3939
end
4040

4141
protected
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# frozen_string_literal: true
2+
class Course::Assessment::Condition::ScholaisticAssessmentsController <
3+
Course::Condition::ScholaisticAssessmentsController
4+
include Course::AssessmentConditionalConcern
5+
end
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# frozen_string_literal: true
2+
class Course::Condition::ScholaisticAssessmentsController < Course::ConditionsController
3+
load_resource :scholaistic_assessment_condition, class: Course::Condition::ScholaisticAssessment.name, parent: false
4+
before_action :set_course_and_conditional, only: [:create]
5+
authorize_resource :scholaistic_assessment_condition, class: Course::Condition::ScholaisticAssessment.name
6+
7+
def index
8+
render_available_scholaistic_assessments
9+
end
10+
11+
def show
12+
render_available_scholaistic_assessments
13+
end
14+
15+
def create
16+
try_to_perform @scholaistic_assessment_condition.save
17+
end
18+
19+
def update
20+
try_to_perform @scholaistic_assessment_condition.update(scholaistic_assessment_condition_params)
21+
end
22+
23+
def destroy
24+
try_to_perform @scholaistic_assessment_condition.destroy
25+
end
26+
27+
private
28+
29+
def render_available_scholaistic_assessments
30+
scholaistic_assessments = current_course.scholaistic_assessments
31+
existing_conditions = @conditional.specific_conditions - [@scholaistic_assessment_condition]
32+
@available_assessments = (scholaistic_assessments - existing_conditions.map(&:dependent_object)).sort_by(&:title)
33+
render 'available_scholaistic_assessments'
34+
end
35+
36+
def try_to_perform(operation_succeeded)
37+
if operation_succeeded
38+
success_action
39+
else
40+
render json: { errors: @scholaistic_assessment_condition.errors }, status: :bad_request
41+
end
42+
end
43+
44+
def scholaistic_assessment_condition_params
45+
params.require(:condition_scholaistic_assessment).permit(:scholaistic_assessment_id)
46+
end
47+
48+
def set_course_and_conditional
49+
@scholaistic_assessment_condition.course = current_course
50+
@scholaistic_assessment_condition.conditional = @conditional
51+
end
52+
53+
def component
54+
current_component_host[:course_scholaistic_component]
55+
end
56+
end
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# frozen_string_literal: true
2+
class Course::Scholaistic::AssistantsController < Course::Scholaistic::Controller
3+
def index
4+
authorize! :manage_scholaistic_assistants, current_course
5+
6+
@embed_src = ScholaisticApiService.embed!(
7+
current_course_user,
8+
ScholaisticApiService.assistants_path,
9+
request.origin
10+
)
11+
end
12+
13+
def show
14+
authorize! :read_scholaistic_assistants, current_course
15+
16+
@assistant_title = ScholaisticApiService.assistant!(current_course, params[:id])[:title]
17+
18+
@embed_src = ScholaisticApiService.embed!(
19+
current_course_user,
20+
ScholaisticApiService.assistant_path(params[:id]),
21+
request.origin
22+
)
23+
end
24+
end
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# frozen_string_literal: true
2+
class Course::Scholaistic::Controller < Course::ComponentController
3+
include Course::Scholaistic::Concern
4+
5+
before_action :not_found_if_scholaistic_course_not_linked
6+
7+
private
8+
9+
def component
10+
current_component_host[:course_scholaistic_component]
11+
end
12+
13+
def not_found_if_scholaistic_course_not_linked
14+
head :not_found unless scholaistic_course_linked?
15+
end
16+
end
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# frozen_string_literal: true
2+
class Course::Scholaistic::ScholaisticAssessmentsController < Course::Scholaistic::Controller
3+
load_and_authorize_resource :scholaistic_assessment, through: :course, class: Course::ScholaisticAssessment.name
4+
5+
before_action :sync_scholaistic_assessments!, only: [:index, :show, :edit]
6+
7+
def index
8+
submissions_status_hash = ScholaisticApiService.submissions!(
9+
@scholaistic_assessments.map(&:upstream_id),
10+
current_course_user
11+
)
12+
13+
@assessments_status = @scholaistic_assessments.to_h do |assessment|
14+
[assessment.id,
15+
submissions_status_hash[assessment.upstream_id]&.[](:status) ||
16+
(condition_satisfied?(assessment) ? 'unavailable' : 'open')]
17+
end
18+
end
19+
20+
def new
21+
@embed_src = ScholaisticApiService.embed!(
22+
current_course_user,
23+
ScholaisticApiService.new_assessment_path,
24+
request.origin
25+
)
26+
end
27+
28+
def show
29+
upstream_id = @scholaistic_assessment.upstream_id
30+
31+
@embed_src =
32+
if can?(:update, @scholaistic_assessment)
33+
ScholaisticApiService.embed!(
34+
current_course_user,
35+
ScholaisticApiService.edit_assessment_path(upstream_id),
36+
request.origin
37+
)
38+
else
39+
ScholaisticApiService.embed!(
40+
current_course_user,
41+
ScholaisticApiService.assessment_path(upstream_id),
42+
request.origin
43+
)
44+
end
45+
end
46+
47+
def edit
48+
@embed_src = ScholaisticApiService.embed!(
49+
current_course_user,
50+
ScholaisticApiService.edit_assessment_details_path(@scholaistic_assessment.upstream_id),
51+
request.origin
52+
)
53+
end
54+
55+
def update
56+
if @scholaistic_assessment.update(update_params)
57+
head :ok
58+
else
59+
render json: { errors: @scholaistic_assessment.errors.full_messages.to_sentence }, status: :bad_request
60+
end
61+
end
62+
63+
private
64+
65+
def update_params
66+
params.require(:scholaistic_assessment).permit(:base_exp)
67+
end
68+
69+
def condition_satisfied?(assessment)
70+
(can?(:attempt, assessment) || assessment.conditions_satisfied_by?(current_course_user)) &&
71+
(assessment.start_at <= Time.zone.now)
72+
end
73+
74+
def sync_scholaistic_assessments!
75+
response = ScholaisticApiService.assessments!(current_course)
76+
77+
# TODO: The SQL queries will scale proportionally with `response[:assessments].size`,
78+
# but we won't always have to sync all assessments since there's `last_synced_at`.
79+
# In the future, we can optimise this, but it's not easy because there are multiple
80+
# relations to `Course::ScholaisticAssessment` that need to be updated.
81+
ActiveRecord::Base.transaction do
82+
response[:assessments].map do |assessment|
83+
current_course.scholaistic_assessments.find_or_initialize_by(
84+
upstream_id: assessment[:upstream_id]
85+
).tap do |scholaistic_assessment|
86+
scholaistic_assessment.start_at = assessment[:start_at]
87+
scholaistic_assessment.end_at = assessment[:end_at]
88+
scholaistic_assessment.title = assessment[:title]
89+
scholaistic_assessment.description = assessment[:description]
90+
scholaistic_assessment.published = assessment[:published]
91+
end.save!
92+
end
93+
94+
if response[:deleted].present? && !current_course.scholaistic_assessments.
95+
where(upstream_id: response[:deleted]).destroy_all
96+
raise ActiveRecord::Rollback
97+
end
98+
99+
current_course.settings(:course_scholaistic_component).public_send('last_synced_at=', response[:last_synced_at])
100+
101+
current_course.save!
102+
end
103+
end
104+
end
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# frozen_string_literal: true
2+
class Course::Scholaistic::SubmissionsController < Course::Scholaistic::Controller
3+
before_action :load_and_authorize_scholaistic_assessment
4+
5+
before_action :sync_scholaistic_submission!, only: [:show]
6+
7+
def index
8+
@embed_src = ScholaisticApiService.embed!(
9+
current_course_user,
10+
ScholaisticApiService.submissions_path(@scholaistic_assessment.upstream_id),
11+
request.origin
12+
)
13+
end
14+
15+
def show
16+
result = ScholaisticApiService.submission!(current_course, submission_id)
17+
head :not_found and return if result[:status] == :not_found
18+
19+
@creator_name = result[:creator_name]
20+
21+
@embed_src = ScholaisticApiService.embed!(
22+
current_course_user,
23+
ScholaisticApiService.submission_path(@scholaistic_assessment.upstream_id, submission_id),
24+
request.origin
25+
)
26+
end
27+
28+
def submission
29+
submission_id = ScholaisticApiService.find_or_create_submission!(
30+
current_course_user,
31+
@scholaistic_assessment.upstream_id
32+
)
33+
34+
render json: { id: submission_id }
35+
end
36+
37+
private
38+
39+
def load_and_authorize_scholaistic_assessment
40+
@scholaistic_assessment = current_course.scholaistic_assessments.find(params[:assessment_id] || params[:id])
41+
authorize! :read, @scholaistic_assessment
42+
end
43+
44+
def sync_scholaistic_submission!
45+
result = ScholaisticApiService.submission!(current_course, submission_id)
46+
47+
if result[:status] != :graded
48+
@scholaistic_assessment.submissions.where(upstream_id: submission_id).destroy_all
49+
50+
return
51+
end
52+
53+
email = User::Email.find_by(email: result[:creator_email], primary: true)
54+
creator = email && current_course.users.find(email.user_id)
55+
submission = creator && @scholaistic_assessment.submissions.find_or_initialize_by(creator_id: creator.id)
56+
return unless submission
57+
58+
submission.upstream_id = submission_id
59+
submission.reason = @scholaistic_assessment.title
60+
submission.points_awarded = @scholaistic_assessment.base_exp
61+
submission.course_user = current_course.course_users.find_by(user_id: creator.id)
62+
submission.awarded_at = Time.zone.now
63+
submission.awarder = User.system
64+
65+
submission.save!
66+
end
67+
68+
def submission_id
69+
params[:id]
70+
end
71+
end

app/helpers/course/condition/conditions_helper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def conditions_component_hash
2121
hash[Course::Condition::Level.name] = :course_levels_component
2222
hash[Course::Condition::Survey.name] = :course_survey_component
2323
hash[Course::Condition::Video.name] = :course_videos_component
24+
hash[Course::Condition::ScholaisticAssessment.name] = :course_scholaistic_component
2425
end
2526
end
2627
end

0 commit comments

Comments
 (0)