Skip to content

Commit cacafb5

Browse files
purfectliteraturecysjonathan
authored andcommitted
feat: add scholaistic integration
1 parent d3d95b1 commit cacafb5

File tree

83 files changed

+2415
-33
lines changed

Some content is hidden

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

83 files changed

+2415
-33
lines changed

app/controllers/components/course/scholaistic_component.rb

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,62 @@ 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+
return [] unless can?(:manage_scholaistic_assistants, current_course)
57+
58+
[
59+
{
60+
key: :scholaistic_assistants,
61+
type: :admin,
62+
icon: :chatbot,
63+
title: I18n.t('components.scholaistic.manage_assistants'),
64+
weight: 9,
65+
path: course_scholaistic_assistants_path(current_course),
66+
exact: true
67+
}
68+
]
69+
end
70+
2071
def settings_sidebar_items
2172
[
2273
{
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: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,18 @@ def edit
88
end
99

1010
def update
11-
update_settings_and_render(scholaistic_settings_params)
11+
if @settings.update(params) && current_course.save
12+
render_settings
13+
else
14+
render json: { errors: @settings.errors }, status: :bad_request
15+
end
1216
end
1317

1418
def confirm_link_course
1519
key = ScholaisticApiService.parse_link_course_callback_request(request, params)
1620
head :bad_request and return if key.blank?
1721

18-
@settings.update(integration_key: key) && current_course.save
22+
@settings.update(integration_key: key, last_synced_at: nil) && current_course.save
1923
end
2024

2125
def link_course
@@ -33,9 +37,18 @@ def link_course
3337
def unlink_course
3438
head :ok and return if @settings.integration_key.blank?
3539

36-
ScholaisticApiService.unlink_course!(@settings.integration_key)
40+
ActiveRecord::Base.transaction do
41+
ScholaisticApiService.unlink_course!(@settings.integration_key)
42+
43+
raise ActiveRecord::Rollback unless current_course.scholaistic_assessments.destroy_all
44+
45+
@settings.update(integration_key: nil, last_synced_at: nil)
46+
current_course.save!
47+
end
3748

38-
update_settings_and_render(integration_key: nil)
49+
render_settings
50+
rescue ActiveRecord::Rollback
51+
render json: { errors: @settings.errors }, status: :bad_request
3952
end
4053

4154
protected
@@ -58,12 +71,4 @@ def render_settings
5871
@ping_result = ScholaisticApiService.ping_course(@settings.integration_key) if @settings.integration_key.present?
5972
render 'edit'
6073
end
61-
62-
def update_settings_and_render(params)
63-
if @settings.update(params) && current_course.save
64-
render_settings
65-
else
66-
render json: { errors: @settings.errors }, status: :bad_request
67-
end
68-
end
6974
end
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: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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+
submission_status = submissions_status_hash[assessment.upstream_id]&.[](:status)
15+
16+
[assessment.id,
17+
if submission_status == :graded
18+
:submitted
19+
elsif submission_status.present?
20+
submission_status
21+
else
22+
(can?(:attempt, assessment) && (assessment.start_at <= Time.zone.now)) ? :open : :unavailable
23+
end]
24+
end
25+
end
26+
27+
def new
28+
@embed_src = ScholaisticApiService.embed!(
29+
current_course_user,
30+
ScholaisticApiService.new_assessment_path,
31+
request.origin
32+
)
33+
end
34+
35+
def show
36+
upstream_id = @scholaistic_assessment.upstream_id
37+
38+
@embed_src =
39+
ScholaisticApiService.embed!(
40+
current_course_user,
41+
if can?(:update, @scholaistic_assessment)
42+
ScholaisticApiService.edit_assessment_path(upstream_id)
43+
else
44+
ScholaisticApiService.assessment_path(upstream_id)
45+
end,
46+
request.origin
47+
)
48+
end
49+
50+
def edit
51+
@embed_src = ScholaisticApiService.embed!(
52+
current_course_user,
53+
ScholaisticApiService.edit_assessment_details_path(@scholaistic_assessment.upstream_id),
54+
request.origin
55+
)
56+
end
57+
58+
def update
59+
if @scholaistic_assessment.update(update_params)
60+
head :ok
61+
else
62+
render json: { errors: @scholaistic_assessment.errors.full_messages.to_sentence }, status: :bad_request
63+
end
64+
end
65+
66+
private
67+
68+
def update_params
69+
params.require(:scholaistic_assessment).permit(:base_exp)
70+
end
71+
72+
def sync_scholaistic_assessments!
73+
response = ScholaisticApiService.assessments!(current_course)
74+
75+
# TODO: The SQL queries will scale proportionally with `response[:assessments].size`,
76+
# but we won't always have to sync all assessments since there's `last_synced_at`.
77+
# In the future, we can optimise this, but it's not easy because there are multiple
78+
# relations to `Course::ScholaisticAssessment` that need to be updated.
79+
ActiveRecord::Base.transaction do
80+
response[:assessments].map do |assessment|
81+
current_course.scholaistic_assessments.find_or_initialize_by(
82+
upstream_id: assessment[:upstream_id]
83+
).tap do |scholaistic_assessment|
84+
scholaistic_assessment.start_at = assessment[:start_at]
85+
scholaistic_assessment.end_at = assessment[:end_at]
86+
scholaistic_assessment.title = assessment[:title]
87+
scholaistic_assessment.description = assessment[:description]
88+
scholaistic_assessment.published = assessment[:published]
89+
end.save!
90+
end
91+
92+
if response[:deleted].present? && !current_course.scholaistic_assessments.
93+
where(upstream_id: response[:deleted]).destroy_all
94+
raise ActiveRecord::Rollback
95+
end
96+
97+
current_course.settings(:course_scholaistic_component).public_send('last_synced_at=', response[:last_synced_at])
98+
99+
current_course.save!
100+
end
101+
end
102+
103+
def submission_status(assessment)
104+
end
105+
end

0 commit comments

Comments
 (0)