Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ for `DF_SECRET_KEY_BASE`, `DF_SECRET_KEY_ATTR`, `DF_SECRET_KEY_DEVISE`,
| `TII_REGISTER_WEBHOOK` | Register the Turnitin webhook. | `false` |
| `TCA_API_KEY` | Turnitin Core API key. | Unset |
| `TCA_HOST` | Turnitin institution host. | Unset |
| `DF_MOODLE_API_URL` | Moodle base URL used by unit integrations. | Unset |
| `DF_JPLAG_MIN_TOKENS` | Minimum matching-token threshold used by JPlag. | `-1` |
| `DF_JPLAG_SKIP_CLUSTER_CHECK` | Skip JPlag cluster calculation. | `false` |
| `DF_JPLAG_MAX_SHOWN_COMPARISONS` | Maximum comparisons retained in a JPlag report; `-1` means all. | `2500` |
Expand Down
2 changes: 2 additions & 0 deletions app/api/api_root.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class ApiRoot < Grape::API
mount DiscussionCommentApi
mount EngagementsApi
mount ExtensionCommentsApi
mount MoodleIntegrationApi
mount ScormExtensionCommentsApi
mount GroupSetsApi
mount LearningOutcomesApi
Expand Down Expand Up @@ -122,6 +123,7 @@ class ApiRoot < Grape::API
AuthenticationHelpers.add_auth_to DiscussionCommentApi
AuthenticationHelpers.add_auth_to EngagementsApi
AuthenticationHelpers.add_auth_to ExtensionCommentsApi
AuthenticationHelpers.add_auth_to MoodleIntegrationApi
AuthenticationHelpers.add_auth_to ScormExtensionCommentsApi
AuthenticationHelpers.add_auth_to GroupSetsApi
AuthenticationHelpers.add_auth_to LearningOutcomesApi
Expand Down
14 changes: 14 additions & 0 deletions app/api/entities/moodle_group_mapping_entity.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module Entities
class MoodleGroupMappingEntity < Grape::Entity
expose :id
expose :moodle_group_id
expose :moodle_group_name
expose :target_type
expose :group_set_id
expose :group_id
expose :campus_id
expose :tutorial_stream_id
expose :tutorial_id
expose :create_if_missing
end
end
20 changes: 20 additions & 0 deletions app/api/entities/moodle_integration_entity.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
require 'entities/moodle_group_mapping_entity'

module Entities
class MoodleIntegrationEntity < Grape::Entity
expose :id
expose :course_id
expose :assignment_id
expose :assignment_name
expose :fetch_extensions
expose :auto_sync_students
expose :auto_sync_extensions
expose :group_mapping_enabled
expose :moodle_group_mappings,
as: :group_mappings,
using: Entities::MoodleGroupMappingEntity
expose :api_key_configured do |integration|
integration.api_key.present?
end
end
end
1 change: 1 addition & 0 deletions app/api/entities/unit_entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
unless: :summary_only

expose :overseer_image_id, unless: :summary_only, if: lambda { |unit, options| can_read_unit_config?(options[:my_role]) }
expose :moodle_enabled, unless: :summary_only, if: lambda { |unit, options| can_read_unit_config?(options[:my_role]) }

Check warning on line 53 in app/api/entities/unit_entity.rb

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefix unused parameter 'unit' with an underscore (e.g., '_unit'), or remove it if it is not needed.

See more on https://sonarcloud.io/project/issues?id=doubtfire-lms_doubtfire-api&issues=AZ_Ky2F2XaSC21t9cK-4&open=AZ_Ky2F2XaSC21t9cK-4&pullRequest=666
expose :assessment_enabled, unless: :summary_only

expose :auto_apply_extension_before_deadline, unless: :summary_only, if: lambda { |unit, options| is_staff?(options[:my_role]) }
Expand Down
141 changes: 141 additions & 0 deletions app/api/moodle_integration_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# frozen_string_literal: true

require 'grape'
require 'entities/moodle_integration_entity'
require 'entities/sidekiq_job_entity'

class MoodleIntegrationApi < Grape::API
helpers AuthenticationHelpers
helpers AuthorisationHelpers
helpers SidekiqHelper

before do
authenticated?
end

desc 'Get Moodle settings for a unit'
get '/units/:unit_id/moodle' do
unit = Unit.find(params[:unit_id])
error!({ error: 'Moodle integration is not enabled for this unit' }, 404) unless unit.moodle_enabled?

Check failure on line 19 in app/api/moodle_integration_api.rb

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "Moodle integration is not enabled for this unit" 5 times.

See more on https://sonarcloud.io/project/issues?id=doubtfire-lms_doubtfire-api&issues=AZ_Ky2JVXaSC21t9cK-7&open=AZ_Ky2JVXaSC21t9cK-7&pullRequest=666
unless authorise?(current_user, unit, :update)
error!({ error: 'Not authorised to manage Moodle for this unit' }, 403)

Check failure on line 21 in app/api/moodle_integration_api.rb

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "Not authorised to manage Moodle for this unit" 5 times.

See more on https://sonarcloud.io/project/issues?id=doubtfire-lms_doubtfire-api&issues=AZ_Ky2JVXaSC21t9cK-5&open=AZ_Ky2JVXaSC21t9cK-5&pullRequest=666
end

integration = unit.moodle_integration || unit.build_moodle_integration
present integration, with: Entities::MoodleIntegrationEntity
end

desc 'Update Moodle settings for a unit'
params do
requires :course_id, type: Integer
optional :api_key, type: String
optional :assignment_id, type: Integer
optional :assignment_name, type: String
optional :fetch_extensions, type: Boolean, default: false
optional :auto_sync_students, type: Boolean, default: false
optional :auto_sync_extensions, type: Boolean, default: false
optional :group_mapping_enabled, type: Boolean, default: false
optional :group_mappings, type: Array do
requires :moodle_group_id, type: Integer
requires :moodle_group_name, type: String
requires :target_type, type: String, values: MoodleGroupMapping::TARGET_TYPES
optional :group_set_id, type: Integer
optional :group_id, type: Integer
optional :campus_id, type: Integer
optional :tutorial_stream_id, type: Integer
optional :tutorial_id, type: Integer
optional :create_if_missing, type: Boolean, default: false
end
end
put '/units/:unit_id/moodle' do
unit = Unit.find(params[:unit_id])
error!({ error: 'Moodle integration is not enabled for this unit' }, 404) unless unit.moodle_enabled?
unless authorise?(current_user, unit, :update)
error!({ error: 'Not authorised to manage Moodle for this unit' }, 403)
end

integration = unit.moodle_integration || unit.build_moodle_integration
MoodleIntegration.transaction do
integration.course_id = params[:course_id]
integration.api_key = params[:api_key] if params[:api_key].present?
integration.fetch_extensions = params[:fetch_extensions]
integration.assignment_id = params[:fetch_extensions] ? params[:assignment_id] : nil
integration.assignment_name = params[:fetch_extensions] ? params[:assignment_name] : nil
integration.auto_sync_students = params[:auto_sync_students]
integration.auto_sync_extensions = params[:fetch_extensions] && params[:auto_sync_extensions]
integration.group_mapping_enabled = params[:group_mapping_enabled]
integration.save!

if integration.group_mapping_enabled?
integration.moodle_group_mappings.delete_all
Array(params[:group_mappings]).each do |mapping|
integration.moodle_group_mappings.create!(
moodle_group_id: mapping[:moodle_group_id],
moodle_group_name: mapping[:moodle_group_name],
target_type: mapping[:target_type],
group_set_id: mapping[:group_set_id],
group_id: mapping[:group_id],
campus_id: mapping[:campus_id],
tutorial_stream_id: mapping[:tutorial_stream_id],
tutorial_id: mapping[:tutorial_id],
create_if_missing: mapping[:create_if_missing]
)
end
end
end

integration.moodle_group_mappings.reload
present integration, with: Entities::MoodleIntegrationEntity
end

desc 'Test Moodle API permissions for a unit'
post '/units/:unit_id/moodle/test' do
unit = Unit.find(params[:unit_id])
error!({ error: 'Moodle integration is not enabled for this unit' }, 404) unless unit.moodle_enabled?
unless authorise?(current_user, unit, :update)
error!({ error: 'Not authorised to manage Moodle for this unit' }, 403)
end
error!({ error: 'Configure Moodle for this unit first' }, 422) if unit.moodle_integration.blank?

Check failure on line 98 in app/api/moodle_integration_api.rb

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "Configure Moodle for this unit first" 3 times.

See more on https://sonarcloud.io/project/issues?id=doubtfire-lms_doubtfire-api&issues=AZ_Ky2JVXaSC21t9cK-6&open=AZ_Ky2JVXaSC21t9cK-6&pullRequest=666

job_id = TestMoodleConnectionJob.perform_async(unit.id)
job = setup_job(job_id)
present job, with: Entities::SidekiqJobEntity
end

desc 'Import active Moodle students into a unit'
params do
requires :preview_only, type: Boolean, default: false
end
post '/units/:unit_id/moodle/import_students' do
unit = Unit.find(params[:unit_id])
error!({ error: 'Moodle integration is not enabled for this unit' }, 404) unless unit.moodle_enabled?
unless authorise?(current_user, unit, :upload_csv)
error!({ error: 'Not authorised to manage Moodle for this unit' }, 403)
end
error!({ error: 'Configure Moodle for this unit first' }, 422) if unit.moodle_integration.blank?

job_id = ImportMoodleStudentsJob.perform_async(unit.id, params[:preview_only])
present setup_job(job_id), with: Entities::SidekiqJobEntity
end

desc 'Import Moodle assignment extensions into a unit'
params do
requires :preview_only, type: Boolean, default: false
end
post '/units/:unit_id/moodle/import_extensions' do
unit = Unit.find(params[:unit_id])
error!({ error: 'Moodle integration is not enabled for this unit' }, 404) unless unit.moodle_enabled?
unless authorise?(current_user, unit, :update)
error!({ error: 'Not authorised to manage Moodle for this unit' }, 403)
end

integration = unit.moodle_integration
error!({ error: 'Configure Moodle for this unit first' }, 422) if integration.blank?
unless integration.fetch_extensions && integration.assignment_id.present?
error!({ error: 'Enable extension imports and select a Moodle assignment first' }, 422)
end

job_id = ImportMoodleExtensionsJob.perform_async(unit.id, params[:preview_only])
present setup_job(job_id), with: Entities::SidekiqJobEntity
end
end
2 changes: 2 additions & 0 deletions app/api/units_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ class UnitsApi < Grape::API
optional :send_notifications, type: Boolean, desc: 'Indicates if emails should be sent on updates each week'
optional :enable_sync_timetable, type: Boolean, desc: 'Sync to timetable automatically if supported by deployment'
optional :enable_sync_enrolments, type: Boolean, desc: 'Sync student enrolments automatically if supported by deployment'
optional :moodle_enabled, type: Boolean, desc: 'Enable the Moodle integration for this unit'
optional :draft_task_definition_id, type: Integer, desc: 'Indicates the ID of the task definition used as the "draft learning summary task"'
optional :portfolio_auto_generation_date, type: Date, desc: 'Indicates a date where student portfolio will automatically compile'
optional :allow_flexible_dates, type: Boolean, desc: 'Can turn on/off flexible dates for tasks in this unit'
Expand Down Expand Up @@ -128,6 +129,7 @@ class UnitsApi < Grape::API
:send_notifications,
:enable_sync_timetable,
:enable_sync_enrolments,
:moodle_enabled,
:draft_task_definition_id,
:portfolio_auto_generation_date,
:allow_flexible_dates,
Expand Down
57 changes: 57 additions & 0 deletions app/models/moodle_group_mapping.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# frozen_string_literal: true

class MoodleGroupMapping < ApplicationRecord
TARGET_TYPES = %w[group campus tutorial].freeze

belongs_to :moodle_integration
belongs_to :group_set, optional: true
belongs_to :group, optional: true
belongs_to :campus, optional: true
belongs_to :tutorial_stream, optional: true
belongs_to :tutorial, optional: true

validates :moodle_group_id, numericality: { only_integer: true, greater_than: 0 }
validates :moodle_group_id, uniqueness: { scope: :moodle_integration_id }
validates :moodle_group_name, presence: true
validates :target_type, inclusion: { in: TARGET_TYPES }
validate :valid_target

private

def valid_target

Check failure on line 21 in app/models/moodle_group_mapping.rb

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 45 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=doubtfire-lms_doubtfire-api&issues=AZ_LISRARWAtOc_KA5sS&open=AZ_LISRARWAtOc_KA5sS&pullRequest=666
unit = moodle_integration&.unit

case target_type

Check failure on line 24 in app/models/moodle_group_mapping.rb

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a default clause to this "case" statement.

See more on https://sonarcloud.io/project/issues?id=doubtfire-lms_doubtfire-api&issues=AZ_LISRARWAtOc_KA5sT&open=AZ_LISRARWAtOc_KA5sT&pullRequest=666
when 'group'
errors.add(:group_set, 'must be selected') if group_set.blank?

Check failure on line 26 in app/models/moodle_group_mapping.rb

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "must be selected" 5 times.

See more on https://sonarcloud.io/project/issues?id=doubtfire-lms_doubtfire-api&issues=AZ_LISRARWAtOc_KA5sR&open=AZ_LISRARWAtOc_KA5sR&pullRequest=666
errors.add(:group_set, 'must belong to this unit') if group_set.present? && group_set.unit != unit

Check failure on line 27 in app/models/moodle_group_mapping.rb

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "must belong to this unit" 4 times.

See more on https://sonarcloud.io/project/issues?id=doubtfire-lms_doubtfire-api&issues=AZ_LNRTI0H6gIcoBw-n9&open=AZ_LNRTI0H6gIcoBw-n9&pullRequest=666
if create_if_missing?
if tutorial.blank? == tutorial_stream.blank?
errors.add(:base, 'select an existing tutorial or a tutorial stream for the new group')
end
errors.add(:tutorial, 'must belong to this unit') if tutorial.present? && tutorial.unit != unit
if tutorial_stream.present? && tutorial_stream.unit != unit
errors.add(:tutorial_stream, 'must belong to this unit')
end
else
errors.add(:group, 'must be selected') if group.blank?
if group.present? && (group.group_set != group_set || group.unit != unit)
errors.add(:group, 'must belong to the selected group set')
end
end
when 'campus'
errors.add(:campus, 'must be selected') if campus.blank?
when 'tutorial'
errors.add(:tutorial_stream, 'must be selected') if tutorial_stream.blank?
if tutorial_stream.present? && tutorial_stream.unit != unit
errors.add(:tutorial_stream, 'must belong to this unit')
end
unless create_if_missing?
errors.add(:tutorial, 'must be selected') if tutorial.blank?
if tutorial.present? && (tutorial.tutorial_stream != tutorial_stream || tutorial.unit != unit)
errors.add(:tutorial, 'must belong to the selected tutorial stream')
end
end
end
end
end
12 changes: 12 additions & 0 deletions app/models/moodle_integration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

class MoodleIntegration < ApplicationRecord
belongs_to :unit
has_many :moodle_group_mappings, dependent: :destroy

encrypts :api_key

validates :course_id, numericality: { only_integer: true, greater_than: 0 }
validates :assignment_id, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
validates :unit_id, uniqueness: true
end
3 changes: 3 additions & 0 deletions app/models/unit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ class Unit < ApplicationRecord
include MimeCheckHelpers
include CsvHelper


has_one :moodle_integration, dependent: :destroy

#
# Permissions around unit data
#
Expand Down
Loading
Loading