From bf3c4f3d8d02f05a7a0be0b9f70d12bb4857bbb1 Mon Sep 17 00:00:00 2001 From: paolahoff Date: Mon, 21 Oct 2024 23:15:48 -0300 Subject: [PATCH 01/10] [migration]: create the `MoodleCalendarEvents` table --- .../20241021171714_create_moodle_calendar_events.rb | 10 ++++++++++ db/schema.rb | 9 ++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20241021171714_create_moodle_calendar_events.rb diff --git a/db/migrate/20241021171714_create_moodle_calendar_events.rb b/db/migrate/20241021171714_create_moodle_calendar_events.rb new file mode 100644 index 00000000..4e902427 --- /dev/null +++ b/db/migrate/20241021171714_create_moodle_calendar_events.rb @@ -0,0 +1,10 @@ +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.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index ce02c5e7..64d80c07 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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" @@ -99,6 +99,13 @@ 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 "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" From 49df127896cf6d236623c2faefa31c39ea3d77c5 Mon Sep 17 00:00:00 2001 From: paolahoff Date: Mon, 21 Oct 2024 23:16:57 -0300 Subject: [PATCH 02/10] add: create `moodle_calendar_event` model --- app/models/moodle_calendar_event.rb | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 app/models/moodle_calendar_event.rb diff --git a/app/models/moodle_calendar_event.rb b/app/models/moodle_calendar_event.rb new file mode 100644 index 00000000..b47669ed --- /dev/null +++ b/app/models/moodle_calendar_event.rb @@ -0,0 +1,5 @@ +class MoodleCalendarEvent < ApplicationRecord + validates :scheduled_meeting_hash_id, presence: true + validates :event_id, presence: true +end + \ No newline at end of file From 7819336c3dc22d08d35f636f9708159c2f26d456 Mon Sep 17 00:00:00 2001 From: paolahoff Date: Mon, 21 Oct 2024 23:31:07 -0300 Subject: [PATCH 03/10] feat: create recurring events in Moodle Calendar - For recurring Meetings, generates the events for the next 12 months and creates them individually in the Moodle Calendar - Also creates objects in the MoodleCalendarEvents table corresponding to the events created in the Moodle, whether they are recurring or not --- .env.example | 1 + .../scheduled_meetings_controller.rb | 6 ++- config/application.rb | 1 + lib/moodle.rb | 38 +++++++++++++++++++ .../elos/assets/javascripts/schedule-elos.js | 20 ---------- themes/elos/config/locales/en.yml | 2 +- themes/elos/config/locales/es.yml | 2 +- themes/elos/config/locales/pt.yml | 2 +- themes/rnp/config/locales/en.yml | 2 +- themes/rnp/config/locales/es.yml | 2 +- themes/rnp/config/locales/pt.yml | 2 +- 11 files changed, 51 insertions(+), 27 deletions(-) diff --git a/.env.example b/.env.example index 738f87cc..b852ddba 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/app/controllers/scheduled_meetings_controller.rb b/app/controllers/scheduled_meetings_controller.rb index 4dbbb388..0a9e81f0 100644 --- a/app/controllers/scheduled_meetings_controller.rb +++ b/app/controllers/scheduled_meetings_controller.rb @@ -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? + Moodle::API.generate_recurring_events(moodle_token, @scheduled_meeting, @app_launch.context_id, {nonce: @app_launch.nonce}) + else + Moodle::API.create_calendar_event(moodle_token, @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) diff --git a/config/application.rb b/config/application.rb index f845730a..461c4561 100644 --- a/config/application.rb +++ b/config/application.rb @@ -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 diff --git a/lib/moodle.rb b/lib/moodle.rb index 163edc15..d9002c07 100644 --- a/lib/moodle.rb +++ b/lib/moodle.rb @@ -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: scheduled_meeting.hash_id } + puts("Event params: #{event_params.inspect}") + MoodleCalendarEvent.create!(event_params) end if result["warnings"].present? @@ -38,6 +44,38 @@ def self.create_calendar_event(moodle_token, scheduled_meeting, context_id, opts true end + def self.generate_recurring_events(moodle_token, scheduled_meeting, context_id, opts) + 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 + + Rails.logger.info "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 + + Rails.logger.info "#{event_count} recurring events generated. Calling Moodle API create_calendar_event" + recurring_events.each do |event| + self.create_calendar_event(moodle_token, event, context_id, opts) + end + + end + def self.get_user_groups(moodle_token, user_id, context_id, opts={}) params = { wstoken: moodle_token.token, diff --git a/themes/elos/assets/javascripts/schedule-elos.js b/themes/elos/assets/javascripts/schedule-elos.js index 1f08e30d..2f675bd5 100644 --- a/themes/elos/assets/javascripts/schedule-elos.js +++ b/themes/elos/assets/javascripts/schedule-elos.js @@ -13,26 +13,6 @@ $(document).on('turbolinks:load', function(){ contentCustomDuration.classList.remove('d-block') } - if($('input[name="scheduled_meeting[create_moodle_calendar_event]"]')) { - let recurrenceSelect = document.getElementsByName("scheduled_meeting[repeat]")[0] - recurrenceSelect?.addEventListener('change', toggleMoodleCalendarCheckbox) - - function toggleMoodleCalendarCheckbox(e) { - let valueSelectDuration = e.target.value; - if (!!valueSelectDuration) { - $('input[name="scheduled_meeting[create_moodle_calendar_event]"]').each(function () { - $(this).prop('checked', false); - $(this).prop('disabled', true); - }); - } else { - $('input[name="scheduled_meeting[create_moodle_calendar_event]"]').each(function () { - $(this).prop('checked', true); - $(this).prop('disabled', false); - }); - } - } - } - if(window.location.href.includes('/edit')){ var duration = document.getElementsByName("scheduled_meeting[custom_duration]")[0].value, durationSeconds = (duration.split(':')[0] * 60 * 60 ) + ( duration.split(':')[1] * 60 ), diff --git a/themes/elos/config/locales/en.yml b/themes/elos/config/locales/en.yml index 3e9a0a5d..4c159c19 100644 --- a/themes/elos/config/locales/en.yml +++ b/themes/elos/config/locales/en.yml @@ -21,7 +21,7 @@ en: hour: "%{count}h" minute: "%{count}min" hint: - create_moodle_calendar_event: "Creates a corresponding event on the Moodle Calendar for this scheduled meeting. This option is disabled when the scheduled meeting is set to be recurrent." + create_moodle_calendar_event: "Creates a corresponding event on the Moodle Calendar for this scheduled meeting." disable_external_link: "The access to the virtual room will be restricted to authenticated students." disable_private_chat: "The student will still be able to send private messages to the teacher. This feature can be modified during the conference using the \"Manage users\" section." disable_note: "The teacher will be able to use the shared notes as usual, but the students will only be able to read them, not edit. This feature can be modified during the conference using the \"Manage users\" section." diff --git a/themes/elos/config/locales/es.yml b/themes/elos/config/locales/es.yml index 098ba248..e3840e91 100644 --- a/themes/elos/config/locales/es.yml +++ b/themes/elos/config/locales/es.yml @@ -21,7 +21,7 @@ es: hour: "%{count}h" minute: "%{count}min" hint: - create_moodle_calendar_event: "Crea un evento correspondiente en el Calendario de Moodle para esta reunión programada. Esta opción está deshabilitada cuando la reunión programada está configurada como recurrente." + create_moodle_calendar_event: "Crea un evento correspondiente en el Calendario de Moodle para esta reunión programada." disable_external_link: "El acceso a la sala virtual estará restringido a estudiantes autenticados." disable_private_chat: "El estudiante aún puede enviar mensajes privados al maestro. Esta función se puede reactivar dentro de la habitación a través de la opción \"Restringir participantes\"." disable_note: "El profesor aún puede usar las notas compartidas, pero los estudiantes solo pueden ver el contenido, no editarlo. Esta función se puede reactivar dentro de la habitación a través de la opción \"Restringir participantes\"." diff --git a/themes/elos/config/locales/pt.yml b/themes/elos/config/locales/pt.yml index c75426c9..7fc4e759 100644 --- a/themes/elos/config/locales/pt.yml +++ b/themes/elos/config/locales/pt.yml @@ -21,7 +21,7 @@ pt: hour: "%{count}h" minute: "%{count}min" hint: - create_moodle_calendar_event: "Cria um evento correspondente no Calendário do Moodle para esta sessão agendada. Esta opção é desativada quando a sessão agendada é configurada para ser recorrente." + create_moodle_calendar_event: "Cria um evento correspondente no Calendário do Moodle para esta sessão agendada." disable_external_link: "O acesso à sala virtual será restrito aos alunos autenticados." disable_private_chat: "O aluno ainda poderá enviar mensagens privadas para o professor. Este recurso pode ser reativado dentro da sala através da opção \"Restringir participantes\"." disable_note: "O professor ainda poderá utilizar as notas compartilhadas, mas os alunos poderão apenas visualizar o conteúdo, não editar. Este recurso pode ser reativado dentro da sala através da opção \"Restringir participantes\"." diff --git a/themes/rnp/config/locales/en.yml b/themes/rnp/config/locales/en.yml index 31c0a274..eda93069 100644 --- a/themes/rnp/config/locales/en.yml +++ b/themes/rnp/config/locales/en.yml @@ -27,7 +27,7 @@ en: hour: "%{count}h" minute: "%{count}min" hint: - create_moodle_calendar_event: "Creates a corresponding event on the Moodle Calendar for this scheduled meeting. This option is disabled when the scheduled meeting is set to be recurrent." + create_moodle_calendar_event: "Creates a corresponding event on the Moodle Calendar for this scheduled meeting." disable_external_link: "The access to the virtual room will be restricted to authenticated students." disable_private_chat: "The student will still be able to send private messages to the teacher. This feature can be modified during the conference using the \"Manage users\" section." disable_note: "The teacher will be able to use the shared notes as usual, but the students will only be able to read them, not edit. This feature can be modified during the conference using the \"Manage users\" section." diff --git a/themes/rnp/config/locales/es.yml b/themes/rnp/config/locales/es.yml index 158ee4ba..133a3545 100644 --- a/themes/rnp/config/locales/es.yml +++ b/themes/rnp/config/locales/es.yml @@ -27,7 +27,7 @@ es: hour: "%{count}h" minute: "%{count}min" hint: - create_moodle_calendar_event: "Crea un evento correspondiente en el Calendario de Moodle para esta reunión programada. Esta opción está deshabilitada cuando la reunión programada está configurada como recurrente." + create_moodle_calendar_event: "Crea un evento correspondiente en el Calendario de Moodle para esta reunión programada." disable_external_link: "El acceso a la sala virtual estará restringido a estudiantes autenticados." disable_private_chat: "El estudiante aún puede enviar mensajes privados al maestro. Esta función se puede reactivar dentro de la habitación a través de la opción \"Restringir participantes\"." disable_note: "El profesor aún puede usar las notas compartidas, pero los estudiantes solo pueden ver el contenido, no editarlo. Esta función se puede reactivar dentro de la habitación a través de la opción \"Restringir participantes\"." diff --git a/themes/rnp/config/locales/pt.yml b/themes/rnp/config/locales/pt.yml index b8b22b4e..1c14f619 100644 --- a/themes/rnp/config/locales/pt.yml +++ b/themes/rnp/config/locales/pt.yml @@ -27,7 +27,7 @@ pt: hour: "%{count}h" minute: "%{count}min" hint: - create_moodle_calendar_event: "Cria um evento correspondente no Calendário do Moodle para esta sessão agendada. Esta opção é desativada quando a sessão agendada é configurada para ser recorrente." + create_moodle_calendar_event: "Cria um evento correspondente no Calendário do Moodle para esta sessão agendada." disable_external_link: "O acesso à sala virtual será restrito aos alunos autenticados." disable_private_chat: "O aluno ainda poderá enviar mensagens privadas para o professor. Este recurso pode ser reativado dentro da sala através da opção \"Restringir participantes\"." disable_note: "O professor ainda poderá utilizar as notas compartilhadas, mas os alunos poderão apenas visualizar o conteúdo, não editar. Este recurso pode ser reativado dentro da sala através da opção \"Restringir participantes\"." From 0ada551f48266d8aeb1a6b69c1524f11bf226223 Mon Sep 17 00:00:00 2001 From: paolahoff Date: Wed, 23 Oct 2024 14:08:01 -0300 Subject: [PATCH 04/10] add: auxiliary functions for Room and ScheduledMeeting --- app/models/room.rb | 18 ++++++++++++++++++ app/models/scheduled_meeting.rb | 8 ++++++++ 2 files changed, 26 insertions(+) diff --git a/app/models/room.rb b/app/models/room.rb index 70404647..8a7efcdc 100644 --- a/app/models/room.rb +++ b/app/models/room.rb @@ -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 diff --git a/app/models/scheduled_meeting.rb b/app/models/scheduled_meeting.rb index 25dae026..e876837f 100644 --- a/app/models/scheduled_meeting.rb +++ b/app/models/scheduled_meeting.rb @@ -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}" From a6a78660f19d37eebd638785b89b75d1e331671e Mon Sep 17 00:00:00 2001 From: paolahoff Date: Wed, 23 Oct 2024 14:08:37 -0300 Subject: [PATCH 05/10] feat: update and delete Moodle Calendar Events --- .../scheduled_meetings_controller.rb | 24 ++++++ lib/moodle.rb | 76 ++++++++++++++++++- 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/app/controllers/scheduled_meetings_controller.rb b/app/controllers/scheduled_meetings_controller.rb index 0a9e81f0..8d2afa3b 100644 --- a/app/controllers/scheduled_meetings_controller.rb +++ b/app/controllers/scheduled_meetings_controller.rb @@ -156,6 +156,7 @@ def edit end def update + old_start_at = @scheduled_meeting.start_at respond_to do |format| valid_start_at = validate_start_at(@scheduled_meeting) if valid_start_at @@ -170,6 +171,16 @@ def update end if valid_start_at && @scheduled_meeting.update(scheduled_meeting_params(@room)) + moodle_calendar_events = MoodleCalendarEvent.where(scheduled_meeting_hash_id: @scheduled_meeting.hash_id) + changed_start_day = (old_start_at.to_date != @scheduled_meeting.start_at.to_date) + if @room.can_update_moodle_calendar_event && moodle_calendar_events.any? && changed_start_day + moodle_token = @room.consumer_config.moodle_token + if @scheduled_meeting.recurring? + Moodle::API.handle_recurring_events_update(moodle_token, @scheduled_meeting, moodle_calendar_events, @app_launch.context_id, {nonce: @app_launch.nonce}) + else + Moodle::API.update_calendar_event_day(moodle_token, moodle_calendar_events, @app_launch.context_id, {nonce: @app_launch.nonce}) + 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) @@ -390,6 +401,19 @@ def destroy format.json { head :no_content } end end + moodle_calendar_events = MoodleCalendarEvent.where(scheduled_meeting_hash_id: @scheduled_meeting.hash_id) + if @room.can_delete_moodle_calendar_event && moodle_calendar_events.any? + moodle_token = @room.consumer_config.moodle_token + if @scheduled_meeting.recurring? + moodle_calendar_events.each do |event| + Moodle::API.delete_calendar_event(moodle_token, event.event_id, @app_launch.context_id, {nonce: @app_launch.nonce}) + event.destroy + end + else + Moodle::API.delete_calendar_event(moodle_token, moodle_calendar_events.first.event_id, @app_launch.context_id, {nonce: @app_launch.nonce}) + moodle_calendar_events.first.destroy + end + end @scheduled_meeting.destroy end diff --git a/lib/moodle.rb b/lib/moodle.rb index d9002c07..a6832401 100644 --- a/lib/moodle.rb +++ b/lib/moodle.rb @@ -32,7 +32,6 @@ def self.create_calendar_event(moodle_token, scheduled_meeting, context_id, opts # Create a new Moodle Calendar Event event_params = { event_id: result["events"].first['id'], scheduled_meeting_hash_id: scheduled_meeting.hash_id } - puts("Event params: #{event_params.inspect}") MoodleCalendarEvent.create!(event_params) end @@ -44,6 +43,79 @@ def self.create_calendar_event(moodle_token, scheduled_meeting, context_id, opts true end + def self.update_calendar_event_day(moodle_token, event, new_start_at, context_id, opts={}) + Rails.logger.info "Updating event `#{event.event_id}`" + + params = { + wstoken: moodle_token.token, + wsfunction: 'core_calendar_update_event_start_day', + moodlewsrestformat: 'json', + eventid: event.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: #{result["events"].first['id']}\"") + + true + end + + def self.delete_calendar_event(moodle_token, event_id, context_id, opts) + Rails.logger.info("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.handle_recurring_events_update(moodle_token, scheduled_meeting, calendar_events, context_id, opts={}) + start_at = scheduled_meeting.start_at + cycle = scheduled_meeting.weekly? ? 4 : 2 + + Rails.logger.info "Calling Moodle API update_calendar_event_day for #{calendar_events.count} recurring events. " + calendar_events.each_with_index do |event, i| + next_start_at = start_at + (i * cycle).weeks + self.update_calendar_event_day(moodle_token, event, next_start_at, context_id, opts) + end + end + def self.generate_recurring_events(moodle_token, scheduled_meeting, context_id, opts) start_at = scheduled_meeting.start_at recurrence_type = scheduled_meeting.repeat @@ -69,7 +141,7 @@ def self.generate_recurring_events(moodle_token, scheduled_meeting, context_id, ) end - Rails.logger.info "#{event_count} recurring events generated. Calling Moodle API create_calendar_event" + Rails.logger.info "Calling Moodle API create_calendar_event for the #{event_count} recurring events generated. " recurring_events.each do |event| self.create_calendar_event(moodle_token, event, context_id, opts) end From 06118c3a509b6adc60c64b56f6266135efdaeb6f Mon Sep 17 00:00:00 2001 From: paolahoff Date: Sun, 27 Oct 2024 19:47:43 -0300 Subject: [PATCH 06/10] chore: async event creation/update/deletion in Moodle calendar --- .../scheduled_meetings_controller.rb | 27 +++++----- ...recurring_events_in_moodle_calendar_job.rb | 44 +++++++++++++++ ...recurring_events_in_moodle_calendar_job.rb | 15 ++++++ ...recurring_events_in_moodle_calendar_job.rb | 17 ++++++ lib/moodle.rb | 54 ++----------------- 5 files changed, 94 insertions(+), 63 deletions(-) create mode 100644 app/jobs/create_recurring_events_in_moodle_calendar_job.rb create mode 100644 app/jobs/delete_recurring_events_in_moodle_calendar_job.rb create mode 100644 app/jobs/update_recurring_events_in_moodle_calendar_job.rb diff --git a/app/controllers/scheduled_meetings_controller.rb b/app/controllers/scheduled_meetings_controller.rb index 8d2afa3b..af4a8e01 100644 --- a/app/controllers/scheduled_meetings_controller.rb +++ b/app/controllers/scheduled_meetings_controller.rb @@ -128,9 +128,9 @@ def create moodle_token = @room.consumer_config.moodle_token begin if @scheduled_meeting.recurring? - Moodle::API.generate_recurring_events(moodle_token, @scheduled_meeting, @app_launch.context_id, {nonce: @app_launch.nonce}) + 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, @app_launch.context_id, {nonce: @app_launch.nonce}) + 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) @@ -171,14 +171,15 @@ def update end if valid_start_at && @scheduled_meeting.update(scheduled_meeting_params(@room)) - moodle_calendar_events = MoodleCalendarEvent.where(scheduled_meeting_hash_id: @scheduled_meeting.hash_id) + moodle_calendar_events_ids = {} + 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) - if @room.can_update_moodle_calendar_event && moodle_calendar_events.any? && changed_start_day + if @room.can_update_moodle_calendar_event && moodle_calendar_events_ids.any? && changed_start_day moodle_token = @room.consumer_config.moodle_token if @scheduled_meeting.recurring? - Moodle::API.handle_recurring_events_update(moodle_token, @scheduled_meeting, moodle_calendar_events, @app_launch.context_id, {nonce: @app_launch.nonce}) + 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, @app_launch.context_id, {nonce: @app_launch.nonce}) + 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}) end end format.html do @@ -401,17 +402,15 @@ def destroy format.json { head :no_content } end end - moodle_calendar_events = MoodleCalendarEvent.where(scheduled_meeting_hash_id: @scheduled_meeting.hash_id) - if @room.can_delete_moodle_calendar_event && moodle_calendar_events.any? + 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? - moodle_calendar_events.each do |event| - Moodle::API.delete_calendar_event(moodle_token, event.event_id, @app_launch.context_id, {nonce: @app_launch.nonce}) - event.destroy - end + 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.first.event_id, @app_launch.context_id, {nonce: @app_launch.nonce}) - moodle_calendar_events.first.destroy + 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 diff --git a/app/jobs/create_recurring_events_in_moodle_calendar_job.rb b/app/jobs/create_recurring_events_in_moodle_calendar_job.rb new file mode 100644 index 00000000..6f299604 --- /dev/null +++ b/app/jobs/create_recurring_events_in_moodle_calendar_job.rb @@ -0,0 +1,44 @@ +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) + 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 diff --git a/app/jobs/delete_recurring_events_in_moodle_calendar_job.rb b/app/jobs/delete_recurring_events_in_moodle_calendar_job.rb new file mode 100644 index 00000000..4f6d27a6 --- /dev/null +++ b/app/jobs/delete_recurring_events_in_moodle_calendar_job.rb @@ -0,0 +1,15 @@ +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 + end + end +end diff --git a/app/jobs/update_recurring_events_in_moodle_calendar_job.rb b/app/jobs/update_recurring_events_in_moodle_calendar_job.rb new file mode 100644 index 00000000..a4309faf --- /dev/null +++ b/app/jobs/update_recurring_events_in_moodle_calendar_job.rb @@ -0,0 +1,17 @@ +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) + end + end +end diff --git a/lib/moodle.rb b/lib/moodle.rb index a6832401..02b19b47 100644 --- a/lib/moodle.rb +++ b/lib/moodle.rb @@ -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', @@ -31,7 +31,7 @@ def self.create_calendar_event(moodle_token, scheduled_meeting, context_id, opts else # Create a new Moodle Calendar Event event_params = { event_id: result["events"].first['id'], - scheduled_meeting_hash_id: scheduled_meeting.hash_id } + scheduled_meeting_hash_id: sched_meeting_hash_id } MoodleCalendarEvent.create!(event_params) end @@ -43,14 +43,13 @@ def self.create_calendar_event(moodle_token, scheduled_meeting, context_id, opts true end - def self.update_calendar_event_day(moodle_token, event, new_start_at, context_id, opts={}) - Rails.logger.info "Updating event `#{event.event_id}`" + 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.event_id, + eventid: event_id, daytimestamp: new_start_at.to_i } @@ -69,7 +68,7 @@ def self.update_calendar_event_day(moodle_token, event, new_start_at, context_id 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: #{result["events"].first['id']}\"") + Rails.logger.info(log_labels + "message=\"Event updated on Moodle calendar: event_id=#{result.dig('event', 'id')}, name='#{result.dig('event', 'name')}'\"") true end @@ -105,49 +104,6 @@ def self.delete_calendar_event(moodle_token, event_id, context_id, opts) true end - def self.handle_recurring_events_update(moodle_token, scheduled_meeting, calendar_events, context_id, opts={}) - start_at = scheduled_meeting.start_at - cycle = scheduled_meeting.weekly? ? 4 : 2 - - Rails.logger.info "Calling Moodle API update_calendar_event_day for #{calendar_events.count} recurring events. " - calendar_events.each_with_index do |event, i| - next_start_at = start_at + (i * cycle).weeks - self.update_calendar_event_day(moodle_token, event, next_start_at, context_id, opts) - end - end - - def self.generate_recurring_events(moodle_token, scheduled_meeting, context_id, opts) - 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 - - Rails.logger.info "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 - - Rails.logger.info "Calling Moodle API create_calendar_event for the #{event_count} recurring events generated. " - recurring_events.each do |event| - self.create_calendar_event(moodle_token, event, context_id, opts) - end - - end - def self.get_user_groups(moodle_token, user_id, context_id, opts={}) params = { wstoken: moodle_token.token, From 1a5ba78dcfc79ff595c99e25d4235730d6e63f37 Mon Sep 17 00:00:00 2001 From: paolahoff Date: Tue, 29 Oct 2024 10:34:55 -0300 Subject: [PATCH 07/10] [migration]: add `start_at` to MoodleCalendarEvents table - it will be needed to create a new event in Moodle calendar when the scheduled meeting has its date updated (to keep recurrence) --- db/migrate/20241021171714_create_moodle_calendar_events.rb | 1 + db/schema.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/db/migrate/20241021171714_create_moodle_calendar_events.rb b/db/migrate/20241021171714_create_moodle_calendar_events.rb index 4e902427..60f70a92 100644 --- a/db/migrate/20241021171714_create_moodle_calendar_events.rb +++ b/db/migrate/20241021171714_create_moodle_calendar_events.rb @@ -3,6 +3,7 @@ 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 diff --git a/db/schema.rb b/db/schema.rb index 64d80c07..8808d1ca 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -102,6 +102,7 @@ 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 From fc84e1693f6966d97e5996267de872bc7498753f Mon Sep 17 00:00:00 2001 From: paolahoff Date: Tue, 29 Oct 2024 11:26:45 -0300 Subject: [PATCH 08/10] feat [JOB]: new job to update recurring meetings start date - The job also checks for the need to create a new event in Moodle Calendar, in order to keep recurrence for that scheduled meeting --- app/controllers/rooms_controller.rb | 1 - app/jobs/update_recurring_meetings_job.rb | 31 +++++++++++++++++++++++ config/jobs_schedule.yml | 7 +++++ lib/moodle.rb | 3 ++- 4 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 app/jobs/update_recurring_meetings_job.rb diff --git a/app/controllers/rooms_controller.rb b/app/controllers/rooms_controller.rb index 9418efec..4b7fc9d1 100644 --- a/app/controllers/rooms_controller.rb +++ b/app/controllers/rooms_controller.rb @@ -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 diff --git a/app/jobs/update_recurring_meetings_job.rb b/app/jobs/update_recurring_meetings_job.rb new file mode 100644 index 00000000..86dd02d8 --- /dev/null +++ b/app/jobs/update_recurring_meetings_job.rb @@ -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 diff --git a/config/jobs_schedule.yml b/config/jobs_schedule.yml index da0b73a2..eec365c4 100644 --- a/config/jobs_schedule.yml +++ b/config/jobs_schedule.yml @@ -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." diff --git a/lib/moodle.rb b/lib/moodle.rb index 02b19b47..b3e744d5 100644 --- a/lib/moodle.rb +++ b/lib/moodle.rb @@ -31,7 +31,8 @@ def self.create_calendar_event(moodle_token, sched_meeting_hash_id, scheduled_me else # Create a new Moodle Calendar Event event_params = { event_id: result["events"].first['id'], - scheduled_meeting_hash_id: sched_meeting_hash_id } + scheduled_meeting_hash_id: sched_meeting_hash_id, + start_at: scheduled_meeting.start_at } MoodleCalendarEvent.create!(event_params) end From fb37e9a32b86b61f3bb7186fca59c448a07c368c Mon Sep 17 00:00:00 2001 From: paolahoff Date: Mon, 4 Nov 2024 09:36:06 -0300 Subject: [PATCH 09/10] fix: introduce a delay between recurring calls to the Moodle API - The previous model of consecutive API calls without pauses was causing timeout errors --- app/jobs/create_recurring_events_in_moodle_calendar_job.rb | 1 + app/jobs/delete_recurring_events_in_moodle_calendar_job.rb | 1 + app/jobs/update_recurring_events_in_moodle_calendar_job.rb | 2 ++ lib/moodle.rb | 2 +- 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/jobs/create_recurring_events_in_moodle_calendar_job.rb b/app/jobs/create_recurring_events_in_moodle_calendar_job.rb index 6f299604..5ee3891f 100644 --- a/app/jobs/create_recurring_events_in_moodle_calendar_job.rb +++ b/app/jobs/create_recurring_events_in_moodle_calendar_job.rb @@ -9,6 +9,7 @@ def perform(moodle_token, scheduled_meeting, context_id, opts={}) 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 diff --git a/app/jobs/delete_recurring_events_in_moodle_calendar_job.rb b/app/jobs/delete_recurring_events_in_moodle_calendar_job.rb index 4f6d27a6..dd1ff77a 100644 --- a/app/jobs/delete_recurring_events_in_moodle_calendar_job.rb +++ b/app/jobs/delete_recurring_events_in_moodle_calendar_job.rb @@ -10,6 +10,7 @@ def perform(moodle_token, calendar_events_ids, context_id, opts={}) 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 diff --git a/app/jobs/update_recurring_events_in_moodle_calendar_job.rb b/app/jobs/update_recurring_events_in_moodle_calendar_job.rb index a4309faf..b935aef3 100644 --- a/app/jobs/update_recurring_events_in_moodle_calendar_job.rb +++ b/app/jobs/update_recurring_events_in_moodle_calendar_job.rb @@ -12,6 +12,8 @@ def perform(moodle_token, scheduled_meeting, calendar_events_ids, context_id, op 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 diff --git a/lib/moodle.rb b/lib/moodle.rb index b3e744d5..0b396015 100644 --- a/lib/moodle.rb +++ b/lib/moodle.rb @@ -75,7 +75,7 @@ def self.update_calendar_event_day(moodle_token, event_id, new_start_at, context end def self.delete_calendar_event(moodle_token, event_id, context_id, opts) - Rails.logger.info("Deleting event `#{event_id}`") + Rails.logger.info("[MOODLE API] Deleting event=`#{event_id}`") params = { wstoken: moodle_token.token, From 4cfa586dee73a61dfc7d754f5beb17e96b851f04 Mon Sep 17 00:00:00 2001 From: paolahoff Date: Mon, 4 Nov 2024 09:41:47 -0300 Subject: [PATCH 10/10] fix: update events when meeting's recurrence has change - If the sched meeting has lost recurrence, its recurring events are deleted in Moodle Calendar - If the sched meeting has become recurrent, its previous single event are deleted and new recurring events are created in Moodle Calendar --- .../scheduled_meetings_controller.rb | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/app/controllers/scheduled_meetings_controller.rb b/app/controllers/scheduled_meetings_controller.rb index af4a8e01..a000e48a 100644 --- a/app/controllers/scheduled_meetings_controller.rb +++ b/app/controllers/scheduled_meetings_controller.rb @@ -156,6 +156,7 @@ 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) @@ -171,15 +172,29 @@ def update end if valid_start_at && @scheduled_meeting.update(scheduled_meeting_params(@room)) - moodle_calendar_events_ids = {} 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) - if @room.can_update_moodle_calendar_event && moodle_calendar_events_ids.any? && changed_start_day + 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 @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}) + 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