diff --git a/Gemfile.lock b/Gemfile.lock index 1d12bddd212637262206680f8835295343f1fbbe..fa9ba26b2fa9d5ca453aa167b0301f0380cda6e3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -383,6 +383,7 @@ PLATFORMS aarch64-linux arm64-darwin-22 arm64-darwin-23 + arm64-darwin-24 x86_64-linux DEPENDENCIES @@ -414,4 +415,4 @@ RUBY VERSION ruby 3.4.3p32 BUNDLED WITH - 2.6.2 + 2.6.7 diff --git a/app/controllers/assignments_controller.rb b/app/controllers/assignments_controller.rb index 8731d681f7edb2a190897fa09a1d8662c4a32d04..0c20e1812432e6f4cdb60bf3bbb5c81b4d45ea4a 100644 --- a/app/controllers/assignments_controller.rb +++ b/app/controllers/assignments_controller.rb @@ -97,96 +97,13 @@ class AssignmentsController < ApplicationController respond_to do |format| format.html # This will render the existing HTML view (assignments/by_user.html.erb) - format.ics do - calendar = Icalendar::Calendar.new - tz = TZInfo::Timezone.get("UTC") - calendar.add_timezone tz.ical_timezone Time.now - - # Add confirmed assignments from active conferences only - @user.assignments.joins(session: :conference).where(conferences: { active: true }).each do |assignment| - session = assignment.session - assignees = session.assignments.map { |a| a.user.name } - desc = [ - "Assignees: #{assignees.join(', ')}", - "Speakers: #{session.speakers.map(&:name).join(', ')}", - session.description - ] - desc.unshift("Filedrop has data for this session!<br>\n" + conference_session_url(session.conference, session)) if session.filedrop? - - event = Icalendar::Event.new - event.dtstart = Icalendar::Values::DateTime.new(session.starts_at, tzid: session.starts_at.time_zone.tzinfo.name) - event.dtend = Icalendar::Values::DateTime.new(session.ends_at, tzid: session.ends_at.time_zone.tzinfo.name) - event.summary = [ session.title, session.stage.name ].join(" @ ") - event.description = desc.map { |l| helpers.strip_tags(l) }.join("\n\n") - event.location = [ session.stage.name, session.conference.name ].join(" @ ") - event.created = Icalendar::Values::DateTime.new(session.created_at) - event.last_modified = Icalendar::Values::DateTime.new(session.updated_at) - event.uid = [ session.conference.slug, session.ref_id ].join("-") - event.status = "CONFIRMED" - event.append_custom_property("X-ALT-DESC;FMTTYPE=text/html", desc.join("<hr>")) - calendar.add_event(event) - end - - # Add candidate sessions from active conferences if feature flag is enabled - if include_candidate_sessions? - @user.candidates.joins(session: :conference).where(conferences: { active: true }).includes(:session).each do |candidate| - session = candidate.session - # Skip if user is already assigned to this session - next if @user.assignments.any? { |a| a.session_id == session.id } - - assignees = session.assignments.map { |a| a.user.name } - candidates = session.candidates.map { |c| c.user.name } - desc = [ - "🤔 CANDIDATE SESSION - You've expressed interest in this session", - "Current Assignees: #{assignees.any? ? assignees.join(', ') : 'None yet'}", - "Other Candidates: #{candidates.reject { |name| name == @user.name }.join(', ')}", - "Speakers: #{session.speakers.map(&:name).join(', ')}", - session.description - ] - desc.unshift("Filedrop has data for this session!<br>\n" + conference_session_url(session.conference, session)) if session.filedrop? - - event = Icalendar::Event.new - event.dtstart = Icalendar::Values::DateTime.new(session.starts_at, tzid: session.starts_at.time_zone.tzinfo.name) - event.dtend = Icalendar::Values::DateTime.new(session.ends_at, tzid: session.ends_at.time_zone.tzinfo.name) - event.summary = "[CANDIDATE] #{session.title} @ #{session.stage.name}" - event.description = desc.map { |l| helpers.strip_tags(l) }.join("\n\n") - event.location = [ session.stage.name, session.conference.name ].join(" @ ") - event.created = Icalendar::Values::DateTime.new(session.created_at) - event.last_modified = Icalendar::Values::DateTime.new(session.updated_at) - event.uid = [ session.conference.slug, session.ref_id, "candidate" ].join("-") - event.status = "TENTATIVE" - event.transp = "TRANSPARENT" # Shows as "free" time in most calendar apps - event.append_custom_property("X-ALT-DESC;FMTTYPE=text/html", desc.join("<hr>")) - calendar.add_event(event) - end - end - - # Add standby assignments from active conferences - @user.standby_assignments.joins(standby_block: :conference).where(conferences: { active: true }).each do |standby_assignment| - standby_block = standby_assignment.standby_block - other_assignees = standby_block.users.where.not(id: @user.id).map(&:name) - - desc = [ - "🛡️ STANDBY BLOCK - You are on standby for translation needs", - "Conference: #{standby_block.conference.name}", - ("Other standby translators: #{other_assignees.join(', ')}" if other_assignees.any?), - ("Notes: #{standby_block.notes}" if standby_block.notes.present?) - ].compact.reject(&:blank?) - - event = Icalendar::Event.new - event.dtstart = Icalendar::Values::DateTime.new(standby_block.starts_at, tzid: standby_block.conference.time_zone || "UTC") - event.dtend = Icalendar::Values::DateTime.new(standby_block.ends_at, tzid: standby_block.conference.time_zone || "UTC") - event.summary = "[STANDBY] Standby Block @ #{standby_block.conference.name}" - event.description = desc.join("\n\n") - event.location = standby_block.conference.name - event.created = Icalendar::Values::DateTime.new(standby_assignment.created_at) - event.last_modified = Icalendar::Values::DateTime.new(standby_assignment.updated_at) - event.uid = [ standby_block.conference.slug, "standby", standby_block.id ].join("-") - event.status = "CONFIRMED" - event.append_custom_property("X-ALT-DESC;FMTTYPE=text/html", desc.join("<hr>")) - calendar.add_event(event) - end + format.ics do |variant| + # Determine if candidates should be included based on the action name or a parameter + # For the standard .ics feed, candidates are NOT included. + # A new action/route (e.g., by_user_with_candidates) will set params[:include_candidates] + should_include_candidates = params[:include_candidates] == "true" && include_candidate_sessions? + calendar = build_ical_for_user(@user, include_candidates: should_include_candidates) calendar.publish headers["Content-Type"] = "text/calendar; charset=UTF-8" render plain: calendar.to_ical @@ -194,8 +111,137 @@ class AssignmentsController < ApplicationController end end + # Potentially a new action for the candidates feed, or reuse by_user with a param + # For simplicity, let's assume we add a new route that points to by_user + # and sets a param to differentiate. Or, we can create a new action. + # Let's create a new action for clarity, though it will largely reuse by_user's logic. + + def by_user_with_candidates + @user = User.find(params[:user_id]) + # Ensure data is loaded similar to by_user if needed for other formats, + # but for .ics, build_ical_for_user will handle queries. + @active_assignments = @user.assignments.joins(session: :conference).where(conferences: { active: true }).includes(:session, session: [ :conference, :stage ]) + @active_standby_assignments = @user.standby_assignments.joins(standby_block: :conference).where(conferences: { active: true }).includes(standby_block: :conference) + if include_candidate_sessions? + @active_candidates = @user.candidates.joins(session: :conference).where(conferences: { active: true }).includes(:session, session: [ :conference, :stage ]) + end + + respond_to do |format| + format.ics do + if include_candidate_sessions? + calendar = build_ical_for_user(@user, include_candidates: true) + calendar.publish + headers["Content-Type"] = "text/calendar; charset=UTF-8" + render plain: calendar.to_ical + else + # Or render an empty calendar, or raise an error, or redirect. + # For now, let's render an empty calendar if the feature is off but this endpoint is hit. + calendar = Icalendar::Calendar.new + calendar.publish + headers["Content-Type"] = "text/calendar; charset=UTF-8" + render plain: calendar.to_ical, status: :forbidden # Or :not_found + end + end + # Potentially handle .html differently or redirect if direct access to this action via HTML is not desired + format.html { redirect_to by_user_assignments_path(@user), notice: "To include candidates, ensure the feature is enabled and use the specific iCal link." } + end + end + + private + def build_ical_for_user(user, include_candidates: false) + calendar = Icalendar::Calendar.new + tz = TZInfo::Timezone.get("UTC") # Or user.time_zone if available and relevant + calendar.add_timezone tz.ical_timezone Time.now + + # Add confirmed assignments from active conferences only + user.assignments.joins(session: :conference).where(conferences: { active: true }).each do |assignment| + session = assignment.session + assignees = session.assignments.map { |a| a.user.name } + desc = [ + "Assignees: #{assignees.join(', ')}", + "Speakers: #{session.speakers.map(&:name).join(', ')}", + session.description + ] + desc.unshift("Filedrop has data for this session!<br>\n" + conference_session_url(session.conference, session)) if session.filedrop? + + event = Icalendar::Event.new + event.dtstart = Icalendar::Values::DateTime.new(session.starts_at, tzid: session.starts_at.time_zone.tzinfo.name) + event.dtend = Icalendar::Values::DateTime.new(session.ends_at, tzid: session.ends_at.time_zone.tzinfo.name) + event.summary = [ session.title, session.stage.name ].join(" @ ") + event.description = desc.map { |l| helpers.strip_tags(l) }.join("\n\n") + event.location = [ session.stage.name, session.conference.name ].join(" @ ") + event.created = Icalendar::Values::DateTime.new(session.created_at) + event.last_modified = Icalendar::Values::DateTime.new(session.updated_at) + event.uid = [ session.conference.slug, session.ref_id ].join("-") + event.status = "CONFIRMED" + event.append_custom_property("X-ALT-DESC;FMTTYPE=text/html", desc.join("<hr>")) + calendar.add_event(event) + end + + # Add candidate sessions from active conferences if requested and feature flag is enabled + if include_candidates && include_candidate_sessions? + user.candidates.joins(session: :conference).where(conferences: { active: true }).includes(:session).each do |candidate| + session = candidate.session + # Skip if user is already assigned to this session + next if user.assignments.any? { |a| a.session_id == session.id } + + assignees = session.assignments.map { |a| a.user.name } + candidates_list = session.candidates.map { |c| c.user.name } + desc = [ + "🤔 CANDIDATE SESSION - You've expressed interest in this session", + "Current Assignees: #{assignees.any? ? assignees.join(', ') : 'None yet'}", + "Other Candidates: #{candidates_list.reject { |name| name == user.name }.join(', ')}", + "Speakers: #{session.speakers.map(&:name).join(', ')}", + session.description + ] + desc.unshift("Filedrop has data for this session!<br>\n" + conference_session_url(session.conference, session)) if session.filedrop? + + event = Icalendar::Event.new + event.dtstart = Icalendar::Values::DateTime.new(session.starts_at, tzid: session.starts_at.time_zone.tzinfo.name) + event.dtend = Icalendar::Values::DateTime.new(session.ends_at, tzid: session.ends_at.time_zone.tzinfo.name) + event.summary = "[CANDIDATE] #{session.title} @ #{session.stage.name}" + event.description = desc.map { |l| helpers.strip_tags(l) }.join("\n\n") + event.location = [ session.stage.name, session.conference.name ].join(" @ ") + event.created = Icalendar::Values::DateTime.new(session.created_at) + event.last_modified = Icalendar::Values::DateTime.new(session.updated_at) + event.uid = [ session.conference.slug, session.ref_id, "candidate" ].join("-") + event.status = "TENTATIVE" + event.transp = "TRANSPARENT" # Shows as "free" time in most calendar apps + event.append_custom_property("X-ALT-DESC;FMTTYPE=text/html", desc.join("<hr>")) + calendar.add_event(event) + end + end + + # Add standby assignments from active conferences + user.standby_assignments.joins(standby_block: :conference).where(conferences: { active: true }).each do |standby_assignment| + standby_block = standby_assignment.standby_block + other_assignees = standby_block.users.where.not(id: user.id).map(&:name) + + desc = [ + "🛡️ STANDBY BLOCK - You are on standby for translation needs", + "Conference: #{standby_block.conference.name}", + ("Other standby translators: #{other_assignees.join(', ')}" if other_assignees.any?), + ("Notes: #{standby_block.notes}" if standby_block.notes.present?) + ].compact.reject(&:blank?) + + event = Icalendar::Event.new + event.dtstart = Icalendar::Values::DateTime.new(standby_block.starts_at, tzid: standby_block.conference.time_zone || "UTC") + event.dtend = Icalendar::Values::DateTime.new(standby_block.ends_at, tzid: standby_block.conference.time_zone || "UTC") + event.summary = "[STANDBY] Standby Block @ #{standby_block.conference.name}" + event.description = desc.join("\n\n") + event.location = standby_block.conference.name + event.created = Icalendar::Values::DateTime.new(standby_assignment.created_at) + event.last_modified = Icalendar::Values::DateTime.new(standby_assignment.updated_at) + event.uid = [ standby_block.conference.slug, "standby", standby_block.id ].join("-") + event.status = "CONFIRMED" + event.append_custom_property("X-ALT-DESC;FMTTYPE=text/html", desc.join("<hr>")) + calendar.add_event(event) + end + calendar + end + def authorize_permission super("manage_assignments") end diff --git a/app/views/assignments/by_user.html.erb b/app/views/assignments/by_user.html.erb index 05c1140c3cf2c671ea02c1af949c95e61a223d80..4fb3049b96fd65880cd7329e433bf3f4f3e2b808 100644 --- a/app/views/assignments/by_user.html.erb +++ b/app/views/assignments/by_user.html.erb @@ -7,10 +7,15 @@ <%= link_to @user.name, user_assignments_path(@user), class: "text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" %> </h1> <%= link_to user_assignments_path(@user, format: 'ics'), class: "btn btn-info-light ml-4" do %> - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 24" fill="currentColor" aria-hidden="true" class="size-5 inline-block mr-1"><path fill-rule="evenodd" d="M5.75 2a.75.75 0 01.75.75V4h7V2.75a.75.75 0 011.5 0V4h.25A2.75 2.75 0 0118 6.75v8.5A2.75 2.75 0 0115.25 18H4.75A2.75 2.75 0 012 15.25v-8.5A2.75 2.75 0 014.75 4H5V2.75A.75.75 0 015.75 2zm-1 5.5c-.69 0-1.25.56-1.25 1.25v6.5c0 .69.56 1.25 1.25 1.25h10.5c.69 0 1.25-.56 1.25-1.25v-6.5c0-.69-.56-1.25-1.25-1.25H4.75z" clip-rule="evenodd"></path></svg> iCal + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 24" fill="currentColor" aria-hidden="true" class="size-5 inline-block mr-1"><path fill-rule="evenodd" d="M5.75 2a.75.75 0 01.75.75V4h7V2.75a.75.75 0 011.5 0V4h.25A2.75 2.75 0 0118 6.75v8.5A2.75 2.75 0 0115.25 18H4.75A2.75 2.75 0 012 15.25v-8.5A2.75 2.75 0 014.75 4H5V2.75A.75.75 0 015.75 2zm-1 5.5c-.69 0-1.25.56-1.25 1.25v6.5c0 .69.56 1.25 1.25 1.25h10.5c.69 0 1.25-.56 1.25-1.25v-6.5c0-.69-.56-1.25-1.25-1.25H4.75z" clip-rule="evenodd"></path></svg> iCal (Confirmed) + <% end %> + <% if @controller.send(:include_candidate_sessions?) %> + <%= link_to user_with_candidates_assignments_path(@user, format: 'ics'), class: "btn btn-warning-light ml-2" do %> + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 24" fill="currentColor" aria-hidden="true" class="size-5 inline-block mr-1"><path fill-rule="evenodd" d="M5.75 2a.75.75 0 01.75.75V4h7V2.75a.75.75 0 011.5 0V4h.25A2.75 2.75 0 0118 6.75v8.5A2.75 2.75 0 0115.25 18H4.75A2.75 2.75 0 012 15.25v-8.5A2.75 2.75 0 014.75 4H5V2.75A.75.75 0 015.75 2zm-1 5.5c-.69 0-1.25.56-1.25 1.25v6.5c0 .69.56 1.25 1.25 1.25h10.5c.69 0 1.25-.56 1.25-1.25v-6.5c0-.69-.56-1.25-1.25-1.25H4.75z" clip-rule="evenodd"></path></svg> iCal (Incl. Unconfirmed) + <% end %> <% end %> </div> - <% if @active_candidates %> + <% if @active_candidates && @controller.send(:include_candidate_sessions?) %> <div class="flex items-center"> <label class="inline-flex items-center cursor-pointer"> <input type="checkbox" id="showCandidates" class="sr-only peer" checked> diff --git a/config/routes.rb b/config/routes.rb index 6740c3ea6f30250f102ed03446229347ebce6b6d..415e150a0432330dfca6821757ee0021be7cd0ab 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -74,6 +74,7 @@ Rails.application.routes.draw do resources :assignments, only: [ :index ] do get "for/:user_id", action: "by_user", on: :collection, as: :user + get "for/:user_id/with_candidates", action: "by_user_with_candidates", on: :collection, as: :user_with_candidates end resources :sessions, param: :ref_id