Kalender-RSVP-Beiträge

:information_source: Zusammenfassung Erstellt kurze Themenantworten für RSVP-Ereignisse
:hammer_and_wrench: Repository-Link GitHub - mariodsantana/discourse-calendar-rsvp-posts
:open_book: Installationsanleitung So installieren Sie Plugins in Discourse

Funktionen

  • Echtzeit-Benachrichtigungen – Da RSVP-Ereignisse nun einen Antwortbeitrag im Thema erstellen, löst jede RSVP Benachrichtigungen für Themenbeobachter aus
  • Minimale Unordnung – Bisherige Benachrichtigungsbeiträge werden gelöscht, sodass maximal 2 Beiträge gleichzeitig vorhanden sind (1 Verlauf + 1 neueste Benachrichtigung)
  • Vollständiger Verlauf – Alle RSVP-Aktivitäten werden mit Zeitstempeln in chronologischer Reihenfolge gespeichert
  • Entmutigt das Hin- und Herwechseln – Zeitstempel machen wiederholte RSVP-Änderungen sichtbar

Konfiguration

Die relevanten Website-Einstellungen (unter Admin > Einstellungen > Plugins) konfigurieren hauptsächlich, welche RSVP-Änderungen einen Beitrag auslösen sollen – Zusagen, Interessenten, Absagen oder Entfernung einer bestehenden Zusage – und ob bei RSVP-Änderungen für Ereignisse, die in der Vergangenheit beginnen, ausgelöst werden soll.

Die verbleibende Einstellung schaltet den Verlaufsmodus um. Wenn der Verlaufsmodus deaktiviert ist, bleibt nur der neueste Benachrichtigungsbeitrag im Thema erhalten. Wenn der Verlaufsmodus aktiviert ist, verwaltet das Plugin einen zusätzlichen „Verlaufs“-Kommentar wie folgt:

  • Bei der ersten Zusage wird ein einfacher Benachrichtigungsbeitrag erstellt, der die Zusage ankündigt
  • Bei der zweiten Zusage wird der erste Beitrag in einen zeitgestempelten Verlaufsbeitrag umgewandelt und dann ein neuer Benachrichtigungsbeitrag erstellt
  • Bei nachfolgenden Zusagen wird die Zusage an den Verlaufsbeitrag mit Zeitstempel angehängt, der vorherige Benachrichtigungsbeitrag gelöscht und ein neuer erstellt

Einstellungen

Name Beschreibung
calendar_rsvp_posts_on_new_going Beitrag bei neuer „Zusage“
calendar_rsvp_posts_on_new_interested Beitrag bei neuem „Interessent“
calendar_rsvp_posts_on_new_not_going Beitrag bei neuer „Absage“
calendar_rsvp_posts_on_removed_rsvp Beitrag, wenn eine Zusage entfernt wird
calendar_rsvp_posts_allow_past_events ob für Ereignisse gepostet werden soll, die in der Vergangenheit beginnen
calendar_rsvp_posts_enable_history einen zeitgestempelten Verlaufsbeitrag beibehalten (Standard: aktiviert)
6 „Gefällt mir“

Das wird das Eventmanagement für viele verbessern!! Vielen Dank für die Bereitstellung!!

1 „Gefällt mir“

Danke für das Plugin :+1:

nur eine kleine Frage: gibt es die Möglichkeit, die Antworten in eine Datei zu übersetzen?

Ich mag diese Idee. Ich könnte einen Link zum Verlaufspost hinzufügen, der eine CSV-Datei herunterlädt. Ist das die Art von Sache, die Sie sich vorstellen?

Ich habe mich falsch ausgedrückt :sweat_smile: , ich wollte wissen, wie man die Antwortbenachrichtigungen übersetzt, da sie auf Englisch sind und ich sie auf Französisch haben möchte.

1 „Gefällt mir“

LOL – Ich verstehe jetzt. Im Moment steht der englische Text direkt im Quellcode. Ich bin neu bei Discourse, aber ich bin sicher, dass es eine Standardmethode gibt, ein Plugin übersetzbar zu machen. Ich werde es mir ansehen, wenn ich etwas Zeit habe.

Ich werde wahrscheinlich auch den CSV-Link implementieren. :wink:

1 „Gefällt mir“

Kein Problem :wink: . Nimm dir Zeit, ich habe es nicht eilig.

OK! Neue Version im Repository. Nicht nur mit i18n, sondern auch ein Fehler wurde gefunden und behoben, der verhinderte, dass Updates gepostet wurden, wenn jemand seine Zusage zurückzog.

Lassen Sie mich wissen, wenn Sie Probleme haben!

1 „Gefällt mir“

Hallo, ich hatte ein Problem mit den Benutzern, wenn sie an einer Veranstaltung teilnehmen oder nicht. Es erschienen zwei Avatare in der Veranstaltung anstatt nur einer. Nach einem Neuladen war das Problem zwar behoben, aber es verwirrte die Benutzer. Ich habe den Code auf meiner Seite angepasst; ich weiß nicht, ob dich das interessiert.

# frozen_string_literal: true
# name: discourse-calendar-rsvp-posts
# about: Erstelle kurze Themenantworten für RSVP-Veranstaltungen
# 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

   # Hilfsfunktion zur Entscheidung, ob wir diese Veranstaltung ignorieren sollen
   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

   # Finde alle RSVP-Beiträge für eine Veranstaltung (Verlauf oder einfache Benachrichtigung)
   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

   # Finde speziell den Verlaufsbeitrag
   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

   # Finde und lösche alle Benachrichtigungsbeiträge (aber nicht den Verlaufsbeitrag)
   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: "calendar-rsvp-posts cleanup").destroy
       rescue StandardError => e
         Rails.logger.warn("calendar-rsvp-posts: konnte Benachrichtigungsbeitrag #{post.id} nicht löschen: #{e}")
       end
     end
   end

   # Erstelle einen Verlaufszeileneintrag mit Zeitstempel
   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

   # Erstelle oder aktualisiere den Inhalt des Verlaufsbeitrags
   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

   # Füge neuen Eintrag zum bestehenden Verlauf hinzu
   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

   # Konvertiere einen einfachen Benachrichtigungsbeitrag ins Verlaufsformat
   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

   # Erstelle einen Benachrichtigungsbeitrag
   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

   # Zentrale Logikmethode zur Handhabung der Erstellung/Aktualisierung von Beiträgen
   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: "calendar-rsvp-posts cleanup").destroy
         rescue StandardError => e
           Rails.logger.warn("calendar-rsvp-posts: konnte Beitrag #{post.id} nicht löschen: #{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

 # ==========================================
 # Hintergrund-Job-Definition
 # ==========================================
 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

 # ==========================================
 # Event-Handler
 # ==========================================

 # Handler für Erstellung/Aktualisierung der Teilnahme
 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 || "jemand"
     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

     # Job in die Warteschlange stellen, anstatt die Anfrage zu blockieren
     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: Handler-Fehler: #{e}")
     Rails.logger.warn(e.backtrace.join("\n"))
   end
 end

 on(:discourse_calendar_post_event_invitee_status_changed, &proc_handler)

 # Handler für explizite Löschen von Teilnehmern (entfernte 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 || "jemand"
           action_label = I18n.t('calendar_rsvp_posts.actions.removed')

           # Job in die Warteschlange stellen, anstatt die Anfrage zu blockieren
           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: after_destroy-Handler-Fehler: #{e}")
           Rails.logger.warn(e.backtrace.join("\n"))
         end
       end
     end
   end
 end
end