Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PRT-2181] Create/Update/Delete recurring events in Moodle Calendar #123

Open
wants to merge 10 commits into
base: master-elos
Choose a base branch
from
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ RNP_CHAT_ID=your-token

### Moodle API
MCONF_MOODLE_API_TIMEOUT=5
MCONF_MOODLE_RECURRING_EVENTS_MONTH_PERIOD=12

### Integration with Google Tag Manager
MCONF_GTM_ID=GTM-000000000
Expand Down
1 change: 0 additions & 1 deletion app/controllers/rooms_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ class RoomsController < ApplicationController
# GET /rooms/1
def show
respond_to do |format|
# TODO: do this also in a worker in the future to speed up this request
@room.update_recurring_meetings
@scheduled_meetings = @room.scheduled_meetings.active

Expand Down
44 changes: 43 additions & 1 deletion app/controllers/scheduled_meetings_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,11 @@ def create
@room.can_create_moodle_calendar_event
moodle_token = @room.consumer_config.moodle_token
begin
Moodle::API.create_calendar_event(moodle_token, @scheduled_meeting, @app_launch.context_id, {nonce: @app_launch.nonce})
if @scheduled_meeting.recurring?
CreateRecurringEventsInMoodleCalendarJob.perform_later(moodle_token, @scheduled_meeting, @app_launch.context_id, {nonce: @app_launch.nonce})
else
Moodle::API.create_calendar_event(moodle_token, @scheduled_meeting.hash_id, @scheduled_meeting, @app_launch.context_id, {nonce: @app_launch.nonce})
end
rescue Moodle::UrlNotFoundError => e
set_error('room', 'moodle_url_not_found', 500)
respond_with_error(@error)
Expand All @@ -152,6 +156,8 @@ def edit
end

def update
old_repeat = @scheduled_meeting.repeat
old_start_at = @scheduled_meeting.start_at
respond_to do |format|
valid_start_at = validate_start_at(@scheduled_meeting)
if valid_start_at
Expand All @@ -166,6 +172,31 @@ def update
end

if valid_start_at && @scheduled_meeting.update(scheduled_meeting_params(@room))
moodle_calendar_events_ids = MoodleCalendarEvent.where(scheduled_meeting_hash_id: @scheduled_meeting.hash_id).pluck(:event_id)
changed_start_day = (old_start_at.to_date != @scheduled_meeting.start_at.to_date)
has_become_recurring = old_repeat.nil? && @scheduled_meeting.recurring?
has_lost_recurrence = !old_repeat.nil? && @scheduled_meeting.repeat.nil?
if @room.can_update_moodle_calendar_event && moodle_calendar_events_ids.any?
moodle_token = @room.consumer_config.moodle_token
if has_become_recurring
Moodle::API.delete_calendar_event(moodle_token, moodle_calendar_events_ids.first, @app_launch.context_id, { nonce: @app_launch.nonce })
MoodleCalendarEvent.find_by(event_id: moodle_calendar_events_ids.first).destroy
CreateRecurringEventsInMoodleCalendarJob.perform_later(moodle_token, @scheduled_meeting, @app_launch.context_id, { nonce: @app_launch.nonce })
elsif has_lost_recurrence
if changed_start_day
Moodle::API.update_calendar_event_day(moodle_token, moodle_calendar_events_ids.first, @scheduled_meeting.start_at, @app_launch.context_id, {nonce: @app_launch.nonce})
MoodleCalendarEvent.find_by(event_id: moodle_calendar_events_ids.first).update(start_at: @scheduled_meeting.start_at)
end
DeleteRecurringEventsInMoodleCalendarJob.perform_later(moodle_token, moodle_calendar_events_ids.drop(1), @app_launch.context_id, {nonce: @app_launch.nonce})
elsif changed_start_day
if @scheduled_meeting.recurring?
UpdateRecurringEventsInMoodleCalendarJob.perform_later(moodle_token, @scheduled_meeting, moodle_calendar_events_ids, @app_launch.context_id, {nonce: @app_launch.nonce})
else
Moodle::API.update_calendar_event_day(moodle_token, moodle_calendar_events_ids.first, @scheduled_meeting.start_at, @app_launch.context_id, {nonce: @app_launch.nonce})
MoodleCalendarEvent.find_by(event_id: moodle_calendar_events_ids.first).update(start_at: @scheduled_meeting.start_at)
end
end
end
format.html do
return_path = room_path(@room), { notice: t('default.scheduled_meeting.updated') }
redirect_if_brightspace(return_path) || redirect_to(*return_path)
Expand Down Expand Up @@ -386,6 +417,17 @@ def destroy
format.json { head :no_content }
end
end
moodle_calendar_events_ids = {}
moodle_calendar_events_ids = MoodleCalendarEvent.where(scheduled_meeting_hash_id: @scheduled_meeting.hash_id).pluck(:event_id)
if @room.can_delete_moodle_calendar_event && moodle_calendar_events_ids.any?
moodle_token = @room.consumer_config.moodle_token
if @scheduled_meeting.recurring?
DeleteRecurringEventsInMoodleCalendarJob.perform_later(moodle_token, moodle_calendar_events_ids, @app_launch.context_id, {nonce: @app_launch.nonce})
else
Moodle::API.delete_calendar_event(moodle_token, moodle_calendar_events_ids.first, @app_launch.context_id, {nonce: @app_launch.nonce})
MoodleCalendarEvent.find_by(event_id: moodle_calendar_events_ids.first).destroy
end
end
@scheduled_meeting.destroy
end

Expand Down
45 changes: 45 additions & 0 deletions app/jobs/create_recurring_events_in_moodle_calendar_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
require './lib/moodle'

class CreateRecurringEventsInMoodleCalendarJob < ApplicationJob
queue_as :default

def perform(moodle_token, scheduled_meeting, context_id, opts={})
recurring_events = generate_recurring_events(scheduled_meeting)

Resque.logger.info "[JOB] Calling Moodle API create_calendar_event for the #{recurring_events.count} recurring events generated."
recurring_events.each do |event|
Moodle::API.create_calendar_event(moodle_token, scheduled_meeting.hash_id, event, context_id, opts)
sleep(1)
end
end

private

def generate_recurring_events(scheduled_meeting)
start_at = scheduled_meeting.start_at
recurrence_type = scheduled_meeting.repeat
defaut_period = Rails.application.config.moodle_recurring_events_month_period
if recurrence_type == 'weekly'
event_count = defaut_period*4
cycle = 1
else
event_count = defaut_period*2
cycle = 2
end

Resque.logger.info "[JOB] Generating recurring events"
recurring_events = []
event_count.times do |i|
next_start_at = start_at + (i * cycle).weeks
recurring_events << ScheduledMeeting.new(
hash_id: scheduled_meeting.hash_id,
name: scheduled_meeting.name,
description: scheduled_meeting.description,
start_at: next_start_at,
duration: scheduled_meeting.duration,
)
end

recurring_events
end
end
16 changes: 16 additions & 0 deletions app/jobs/delete_recurring_events_in_moodle_calendar_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
require './lib/moodle'

class DeleteRecurringEventsInMoodleCalendarJob < ApplicationJob
queue_as :default

def perform(moodle_token, calendar_events_ids, context_id, opts={})

calendar_events_ids.each do |event_id|
Resque.logger.info "[JOB] Calling Moodle API delete_calendar_event for event_id: #{event_id}."

Moodle::API.delete_calendar_event(moodle_token, event_id, context_id, opts)
MoodleCalendarEvent.find_by(event_id: event_id).destroy
sleep(1)
end
end
end
19 changes: 19 additions & 0 deletions app/jobs/update_recurring_events_in_moodle_calendar_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
require './lib/moodle'

class UpdateRecurringEventsInMoodleCalendarJob < ApplicationJob
queue_as :default

def perform(moodle_token, scheduled_meeting, calendar_events_ids, context_id, opts={})
start_at = scheduled_meeting.start_at
cycle = scheduled_meeting.weekly? ? 1 : 2

Resque.logger.info "[JOB] Calling Moodle API update_calendar_event_day for #{calendar_events_ids.count} recurring events."

calendar_events_ids.each_with_index do |event_id, i|
next_start_at = start_at + (i * cycle).weeks
Moodle::API.update_calendar_event_day(moodle_token, event_id, next_start_at, context_id, opts)
MoodleCalendarEvent.find_by(event_id: event_id).update(start_at: next_start_at)
sleep(1)
end
end
end
31 changes: 31 additions & 0 deletions app/jobs/update_recurring_meetings_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
require './lib/moodle'

class UpdateRecurringMeetingsJob < ApplicationJob
def perform()
Room.find_each do |room|
Resque.logger.info "[JOB] Looking for meetings to be updated in room `#{room.name}`"
room.scheduled_meetings.inactive.recurring.each do |meeting|
Resque.logger.info "[JOB] Updating meeting: id=#{meeting.id}, name=#{meeting.name}"
meeting.update_to_next_recurring_date
handle_moodle_calendar_events(meeting, room)
end
end
Resque.logger.info "[JOB] All meetings updated."
end

def handle_moodle_calendar_events(meeting, room)
moodle_calendar_events = MoodleCalendarEvent.where(scheduled_meeting_hash_id: meeting.hash_id)
if moodle_calendar_events.any?
begin
app_launch = AppLaunch.find_by(nonce: meeting.created_by_launch_nonce)
cycle = meeting.weekly? ? 1 : 2
new_meeting = meeting
new_meeting.start_at = (moodle_calendar_events.last.start_at + cycle.weeks)
Resque.logger.info "[JOB] Creating a new event in Moodle Calendar in order to keep meeting's recurrence."
Moodle::API.create_calendar_event(room.moodle_token, meeting.hash_id, new_meeting, app_launch.context_id, {nonce: app_launch.nonce})
rescue StandardError => e
Resque.logger.error "Error creating the new calendar event for meeting `#{meeting.id}`, message: #{e.message}."
end
end
end
end
5 changes: 5 additions & 0 deletions app/models/moodle_calendar_event.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class MoodleCalendarEvent < ApplicationRecord
validates :scheduled_meeting_hash_id, presence: true
validates :event_id, presence: true
end

18 changes: 18 additions & 0 deletions app/models/room.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,24 @@ def can_create_moodle_calendar_event
end
end

def can_update_moodle_calendar_event
moodle_token = self.consumer_config&.moodle_token
if moodle_token
Moodle::API.token_functions_configured?(moodle_token, ['core_calendar_update_event_start_day'])
else
false
end
end

def can_delete_moodle_calendar_event
moodle_token = self.consumer_config&.moodle_token
if moodle_token
Moodle::API.token_functions_configured?(moodle_token, ['core_calendar_delete_calendar_events'])
else
false
end
end

def consumer_config
ConsumerConfig.find_by(key: self.consumer_key)
end
Expand Down
8 changes: 8 additions & 0 deletions app/models/scheduled_meeting.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ def recurring?
repeat.present?
end

def weekly?
recurring? && repeat == "weekly"
end

def every_two_weeks?
recurring? && repeat == "every_two_weeks"
end

def meeting_id
if room.moodle_group_select_enabled? && self.moodle_group_id.present?
"#{room.meeting_id}-#{self.id}-#{self.moodle_group_id}"
Expand Down
1 change: 1 addition & 0 deletions config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -165,5 +165,6 @@ class Application < Rails::Application

# Moodle API
config.moodle_api_timeout = Mconf::Env.fetch_int('MCONF_MOODLE_API_TIMEOUT', 5)
config.moodle_recurring_events_month_period = Mconf::Env.fetch_int('MCONF_MOODLE_RECURRING_EVENTS_MONTH_PERIOD', 12)
end
end
7 changes: 7 additions & 0 deletions config/jobs_schedule.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,10 @@ remove_old_app_launch_job:
class: RemoveOldAppLaunchJob
queue: default
description: "Remove old AppLaunches"

update_recurring_meetings_job:
# Every day at 01 GMT -3
cron: "0 4 * * *"
class: UpdateRecurringMeetingsJob
queue: default
description: "Update recurring meetings dates, and create a new event in Moodle Calendar if needed."
11 changes: 11 additions & 0 deletions db/migrate/20241021171714_create_moodle_calendar_events.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class CreateMoodleCalendarEvents < ActiveRecord::Migration[6.1]
def change
create_table :moodle_calendar_events do |t|
t.integer :event_id
t.string :scheduled_meeting_hash_id
t.datetime :start_at

t.timestamps
end
end
end
10 changes: 9 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2024_07_19_181154) do
ActiveRecord::Schema.define(version: 2024_10_21_171714) do

# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
Expand Down Expand Up @@ -99,6 +99,14 @@
t.text "refresh_token"
end

create_table "moodle_calendar_events", force: :cascade do |t|
t.integer "event_id"
t.string "scheduled_meeting_hash_id"
t.datetime "start_at"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end

create_table "moodle_tokens", force: :cascade do |t|
t.bigint "consumer_config_id"
t.string "token"
Expand Down
69 changes: 68 additions & 1 deletion lib/moodle.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

module Moodle
class API
def self.create_calendar_event(moodle_token, scheduled_meeting, context_id, opts={})
def self.create_calendar_event(moodle_token, sched_meeting_hash_id, scheduled_meeting, context_id, opts={})
params = {
wstoken: moodle_token.token,
wsfunction: 'core_calendar_create_calendar_events',
Expand All @@ -28,6 +28,12 @@ def self.create_calendar_event(moodle_token, scheduled_meeting, context_id, opts
if result["exception"].present?
Rails.logger.error(log_labels + "message=\"#{result}\"")
return false
else
# Create a new Moodle Calendar Event
event_params = { event_id: result["events"].first['id'],
scheduled_meeting_hash_id: sched_meeting_hash_id,
start_at: scheduled_meeting.start_at }
MoodleCalendarEvent.create!(event_params)
end

if result["warnings"].present?
Expand All @@ -38,6 +44,67 @@ def self.create_calendar_event(moodle_token, scheduled_meeting, context_id, opts
true
end

def self.update_calendar_event_day(moodle_token, event_id, new_start_at, context_id, opts={})

params = {
wstoken: moodle_token.token,
wsfunction: 'core_calendar_update_event_start_day',
moodlewsrestformat: 'json',
eventid: event_id,
daytimestamp: new_start_at.to_i
}

result = post(moodle_token.url, params)
log_labels = "[MOODLE API] url=#{moodle_token.url} " \
"token_id=#{moodle_token.id} " \
"duration=#{result['duration']&.round(3)}s " \
"wsfunction=core_calendar_update_event_start_day " \
"#{('nonce=' + opts[:nonce].to_s + ' ') if opts[:nonce]}"

if result["exception"].present?
Rails.logger.error(log_labels + "message=\"#{result}\"")
return false
end

if result["warnings"].present?
Rails.logger.warn(log_labels + "message=\"#{result["warnings"].inspect}\"")
end
Rails.logger.info(log_labels + "message=\"Event updated on Moodle calendar: event_id=#{result.dig('event', 'id')}, name='#{result.dig('event', 'name')}'\"")

true
end

def self.delete_calendar_event(moodle_token, event_id, context_id, opts)
Rails.logger.info("[MOODLE API] Deleting event=`#{event_id}`")

params = {
wstoken: moodle_token.token,
wsfunction: 'core_calendar_delete_calendar_events',
moodlewsrestformat: 'json',
'events[0][eventid]'=> event_id,
'events[0][repeat]'=> 0,
}
result = post(moodle_token.url, params)

log_labels = "[MOODLE API] url=#{moodle_token.url} " \
"token_id=#{moodle_token.id} " \
"duration=#{result['duration']&.round(3)}s " \
"wsfunction=core_calendar_delete_calendar_events " \
"#{('nonce=' + opts[:nonce].to_s + ' ') if opts[:nonce]}"

if result["exception"].present?
Rails.logger.error(log_labels + "message=\"#{result}\"")
return false
end

if result["warnings"].present?
Rails.logger.warn(log_labels + "message=\"#{result["warnings"].inspect}\"")
end
Rails.logger.info(log_labels + "message=\"Event deleted on Moodle calendar: #{result}\"")

true
end

def self.get_user_groups(moodle_token, user_id, context_id, opts={})
params = {
wstoken: moodle_token.token,
Expand Down
Loading