Post di RSVP del calendario

:information_source: Riepilogo Crea brevi risposte all’argomento per gli eventi RSVP
:hammer_and_wrench: Link Repository GitHub - mariodsantana/discourse-calendar-rsvp-posts
:open_book: Guida all’installazione Come installare i plugin in Discourse

Funzionalità

  • Notifiche in tempo reale - Poiché gli eventi RSVP creano ora un post di risposta nell’argomento, ogni RSVP attiva le notifiche per gli osservatori dell’argomento
  • Minimo ingombro - I post di notifica precedenti vengono eliminati, quindi ci sono al massimo 2 post in qualsiasi momento (1 cronologia + 1 notifica più recente)
  • Cronologia completa - Tutta l’attività RSVP viene conservata con i timestamp in ordine cronologico
  • Scoraggia il continuo cambiamento di idea - I timestamp rendono visibili le modifiche ripetute dell’RSVP

Configurazione

Le impostazioni del sito pertinenti (in Admin > Impostazioni > Plugin) configurano principalmente quali modifiche RSVP dovrebbero attivare un post: partecipazione, interessato, non partecipazione o rimozione della partecipazione esistente, e se attivare in caso di modifiche RSVP a eventi che iniziano nel passato.

L’impostazione rimanente attiva la modalità cronologia. Con la modalità cronologia disattivata, rimane solo il post di notifica più recente nell’argomento. Con la modalità cronologia attivata, il plugin gestisce un commento di “cronologia” aggiuntivo come segue:

  • Al primo RSVP, crea un semplice post di notifica che annuncia l’RSVP
  • Al secondo RSVP, trasforma il primo post in un post di cronologia con timestamp, quindi crea un nuovo post di notifica
  • Agli RSVP successivi, aggiunge l’RSVP al post di cronologia con timestamp, elimina il post di notifica precedente e ne crea uno nuovo

Impostazioni

Nome Descrizione
calendar_rsvp_posts_on_new_going pubblica per il nuovo “Partecipante”
calendar_rsvp_posts_on_new_interested pubblica per il nuovo “Interessato”
calendar_rsvp_posts_on_new_not_going pubblica per il nuovo “Non parteciperò”
calendar_rsvp_posts_on_removed_rsvp pubblica quando un RSVP viene rimosso
calendar_rsvp_posts_allow_past_events se pubblicare per eventi che iniziano nel passato
calendar_rsvp_posts_enable_history mantiene un post di cronologia con timestamp (predefinito: abilitato)
6 Mi Piace

Questo migliorerà la gestione degli eventi per molti!! Grazie mille per averlo fornito!!

1 Mi Piace

Grazie per il plugin :+1:

solo una piccola domanda: è possibile tradurre le risposte in un file?

Mi piace questa idea. Posso aggiungere un link al post della cronologia, che scaricherebbe un file CSV. È questo il tipo di cosa a cui stai pensando?

Mi sono espresso male :sweat_smile:, volevo sapere come tradurre le notifiche di risposta perché sono in inglese e vorrei metterle in italiano.

1 Mi Piace

LOL - Ora ho capito. Al momento il testo inglese è direttamente nel codice sorgente. Sono nuovo di Discourse, ma sono sicuro che ci sia un modo standard per rendere un plugin traducibile. Ci darò un’occhiata quando avrò un po’ di tempo.

Probabilmente implementerò anche il link CSV. :wink:

1 Mi Piace

Nessun problema :wink: . Prenditi il tuo tempo, non ho fretta

OK! Nuova versione disponibile nel repository. Non solo con l’internazionalizzazione (i18n), ma ho anche trovato e corretto un bug che impediva la pubblicazione degli aggiornamenti quando qualcuno annullava la propria RSVP.

Fatemi sapere se riscontrate problemi!

1 Mi Piace

Ciao, avevo un problema riguardante gli utenti quando partecipano o meno a un evento: nell’evento apparivano due avatar invece di uno, e il problema si risolveva aggiornando la pagina, ma creava confusione tra gli utenti. Ho modificato il codice dal mio lato, non so se possa interessarti.

# frozen_string_literal: true
# name: discourse-calendar-rsvp-posts
# about: Crea risposte brevi ai topic per gli eventi RSVP
# version: 0.4
# authors: Mario Santana

after_initialize do
 module ::CalendarRsvpPosts
   PLUGIN_NAME = "discourse-calendar-rsvp-posts"
   
   def self.history_marker
     I18n.t('calendar_rsvp_posts.markers.history')
   end
   
   def self.notification_marker
     I18n.t('calendar_rsvp_posts.markers.notification')
   end

   # Helper per decidere se ignorare questo evento
   def self.should_post_for_event?(event)
     return false if event.nil?
     return true if SiteSetting.calendar_rsvp_posts_allow_past_events
     return false if event.starts_at.nil?
     event.starts_at >= Time.current
   end

   # Trova eventuali post RSVP per un evento (storico o semplice notifica)
   def self.find_rsvp_posts(event)
     return [] if event.nil? || event.post.nil? || event.post.topic.nil?
     
     event.post.topic.posts
       .where(user_id: Discourse.system_user.id)
       .where("raw LIKE ? OR raw LIKE ?", 
              "%#{history_marker}%",
              "%#{notification_marker}%")
       .order(created_at: :asc)
   end

   # Trova specificamente il post dello storico
   def self.find_history_post(event)
     return nil if event.nil? || event.post.nil? || event.post.topic.nil?
     
     event.post.topic.posts
       .where(user_id: Discourse.system_user.id)
       .where("raw LIKE ?", "%#{history_marker}%")
       .order(created_at: :asc)
       .first
   end

   # Trova ed elimina tutti i post di notifica (ma non il post dello storico)
   def self.delete_notification_posts(event)
     return if event.nil? || event.post.nil? || event.post.topic.nil?
     
     notification_posts = event.post.topic.posts
       .where(user_id: Discourse.system_user.id)
       .where("raw LIKE ?", "%#{notification_marker}%")
     
     notification_posts.each do |post|
       begin
         PostDestroyer.new(Discourse.system_user, post, context: "pulizia calendar-rsvp-posts").destroy
       rescue StandardError => e
         Rails.logger.warn("calendar-rsvp-posts: impossibile eliminare il post di notifica #{post.id}: #{e}")
       end
     end
   end

   # Costruisce una riga di voce dello storico con timestamp
   def self.build_history_entry(username, action_label, extra_text = nil)
     timestamp = Time.current.strftime("%Y-%m-%d %H:%M UTC")
     entry = "- **#{timestamp}** - #{username} #{action_label}"
     entry += " (#{extra_text})" if extra_text.present?
     entry
   end

   # Costruisce o aggiorna il contenuto del post dello storico
   def self.build_history_raw(event, new_entry)
     event_title = (event.name.presence || event.post.topic.title).to_s
     parts = []
     parts << history_marker
     parts << "### #{I18n.t('calendar_rsvp_posts.history.header', event_title: event_title)}"
     parts << ""
     parts << new_entry
     parts.join("\n")
   end

   # Aggiunge una nuova voce allo storico esistente
   def self.append_to_history(existing_raw, new_entry)
     lines = existing_raw.split("\n")
     header_end_idx = lines.index { |line| line.start_with?("### RSVP History") }
     
     if header_end_idx
       insert_idx = header_end_idx + 2
       lines.insert(insert_idx, new_entry)
     else
       lines << new_entry
     end
     
     lines.join("\n")
   end

   # Converte un semplice post di notifica nel formato storico
   def self.convert_to_history(simple_post, event)
     raw = simple_post.raw
     
     going_label = I18n.t('calendar_rsvp_posts.actions.going').gsub('(', '\\(').gsub(')', '\\)')
     interested_label = I18n.t('calendar_rsvp_posts.actions.interested').gsub('(', '\\(').gsub(')', '\\)')
     not_going_label = I18n.t('calendar_rsvp_posts.actions.not_going').gsub('(', '\\(').gsub(')', '\\)')
     removed_label = I18n.t('calendar_rsvp_posts.actions.removed').gsub('(', '\\(').gsub(')', '\\)')
     
     pattern = /\*\*([^\*]+)\s+(#{Regexp.escape(going_label)}|#{Regexp.escape(interested_label)}|#{Regexp.escape(not_going_label)}|#{Regexp.escape(removed_label)})\*\*/
     match = raw.match(pattern)
     
     if match
       username = match[1]
       action = match[2]
       
       extra_match = raw.match(/\.\s+([^.]+)\.$/)
       extra_text = extra_match ? extra_match[1] : nil
       
       timestamp = simple_post.created_at.strftime("%Y-%m-%d %H:%M UTC")
       first_entry = "- **#{timestamp}** - #{username} #{action}"
       first_entry += " (#{extra_text})" if extra_text.present?
       
       event_title = (event.name.presence || event.post.topic.title).to_s
       parts = []
       parts << history_marker
       parts << "### #{I18n.t('calendar_rsvp_posts.history.header', event_title: event_title)}"
       parts << ""
       parts << first_entry
       parts.join("\n")
     else
       event_title = (event.name.presence || event.post.topic.title).to_s
       history_marker + "\n### #{I18n.t('calendar_rsvp_posts.history.header', event_title: event_title)}\n\n" + raw
     end
   end

   # Costruisce un post di notifica
   def self.build_notification_raw(username, action_label, event, extra_text = nil)
     event_title = (event.name.presence || event.post.topic.title).to_s
     extra_text_formatted = extra_text.present? ? "#{extra_text} " : ""
     
     notification_raw = notification_marker + "\n"
     notification_raw += I18n.t('calendar_rsvp_posts.notification.template', 
                               event_title: event_title,
                               extra: extra_text_formatted,
                               username: username,
                               action: action_label)
     notification_raw
   end

   # Metodo logico centrale per gestire la creazione/aggiornamento dei post
   def self.publish_rsvp_update(event, username, action_label, extra_text = nil)
     if SiteSetting.calendar_rsvp_posts_enable_history
       history_post = find_history_post(event)
       all_rsvp_posts = find_rsvp_posts(event)
       new_entry = build_history_entry(username, action_label, extra_text)

       if all_rsvp_posts.empty?
         notification_raw = build_notification_raw(username, action_label, event, extra_text)
         PostCreator.create!(
           Discourse.system_user,
           topic_id: event.post.topic_id,
           raw: notification_raw,
           skip_validations: true
         )
       elsif history_post.nil?
         first_post = all_rsvp_posts.first
         history_raw = convert_to_history(first_post, event)
         history_raw = append_to_history(history_raw, new_entry)
         
         PostRevisor.new(first_post, event.post.topic).revise!(
           Discourse.system_user,
           raw: history_raw,
           skip_validations: true,
           skip_revision: false
         )

         notification_raw = build_notification_raw(username, action_label, event, extra_text)
         PostCreator.create!(
           Discourse.system_user,
           topic_id: event.post.topic_id,
           raw: notification_raw,
           skip_validations: true
         )
       else
         updated_raw = append_to_history(history_post.raw, new_entry)
         PostRevisor.new(history_post, event.post.topic).revise!(
           Discourse.system_user,
           raw: updated_raw,
           skip_validations: true,
           skip_revision: false
         )

         delete_notification_posts(event)

         notification_raw = build_notification_raw(username, action_label, event, extra_text)
         PostCreator.create!(
           Discourse.system_user,
           topic_id: event.post.topic_id,
           raw: notification_raw,
           skip_validations: true
         )
       end
     else
       all_rsvp_posts = find_rsvp_posts(event)
       
       all_rsvp_posts.each do |post|
         begin
           PostDestroyer.new(Discourse.system_user, post, context: "pulizia calendar-rsvp-posts").destroy
         rescue StandardError => e
           Rails.logger.warn("calendar-rsvp-posts: impossibile eliminare il post #{post.id}: #{e}")
         end
       end

       notification_raw = build_notification_raw(username, action_label, event, extra_text)
       PostCreator.create!(
         Discourse.system_user,
         topic_id: event.post.topic_id,
         raw: notification_raw,
         skip_validations: true
       )
     end
   end
 end

 # ==========================================
 # Definizione del Job in Background
 # ==========================================
 module ::Jobs
   class ProcessCalendarRsvpPost < ::Jobs::Base
     def execute(args)
       event_id = args[:event_id]
       username = args[:username]
       action_label = args[:action_label]
       extra_text = args[:extra_text]

       event = DiscoursePostEvent::Event.find_by(id: event_id)
       return unless event

       ::CalendarRsvpPosts.publish_rsvp_update(event, username, action_label, extra_text)
     end
   end
 end

 # ==========================================
 # Gestori degli Eventi
 # ==========================================

 # Gestore per creazione/aggiornamento della partecipazione
 proc_handler = proc do |invitee|
   begin
     event = invitee&.event
     next if event.nil?
     next unless CalendarRsvpPosts.should_post_for_event?(event)

     going_val = DiscoursePostEvent::Invitee.statuses[:going]
     interested_val = DiscoursePostEvent::Invitee.statuses[:interested]
     not_going_val = DiscoursePostEvent::Invitee.statuses[:not_going]

     new_status = invitee.status
     prev_status =
       if invitee.respond_to?(:previous_changes) && invitee.previous_changes["status"]
         invitee.previous_changes["status"][0]
       else
         nil
       end

     next if prev_status && prev_status == new_status

     username = invitee.user&.username || "qualcuno"
     action_label = nil

     if new_status == going_val && SiteSetting.calendar_rsvp_posts_on_new_going
       action_label = I18n.t('calendar_rsvp_posts.actions.going')
     elsif new_status == interested_val && SiteSetting.calendar_rsvp_posts_on_new_interested
       action_label = I18n.t('calendar_rsvp_posts.actions.interested')
     elsif new_status == not_going_val && SiteSetting.calendar_rsvp_posts_on_new_not_going
       action_label = I18n.t('calendar_rsvp_posts.actions.not_going')
     end

     next if action_label.nil?

     extra_text = nil
     if event.max_attendees.present?
       current_going = event.going_count
       prev_going =
         if prev_status == going_val && new_status != going_val
           current_going + 1
         elsif prev_status != going_val && new_status == going_val
           current_going - 1
         else
           current_going
         end

       was_full = prev_going >= event.max_attendees
       is_full = current_going >= event.max_attendees

       if was_full && !is_full
         extra_text = I18n.t('calendar_rsvp_posts.capacity.spots_available')
       elsif !was_full && is_full
         extra_text = I18n.t('calendar_rsvp_posts.capacity.now_full')
       end
     end

     # Metti in coda il job invece di bloccare la richiesta
     Jobs.enqueue(:process_calendar_rsvp_post, 
       event_id: event.id, 
       username: username, 
       action_label: action_label, 
       extra_text: extra_text
     )

   rescue StandardError => e
     Rails.logger.warn("calendar-rsvp-posts: errore del gestore: #{e}")
     Rails.logger.warn(e.backtrace.join("\n"))
   end
 end

 on(:discourse_calendar_post_event_invitee_status_changed, &proc_handler)

 # Gestore per eliminazioni esplicite degli invitati (rimozione RSVP)
 if defined?(DiscoursePostEvent::Invitee)
   DiscoursePostEvent::Invitee.class_eval do
     after_destroy do
       event = self.event
       
       should_process = event.present? &&
                        SiteSetting.calendar_rsvp_posts_on_removed_rsvp &&
                        (SiteSetting.calendar_rsvp_posts_allow_past_events || event.starts_at.nil? || event.starts_at >= Time.current)
       
       if should_process
         begin
           username = self.user&.username || "qualcuno"
           action_label = I18n.t('calendar_rsvp_posts.actions.removed')

           # Metti in coda il job invece di bloccare la richiesta
           Jobs.enqueue(:process_calendar_rsvp_post, 
             event_id: event.id, 
             username: username, 
             action_label: action_label,
             extra_text: nil
           )
           
         rescue StandardError => e
           Rails.logger.warn("calendar-rsvp-posts: errore del gestore after_destroy: #{e}")
           Rails.logger.warn(e.backtrace.join("\n"))
         end
       end
     end
   end
 end
end
1 Mi Piace

È un’ottima idea e capisco come possa creare confusione nell’interfaccia utente. Ho aggiunto un Mutex per evitare condizioni di gara e ho pubblicato una nuova versione del plugin.

Fammi sapere cosa ne pensi!

1 Mi Piace