koalagator 5.0.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 19e11215c36263be21f40683e72b114898c9d61c65961239f5d4edb91d8fc9c4
4
- data.tar.gz: 895412de6ad7a58706dfd518d291dd0161dea28bc9c5e58543a3c647fa67e0d4
3
+ metadata.gz: ff66a4e6b8b4b8d8d8ec7380366f00970a540e62a3151989b5a38436b2405a32
4
+ data.tar.gz: b1fff8d25d098776ead6e5485c18c064a9466bfb59ef43b17b0b4387391bd112
5
5
  SHA512:
6
- metadata.gz: 3d77eba41204db9a67ca1c8823895559175207ab4adaa821979f686beb47dd0e1b24f60f611e64621717cbc9ab0de059925f65a1f87c0057668816e92b9a029f
7
- data.tar.gz: 9bb30e3187b5db345e6239715f1f7f26687d6ae0cd428c7e84352933b7fa243e7b6e8f110d24db4fd4e20e8077039f0145ab0875d0b944106432809f1c19b806
6
+ metadata.gz: 19a313f77215a0546e97cee14916c5b5d5f14c11eaa081abff80fa721eebc8be19f3d8a2f6f0eb71e8b1b4c29d8bcac43133e376ee2708ac5cee59a8a7dadfa6
7
+ data.tar.gz: 1416ae2d0f5f30eb0d44c829cffd29fc081e22160c5cba1934c6cc0852657e7bb39bdd5b998c5e5a8f3c63e03e99f8e9646203ae797b0a3f1bf883112249eb84
data/README.md CHANGED
@@ -17,6 +17,8 @@ Thank you to everyone who contributed to the original calagator project (you're
17
17
  We appreciate the encouragment from the ruby community for us to undertaking a revivial of this project.
18
18
  Koalagator is a hard community fork of calagator. We've upgraded the project from Rails 4.2 to Rails 7.1 as part of this revival.
19
19
 
20
+ See also our [Koalagator Browser Extension for Firefox/Chrome](https://github.com/koalagator/koalagator-browser-extension) it supports most major event sites and enables quick and easy one-click adding of events to your instance.
21
+
20
22
  About the revival
21
23
  -----------------
22
24
 
@@ -727,4 +727,8 @@ fieldset .no_label, fieldset .help {
727
727
  #curation_description {
728
728
  max-width: 800px;
729
729
  margin-left: 0px;
730
+ }
731
+
732
+ a.orange {
733
+ color: orange;
730
734
  }
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+ require "calagator/import_events_csv"
5
+
6
+ module Calagator
7
+ module Admin
8
+ class BulkImportsController < Calagator::ApplicationController
9
+ require_admin
10
+
11
+ def new
12
+ end
13
+
14
+ def create
15
+ # example code via: https://guides.rubyonrails.org/form_helpers.html#uploading-files
16
+
17
+ @uploaded_file = params[:csv_file]
18
+
19
+ if @uploaded_file.present?
20
+ csv_data = CSV.parse(@uploaded_file.read, headers: true)
21
+ @bulk_import = Calagator::BulkImport.new(csv_data)
22
+ @bulk_import.process_csv_file(csv_data)
23
+ else
24
+ Rails.logger.info "upload_file not present"
25
+ end
26
+
27
+ respond_to do |format|
28
+ if @bulk_import&.errors&.empty?
29
+ format.html { redirect_to events_path(order: "created_at"), notice: "Successfully imported all records." }
30
+ else
31
+ format.html { render :new, status: :unprocessable_entity }
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -8,6 +8,7 @@ module Calagator
8
8
  class EventsController < Calagator::ApplicationController
9
9
  # Provides #duplicates and #squash_many_duplicates
10
10
  include Calagator::DuplicateChecking::ControllerActions
11
+
11
12
  require_admin only: %i[duplicates squash_many_duplicates]
12
13
 
13
14
  authorize_resource :events, only: %i[new edit create update destroy clone]
@@ -24,6 +25,15 @@ module Calagator
24
25
  render_events @events
25
26
  end
26
27
 
28
+ # GET /events/tabular
29
+ # GET /events/tabular.xml
30
+ def tabular
31
+ @browse = Event::Browse.new(params)
32
+ @events = @browse.events
33
+ @browse.errors.each { |error| append_flash :failure, error }
34
+ render_events @events
35
+ end
36
+
27
37
  # GET /events/1
28
38
  # GET /events/1.xml
29
39
  def show
@@ -6,6 +6,7 @@ module Calagator
6
6
  class VenuesController < Calagator::ApplicationController
7
7
  # Provides #duplicates and #squash_many_duplicates
8
8
  include DuplicateChecking::ControllerActions
9
+
9
10
  require_admin only: %i[duplicates squash_many_duplicates]
10
11
 
11
12
  authorize_resource :venues, only: %i[new edit create update destroy]
@@ -16,7 +16,7 @@ class PaperTrailManager
16
16
  def index
17
17
  unless change_index_allowed?
18
18
  flash[:error] = "You do not have permission to list changes."
19
- return(redirect_to root_url)
19
+ return redirect_to root_url
20
20
  end
21
21
 
22
22
  @versions = PaperTrail::Version.order("created_at DESC, id DESC")
@@ -49,12 +49,12 @@ class PaperTrailManager
49
49
  @version = PaperTrail::Version.find(params[:id])
50
50
  rescue ActiveRecord::RecordNotFound
51
51
  flash[:error] = "No such version."
52
- return(redirect_to action: :index)
52
+ return redirect_to action: :index
53
53
  end
54
54
 
55
55
  unless change_show_allowed?(@version)
56
56
  flash[:error] = "You do not have permission to show that change."
57
- return(redirect_to action: :index)
57
+ return redirect_to action: :index
58
58
  end
59
59
 
60
60
  respond_to do |format|
@@ -69,12 +69,12 @@ class PaperTrailManager
69
69
  @version = PaperTrail::Version.find(params[:id])
70
70
  rescue ActiveRecord::RecordNotFound
71
71
  flash[:error] = "No such version."
72
- return(redirect_to(changes_path))
72
+ return redirect_to(changes_path)
73
73
  end
74
74
 
75
75
  unless change_revert_allowed?(@version)
76
76
  flash[:error] = "You do not have permission to revert this change."
77
- return(redirect_to changes_path)
77
+ return redirect_to changes_path
78
78
  end
79
79
 
80
80
  if @version.event == "create"
@@ -15,7 +15,7 @@ module Calagator
15
15
  end
16
16
 
17
17
  def markdown(text)
18
- BlueCloth.new(text, relaxed: true).to_html
18
+ Kramdown::Document.new(text).to_html.rstrip
19
19
  end
20
20
 
21
21
  # Return a HTML string with the BR tags converted to XHTML compliant markup.
@@ -45,7 +45,7 @@ module Calagator
45
45
  next if flash[type].blank?
46
46
 
47
47
  content_tag(:div, class: "flash #{type} flash_#{type}") do
48
- "#{(type == :failure) ? "ERROR: " : ""}#{flash[type]}".html_safe
48
+ "#{"ERROR: " if type == :failure}#{flash[type]}".html_safe
49
49
  end
50
50
  end.compact.join.html_safe
51
51
  end
@@ -183,7 +183,8 @@ module Calagator
183
183
  "name" => "Event Name",
184
184
  "venue" => "Location",
185
185
  "score" => "Relevance",
186
- "date" => "Date"
186
+ "created_at" => "Date Added",
187
+ "date" => "Event Date"
187
188
  }.freeze
188
189
  private_constant :SORTING_LABELS
189
190
  end
@@ -0,0 +1,59 @@
1
+ module Calagator
2
+ class BulkImport
3
+ def initialize(csv_data)
4
+ @csv_data = csv_data
5
+ @errors = []
6
+ end
7
+
8
+ attr_reader :errors
9
+
10
+ def process_csv_file(csv_data)
11
+ @errors = []
12
+ processed_events = csv_data.each.with_index(2).map { |row, index| process_row(row, index) }
13
+ return false if @errors.any?
14
+ processed_events.map { |event| event.save }
15
+ end
16
+
17
+ def process_row(row, index)
18
+ venue = set_venue(row["venue"])
19
+ event = Calagator::Event.new(
20
+ title: row["title"],
21
+ venue_id: venue&.id,
22
+ url: row["url"],
23
+ start_time: set_event_at(row["event_start_at"]),
24
+ end_time: set_event_at(row["event_end_at"]),
25
+ description: row["description"],
26
+ venue_details: row["venue_details"],
27
+ tag_list: row["event_tag_list"]
28
+ )
29
+
30
+ if Calagator::Event.find_duplicate(event.title, event.start_time, event.end_time, venue).any?
31
+ return @errors << "Row #{index}: 'Duplicate of an existing event.'"
32
+ end
33
+
34
+ if event.valid?
35
+ event
36
+ else
37
+ @errors << "Row #{index}: #{event.errors.full_messages}"
38
+ end
39
+ end
40
+
41
+ def set_event_at(value)
42
+ return "" unless value.present?
43
+ Time.strptime value, "%d/%m/%Y %H:%M:%S"
44
+ end
45
+
46
+ def set_venue(value)
47
+ return "" unless value.present?
48
+ value_s = value.to_s
49
+ value_i = value_s.to_i
50
+ target_venue = if value_s.match?(/^\s*\d+\s*$/) && value_i > 0
51
+ Calagator::Venue.find_by(id: value_i)
52
+ else
53
+ Calagator::Venue.search(value_s, limit: 1).first
54
+ end
55
+ Rails.logger.info "set_venue.target_venue #{target_venue}"
56
+ target_venue
57
+ end
58
+ end
59
+ end
@@ -18,6 +18,7 @@
18
18
  module Calagator
19
19
  class Curation < Calagator::ApplicationRecord
20
20
  include EventFilterable
21
+
21
22
  scope :listed, -> { where(unlisted: false) }
22
23
 
23
24
  validates :name, :display_name, :priority, presence: true
@@ -53,7 +53,7 @@ module Calagator
53
53
  has_paper_trail
54
54
  acts_as_taggable_on :tags
55
55
 
56
- xss_foliate strip: %i[title description venue_details]
56
+ # xss_foliate strip: %i[title description venue_details]
57
57
 
58
58
  include DecodeHtmlEntitiesHack
59
59
  include ActiveModel::Serializers::Xml
@@ -76,6 +76,7 @@ module Calagator
76
76
 
77
77
  # Duplicates
78
78
  include DuplicateChecking
79
+
79
80
  duplicate_checking_ignores_attributes :source_id, :version, :venue_id, :tag_list
80
81
  duplicate_squashing_ignores_associations :tags, :base_tags, :taggings
81
82
  duplicate_finding_scope -> { future.order(:id) }
@@ -100,7 +101,9 @@ module Calagator
100
101
  end_date += 1.day if start_date == end_date
101
102
  on_or_after_date(start_date).before_date(end_date)
102
103
  }
103
-
104
+ scope :find_duplicate, ->(title, start_time, end_time, venue) {
105
+ includes(:venue).where(title: title, start_time: start_time, end_time: end_time, venue: venue)
106
+ }
104
107
  # Expand the simple sort order names from the URL into more intelligent SQL order strings
105
108
  scope :ordered_by_ui_field, lambda { |ui_field|
106
109
  scope = case ui_field
@@ -108,6 +111,8 @@ module Calagator
108
111
  order(Arel.sql("lower(events.title)"))
109
112
  when "venue"
110
113
  includes(:venue).order(Arel.sql("lower(venues.title)")).references(:venues)
114
+ when "created_at"
115
+ return order(created_at: :desc)
111
116
  else
112
117
  all
113
118
  end
@@ -79,6 +79,7 @@ module Calagator
79
79
 
80
80
  # Duplicates
81
81
  include DuplicateChecking
82
+
82
83
  duplicate_checking_ignores_attributes :source_id, :version, :closed, :wifi, :access_notes, :tag_list
83
84
  duplicate_squashing_ignores_associations :tags, :base_tags, :taggings
84
85
  duplicate_finding_scope -> { non_duplicates.order(:title, :id) }
@@ -0,0 +1,34 @@
1
+ <% if @bulk_import && @bulk_import&.errors.any? %>
2
+ <div style="color: red">
3
+ <h2><%= pluralize(@bulk_import&.errors.count, "error") %> in '<code><%= @uploaded_file.original_filename %></code>' prohibited this import from being saved:</h2>
4
+
5
+ <ul>
6
+ <% @bulk_import&.errors.each do |error| %>
7
+ <li><%= error %></li>
8
+ <% end %>
9
+ </ul>
10
+ </div>
11
+ <% end %>
12
+
13
+ <h1>Bulk Imports</h1>
14
+
15
+ <p>This feature supports bulk importing of events from a spreadsheet file</p>
16
+ <p>To work as expected the uploaded file needs to be in `.csv` format.</p>
17
+ <p>The header row in the csv file also needs to match our system defined field headings<p>
18
+ <p>We recommend you copy your spreadsheet data into this template, and upload the resultant file</p>
19
+ <p><%= link_to 'Koalagator Bulk Import Template v1.0', 'https://docs.google.com/spreadsheets/d/1QBGzaMjVCjUHQxQ3YIfX5Kivck2Rmow0IlyRAjDrbf4/edit?gid=0#gid=0' %></p>
20
+ <p>1. Open the Template. 2. File > 'Make a copy' 3. Copy paste your records in 4. Review and tidy up records for a clean import.</p>
21
+ <p>Once the sheet is ready. Download in .csv format by going: File > Download > Comma Separated Value (.csv)</p>
22
+
23
+ <p>The first version of this importer, will associate all events with a venue named 'TBA', for now its better than nothing.</p>
24
+ <p>Once imported you can go through each event and allocate to a real venue.</p>
25
+
26
+ <p>Drag and drop / attach your .csv file into the file area below: </p>
27
+
28
+ <div class="form-label"><h3>Upload Your Spreadsheet (.csv)</h3></div>
29
+
30
+ <%= form_with url: admin_bulk_imports_path, multipart: true do |form| %>
31
+ <%= file_field_tag :csv_file, accept:'.csv' %>
32
+ <br><br>
33
+ <%= form.submit "Upload attached File" %>
34
+ <% end %>
@@ -9,4 +9,6 @@
9
9
  <li><%= link_to "Manage users", admin_users_path %></li>
10
10
  <% end %>
11
11
  <li><%= link_to "Manage curations", admin_curations_path %></li>
12
+ <li><%= link_to "Tablular Events View - Designed for copy/paste into an external newsletter", '/events/tabular' %></li>
13
+ <li><%= link_to "Bulk Import from CSV File", new_admin_bulk_import_path %></li>
12
14
  </ul>
@@ -20,7 +20,8 @@ rowspans = calculate_rowspans(events)
20
20
  <th class='date'>Sort By: <%= link_to "Date", url_for(params.to_unsafe_h.merge(:order => 'date')) %></th>
21
21
  <th class='event_summary'>
22
22
  <%= events_sort_link('name') -%>,
23
- <%= events_sort_link('venue') -%>
23
+ <%= events_sort_link('venue') -%>,
24
+ <%= events_sort_link('created_at') -%>
24
25
  <%- if scores -%>,
25
26
  <%= events_sort_link('score') -%>
26
27
  <%- end -%>,
@@ -33,16 +34,20 @@ rowspans = calculate_rowspans(events)
33
34
  <% events.each_with_index do |event, index| %>
34
35
  <tr class='vevent h-event'>
35
36
  <% if rowspans[index] > 0 %>
36
- <td class='date' rowspan="<%=rowspans[index]%>">
37
- <div class='day_of_week <%= today_tomorrow_or_weekday(event).downcase -%>'>
38
- <%= today_tomorrow_or_weekday(event) %>
39
- </div>
40
- <% show_year = event.start_time.year != Time.now.year %>
41
- <%= datetime_format(event.start_time,'%b %d') -%><%= ", "+datetime_format(event.start_time,'%Y') if show_year %>
42
- </td>
37
+ <td class='date' rowspan="<%=rowspans[index]%>">
38
+ <div class='day_of_week <%= today_tomorrow_or_weekday(event).downcase -%>'>
39
+ <%= today_tomorrow_or_weekday(event) %>
40
+ </div>
41
+ <% show_year = event.start_time.year != Time.now.year %>
42
+ <%= datetime_format(event.start_time,'%b %d') -%><%= ", "+datetime_format(event.start_time,'%Y') if show_year %>
43
+ </td>
43
44
  <% end %>
44
45
  <td class='event_summary'>
45
- <a class='summary p-name u-url' href='<%= url_for event_url(event) %>' name='<%= "event-#{event.id}" %>' id='<%= "event-#{event.id}" %>'><%= event.title %></a><br />
46
+ <%= link_to event.title, event, class: "summary p-name u-url #{'orange' if event.created_at.to_date == Date.today}",
47
+ id: "event-#{event.id}",
48
+ name: "event-#{event.id}",
49
+ title: "Added #{event.created_at}"
50
+ %>
46
51
  <%= normalize_time(event, :context => event.start_time.to_date) -%>
47
52
  <% if event.venue && !event.venue.title.blank? %>
48
53
  <a class='location p-location h-card' href='<%= url_for venue_url(event.venue) %>'><%= event.venue.title -%></a>
@@ -0,0 +1,59 @@
1
+ <% links ||= {} %>
2
+ <div class='list_description'>
3
+ <h2>Viewing <strong><%= browse.events.size %></strong>
4
+ <%= browse.date ? 'filtered' : 'future' %> events
5
+ <%= events_sort_label(browse.order) %></h2>
6
+ </div>
7
+
8
+ <div id='list_filters' class='sidebar'>
9
+
10
+ <h3 class='first'>Filter:</h3>
11
+
12
+ <%= form_tag events_url, :method => 'get' do -%>
13
+
14
+ <div id="date_filter">
15
+ <h4>by date</h4>
16
+ <div id='start_calendar'>
17
+ <label for='date_start'>From</label>
18
+ <%= text_field_tag 'date[start]', browse.start_date, :id => 'date_start', :class => 'date_picker' %>
19
+ </div>
20
+ <div id='end_calendar'>
21
+ <label for='date_end'>To</label>
22
+ <%= text_field_tag 'date[end]', browse.end_date, :id => 'date_end', :class => 'date_picker' %>
23
+ </div>
24
+ </div>
25
+ <div id='time_filter'>
26
+ <h4>by time</h4>
27
+ <div id='start_time_picker'>
28
+ <label for="time_start">Begins after:</label>
29
+ <%= text_field_tag 'time[start]', browse.start_time, :id => 'filter_time_start', :class => 'time_picker_filter' %>
30
+ </div>
31
+ <div id='end_time_picker'>
32
+ <label for="time_end">Ends before:</label>
33
+ <%= text_field_tag 'time[end]', browse.end_time, :id => 'filter_time_end', :class => 'time_picker_filter' %>
34
+ </div>
35
+ </div>
36
+ <div>
37
+ <label for="commit">&nbsp;</label>
38
+ <%= submit_tag 'Filter' %>
39
+ <span class="clear_filter"><%= link_to 'Reset', events_url %></span>
40
+ </div>
41
+ <% end %>
42
+
43
+ <h3>Subscribe to</h3>
44
+ <ul>
45
+ <li><%= link_to "iCalendar feed", links[:ical] %></li>
46
+ <li><%= link_to "Atom feed", links[:atom] %></li>
47
+ <li><%= link_to "Google Calendar", links[:google] %></li>
48
+ </ul>
49
+
50
+ <h3>Export to</h3>
51
+ <ul>
52
+ <li><%= link_to "iCalendar file", links[:ical_export] %></li>
53
+ </ul>
54
+
55
+ </div>
56
+
57
+ <div class='list_items'>
58
+ <%= render :partial => 'calagator/events/tabular_table', :locals => { :events => browse.events } %>
59
+ </div>
@@ -0,0 +1,82 @@
1
+ <%-
2
+ # Arguments:
3
+ # * events => Array of Event records.
4
+ # * scores => Offer a sort by score, like for search? Default to false.
5
+
6
+ scores = defined?(scores) ? scores : false
7
+
8
+ previous_start_time = nil
9
+ #show_year ||= false
10
+ skipped = 0
11
+
12
+ # calculate rowspans array for events
13
+ # each entry is number of rows spanned by today_tomorrow_weekday entry, if any, to left of event
14
+ # entry will be > 0 for first event of day, 0 for other events
15
+ rowspans = calculate_rowspans(events)
16
+ -%>
17
+ <table class='event_table'>
18
+ <thead>
19
+ <tr>
20
+ <th class='date'>Sort By: <%= link_to "Date", url_for(params.to_unsafe_h.merge(:order => 'date')) %></th>
21
+ <th class='event_summary'>
22
+ <%= events_sort_link('name') -%>,
23
+ <%= events_sort_link('venue') -%>,
24
+ <%= events_sort_link('created_at') -%>
25
+ <%- if scores -%>,
26
+ <%= events_sort_link('score') -%>
27
+ <%- end -%>,
28
+ <%= events_sort_link(nil) -%>
29
+ </th>
30
+ </tr>
31
+ </thead>
32
+ <tbody>
33
+ <% unless events.size==0 %>
34
+ <% events.each_with_index do |event, index| %>
35
+ <tr class='vevent h-event'>
36
+ <% if rowspans[index] > 0 %>
37
+ <td class='event_title rowspan="<%=rowspans[index]%>"'>
38
+ <%= link_to event.title, (event.url || event_url(event)),
39
+ class: "summary p-name u-url #{'orange' if event.created_at.to_date == Date.today}",
40
+ id: "event-#{event.id}",
41
+ name: "event-#{event.id}",
42
+ title: "Added #{event.created_at}"
43
+ %>
44
+ <%= "&nbsp;".html_safe if event&.title&.blank? %>
45
+ <br/>
46
+ </td>
47
+ <td class='event_date'>
48
+ <div class='day_of_week <%= today_tomorrow_or_weekday(event).downcase -%>'>
49
+ <%= today_tomorrow_or_weekday(event) %>
50
+ <% show_year = event.start_time.year != Time.now.year %>
51
+ <%= datetime_format(event.start_time,'%b %d') -%><%= ", "+datetime_format(event.start_time,'%Y') if show_year %>
52
+ </div>
53
+ <%= normalize_time(event, :context => event.start_time.to_date) -%>
54
+ <%= "&nbsp;".html_safe if event&.start_time&.blank? %>
55
+ </td>
56
+ <% end %>
57
+ <td class='event_location'>
58
+ <% if event.venue && !event.venue.title.blank? %>
59
+ <a class='location p-location h-card' href='<%= url_for venue_url(event.venue) %>'><%= event.venue.title %></a>
60
+ <% end -%>
61
+ <%= "&nbsp;".html_safe if event&.venue&.blank? %>
62
+ </td>
63
+ <td class='event_summary'>
64
+ <%= format_description(event.description) %>
65
+ <%= "&nbsp;".html_safe if event&.description&.blank? %>
66
+ </td>
67
+ </tr>
68
+ <% end %>
69
+ <% else %>
70
+ <tr>
71
+ <td colspan=2>No events were found.</td>
72
+ </tr>
73
+ <% end %>
74
+ <% if skipped > 0 %>
75
+ <tr>
76
+ <td colspan=2>
77
+ <%= link_to "(And #{skipped} more)", events_url %>
78
+ </td>
79
+ </tr>
80
+ <% end %>
81
+ </tbody>
82
+ </table>
@@ -0,0 +1,22 @@
1
+ <h1>Tabular Events</h1>
2
+ <p>This special events view has been added to support groups that want to copy/paste events into a newsletter or external publication.</p>
3
+ <p>Specifically it was first added to support a group that is proactivley adding multiple events from the community to then be able to pull this info back out in a useful format.</p>
4
+ <p>The events listing is split into four colummns: Title, Date Time, Venue and Description.</p>
5
+ <p>Note: Unlike elsewhere in the app, the venue title here links to the event website in the first instance.</p>
6
+ <p>If no website is included in the event record, the event title will link to the event record.</p>
7
+ <p>In this way end users of the listing of upcoming events will be linked directly to the event website where available.</p>
8
+ <p>The venue listed will always link to the instances venue record, such that accessibility details and directions are easily discoverable.</p>
9
+ <p>To copy/paste, select from the left hand side of Title the first record and drag until you are at the right hand side of the description of the last record.<p>
10
+ <p>Tip: Use the filter tool (right hand side) to narrow down the event dates before doing a copy/paste</p>
11
+
12
+ <%= content_for :title, "Events" %>
13
+
14
+ <% cache_if(@browse.default? && Calagator.cache_enabled, Calagator::CacheObserver.daily_key_for("events_index", request)) do %>
15
+ <%= render partial: "tabular", locals: {browse: @browse, links: {
16
+ ical: icalendar_feed_link,
17
+ atom: atom_feed_link,
18
+ google: google_events_subscription_link,
19
+ ical_export: icalendar_export_link
20
+ }}
21
+ %>
22
+ <% end %>
@@ -19,7 +19,7 @@ atom_feed do |feed|
19
19
  end
20
20
  end
21
21
 
22
- entry.title "#{version.event.upcase} #{version.item_type} «#{change_title_for(version)}» #{user ? "by " + user.send(PaperTrailManager.whodunnit_name_method) : ""}"
22
+ entry.title "#{version.event.upcase} #{version.item_type} «#{change_title_for(version)}» #{"by " + user.send(PaperTrailManager.whodunnit_name_method) if user}"
23
23
  entry.updated version.created_at.utc.xmlschema
24
24
 
25
25
  xm = ::Builder::XmlMarkup.new
data/config/routes.rb CHANGED
@@ -33,6 +33,7 @@ Calagator::Engine.routes.draw do
33
33
 
34
34
  namespace :admin do
35
35
  resources :curations, except: :show
36
+ resources :bulk_imports
36
37
  if Calagator.devise_enabled
37
38
  resources :users do
38
39
  get :invite, as: :invite
@@ -50,6 +51,7 @@ Calagator::Engine.routes.draw do
50
51
  get :search
51
52
  get :duplicates
52
53
  get "tag/:tag", action: :search, as: :tag
54
+ get "tabular" => "events#tabular"
53
55
  end
54
56
 
55
57
  member do
data/db/seeds.rb CHANGED
@@ -59,7 +59,7 @@ FactoryBot.define do
59
59
  description { Faker::Lorem.paragraph }
60
60
  start_time do
61
61
  [
62
- Faker::Time.between(from: 2.years.ago, to: 2.years.from_now),
62
+ Faker::Time.between(from: 2.years.ago, to: 2.years.from_now.to_datetime),
63
63
  Faker::Time.backward(days: 1),
64
64
  Faker::Time.forward(days: 1),
65
65
  Faker::Time.forward(days: 7)
@@ -15,6 +15,8 @@
15
15
  # Warning: this effectively renders loofah's "escape" scrubbing mode useless by
16
16
  # undoing everything it does. Don't use that mode.
17
17
  #
18
+ # TODO (2025): Review and remove if no longer needed
19
+ #
18
20
  module Calagator
19
21
  module DecodeHtmlEntitiesHack
20
22
  def self.included(base)
@@ -0,0 +1,123 @@
1
+ require "smarter_csv"
2
+
3
+ module Calagator
4
+ class ImportEventsCsv
5
+ attr_reader :csv, :events
6
+
7
+ def initialize(input)
8
+ @csv = SmarterCSV::Reader.new(input,
9
+ col_sep: ",",
10
+ row_sep: "\n",
11
+ user_provided_headers: %i[title description venue start_date end_date tag_list])
12
+ @events = process_events
13
+ end
14
+
15
+ def valid?
16
+ @valid
17
+ end
18
+
19
+ def import!(allow_invalid: false)
20
+ events.each do |event|
21
+ if allow_invalid
22
+ event.import
23
+ else
24
+ event.import!
25
+ end
26
+ end
27
+ end
28
+
29
+ private def process_events
30
+ event_list = []
31
+
32
+ @valid = true
33
+ csv.process do |row|
34
+ event = ImportedEvent.new(row.first)
35
+ @valid = false unless event.valid?
36
+ event_list << event
37
+ end
38
+
39
+ event_list
40
+ end
41
+
42
+ class InvalidEventError < StandardError
43
+ end
44
+
45
+ class ImportedEvent
46
+ attr_reader :data, :errors
47
+ attr_accessor :title, :description, :venue, :start_date, :end_date, :tag_list
48
+
49
+ def initialize(data)
50
+ @data = data
51
+ @errors = {}
52
+
53
+ set_title(data[:title])
54
+ set_description(data[:description])
55
+ set_venue(data[:venue])
56
+ set_start_date(data[:start_date])
57
+ set_end_date(data[:end_date])
58
+ set_tag_list(data[:tag_list])
59
+ end
60
+
61
+ def valid?
62
+ @errors.empty?
63
+ end
64
+
65
+ def import
66
+ return false if !valid?
67
+
68
+ Calagator::Event.create(title: title, description: description, venue: venue, start_date: start_date, end_date: end_date, tag_list: tag_list)
69
+ end
70
+
71
+ def import!
72
+ result = import
73
+ raise InvalidEventError unless result
74
+
75
+ result
76
+ end
77
+
78
+ private
79
+
80
+ def set_title(value)
81
+ return errors[:title] = "is empty" unless value.present?
82
+
83
+ @title = value.to_s
84
+ end
85
+
86
+ def set_description(value)
87
+ @description = value.to_s
88
+ end
89
+
90
+ def set_venue(value)
91
+ return errors[:venue] = "is empty" unless value.present?
92
+
93
+ value_s = value.to_s
94
+ value_i = value_s.to_i
95
+ target_venue = if value_s.match?(/^\s*\d+\s*$/) && value_i > 0
96
+ Calagator::Venue.find_by(id: value_i)
97
+ else
98
+ Calagator::Venue.search(value_s, limit: 1).first
99
+ end
100
+
101
+ return errors[:venue] = "cannot be found" unless target_venue.present?
102
+
103
+ @venue = target_venue
104
+ end
105
+
106
+ def set_start_date(value)
107
+ @start_date = Time.iso8601(value)
108
+ rescue
109
+ errors[:start_date] = "is not in ISO 8601 time format"
110
+ end
111
+
112
+ def set_end_date(value)
113
+ @end_date = Time.iso8601(value)
114
+ rescue
115
+ errors[:end_date] = "is not in ISO 8601 time format"
116
+ end
117
+
118
+ def set_tag_list(value)
119
+ @tag_list = value
120
+ end
121
+ end
122
+ end
123
+ end
@@ -3,7 +3,7 @@
3
3
  module Calagator
4
4
  NAME = "Koalagator"
5
5
  GEMSPEC = "koalagator"
6
- VERSION = "5.0.0"
7
- RAILS_VERSION = "~> 7.1"
6
+ VERSION = "5.1.0"
7
+ RAILS_VERSION = "~> 7.2.2"
8
8
  MAJOR_RUBY_VERSION = RUBY_VERSION[0].to_i
9
9
  end
data/lib/koalagator.rb CHANGED
@@ -16,7 +16,6 @@ require "will_paginate/array"
16
16
  require "rest-client"
17
17
  require "loofah"
18
18
  require "loofah-activerecord"
19
- require "bluecloth"
20
19
  require "acts-as-taggable-on"
21
20
  require "jquery-rails"
22
21
  require "jquery-ui-rails"
@@ -26,6 +25,7 @@ require "utf8-cleaner"
26
25
  require "lucene_query"
27
26
  require "rack/contrib/jsonp"
28
27
  require "devise"
28
+ require "kramdown"
29
29
 
30
30
  module Calagator
31
31
  mattr_accessor :title,
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: koalagator
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.0
4
+ version: 5.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - the Koalagator team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-04-01 00:00:00.000000000 Z
11
+ date: 2026-04-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '7.1'
19
+ version: 7.2.2
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '7.1'
26
+ version: 7.2.2
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: sprockets-rails
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '10.0'
61
+ version: '11.0'
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '10.0'
68
+ version: '11.0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: annotate
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -86,34 +86,20 @@ dependencies:
86
86
  - - "<"
87
87
  - !ruby/object:Gem::Version
88
88
  version: 3.3.0
89
- - !ruby/object:Gem::Dependency
90
- name: bluecloth
91
- requirement: !ruby/object:Gem::Requirement
92
- requirements:
93
- - - "~>"
94
- - !ruby/object:Gem::Version
95
- version: '2.2'
96
- type: :runtime
97
- prerelease: false
98
- version_requirements: !ruby/object:Gem::Requirement
99
- requirements:
100
- - - "~>"
101
- - !ruby/object:Gem::Version
102
- version: '2.2'
103
89
  - !ruby/object:Gem::Dependency
104
90
  name: bootsnap
105
91
  requirement: !ruby/object:Gem::Requirement
106
92
  requirements:
107
93
  - - "~>"
108
94
  - !ruby/object:Gem::Version
109
- version: '1.16'
95
+ version: '1.23'
110
96
  type: :runtime
111
97
  prerelease: false
112
98
  version_requirements: !ruby/object:Gem::Requirement
113
99
  requirements:
114
100
  - - "~>"
115
101
  - !ruby/object:Gem::Version
116
- version: '1.16'
102
+ version: '1.23'
117
103
  - !ruby/object:Gem::Dependency
118
104
  name: font-awesome-rails
119
105
  requirement: !ruby/object:Gem::Requirement
@@ -146,36 +132,30 @@ dependencies:
146
132
  name: geokit
147
133
  requirement: !ruby/object:Gem::Requirement
148
134
  requirements:
149
- - - ">="
150
- - !ruby/object:Gem::Version
151
- version: '1.9'
152
- - - "<"
135
+ - - "~>"
153
136
  - !ruby/object:Gem::Version
154
- version: '1.14'
137
+ version: 1.14.0
155
138
  type: :runtime
156
139
  prerelease: false
157
140
  version_requirements: !ruby/object:Gem::Requirement
158
141
  requirements:
159
- - - ">="
160
- - !ruby/object:Gem::Version
161
- version: '1.9'
162
- - - "<"
142
+ - - "~>"
163
143
  - !ruby/object:Gem::Version
164
- version: '1.14'
144
+ version: 1.14.0
165
145
  - !ruby/object:Gem::Dependency
166
146
  name: htmlentities
167
147
  requirement: !ruby/object:Gem::Requirement
168
148
  requirements:
169
149
  - - "~>"
170
150
  - !ruby/object:Gem::Version
171
- version: '4.3'
151
+ version: 4.4.2
172
152
  type: :runtime
173
153
  prerelease: false
174
154
  version_requirements: !ruby/object:Gem::Requirement
175
155
  requirements:
176
156
  - - "~>"
177
157
  - !ruby/object:Gem::Version
178
- version: '4.3'
158
+ version: 4.4.2
179
159
  - !ruby/object:Gem::Dependency
180
160
  name: jquery-rails
181
161
  requirement: !ruby/object:Gem::Requirement
@@ -204,20 +184,40 @@ dependencies:
204
184
  - - "~>"
205
185
  - !ruby/object:Gem::Version
206
186
  version: '7.0'
187
+ - !ruby/object:Gem::Dependency
188
+ name: kramdown
189
+ requirement: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - "~>"
192
+ - !ruby/object:Gem::Version
193
+ version: '2.5'
194
+ - - ">="
195
+ - !ruby/object:Gem::Version
196
+ version: 2.5.1
197
+ type: :runtime
198
+ prerelease: false
199
+ version_requirements: !ruby/object:Gem::Requirement
200
+ requirements:
201
+ - - "~>"
202
+ - !ruby/object:Gem::Version
203
+ version: '2.5'
204
+ - - ">="
205
+ - !ruby/object:Gem::Version
206
+ version: 2.5.1
207
207
  - !ruby/object:Gem::Dependency
208
208
  name: loofah
209
209
  requirement: !ruby/object:Gem::Requirement
210
210
  requirements:
211
211
  - - "~>"
212
212
  - !ruby/object:Gem::Version
213
- version: '2.0'
213
+ version: 2.25.1
214
214
  type: :runtime
215
215
  prerelease: false
216
216
  version_requirements: !ruby/object:Gem::Requirement
217
217
  requirements:
218
218
  - - "~>"
219
219
  - !ruby/object:Gem::Version
220
- version: '2.0'
220
+ version: 2.25.1
221
221
  - !ruby/object:Gem::Dependency
222
222
  name: loofah-activerecord
223
223
  requirement: !ruby/object:Gem::Requirement
@@ -272,14 +272,14 @@ dependencies:
272
272
  requirements:
273
273
  - - "~>"
274
274
  - !ruby/object:Gem::Version
275
- version: 1.18.1
275
+ version: 1.19.2
276
276
  type: :runtime
277
277
  prerelease: false
278
278
  version_requirements: !ruby/object:Gem::Requirement
279
279
  requirements:
280
280
  - - "~>"
281
281
  - !ruby/object:Gem::Version
282
- version: 1.18.1
282
+ version: 1.19.2
283
283
  - !ruby/object:Gem::Dependency
284
284
  name: paper_trail
285
285
  requirement: !ruby/object:Gem::Requirement
@@ -300,14 +300,14 @@ dependencies:
300
300
  requirements:
301
301
  - - "~>"
302
302
  - !ruby/object:Gem::Version
303
- version: '2.3'
303
+ version: 2.5.0
304
304
  type: :runtime
305
305
  prerelease: false
306
306
  version_requirements: !ruby/object:Gem::Requirement
307
307
  requirements:
308
308
  - - "~>"
309
309
  - !ruby/object:Gem::Version
310
- version: '2.3'
310
+ version: 2.5.0
311
311
  - !ruby/object:Gem::Dependency
312
312
  name: rails-observers
313
313
  requirement: !ruby/object:Gem::Requirement
@@ -398,14 +398,14 @@ dependencies:
398
398
  requirements:
399
399
  - - "~>"
400
400
  - !ruby/object:Gem::Version
401
- version: 1.43.0
401
+ version: 1.54.0
402
402
  type: :runtime
403
403
  prerelease: false
404
404
  version_requirements: !ruby/object:Gem::Requirement
405
405
  requirements:
406
406
  - - "~>"
407
407
  - !ruby/object:Gem::Version
408
- version: 1.43.0
408
+ version: 1.54.0
409
409
  - !ruby/object:Gem::Dependency
410
410
  name: utf8-cleaner
411
411
  requirement: !ruby/object:Gem::Requirement
@@ -460,14 +460,14 @@ dependencies:
460
460
  requirements:
461
461
  - - "~>"
462
462
  - !ruby/object:Gem::Version
463
- version: 2.5.0
463
+ version: 2.9.2
464
464
  type: :runtime
465
465
  prerelease: false
466
466
  version_requirements: !ruby/object:Gem::Requirement
467
467
  requirements:
468
468
  - - "~>"
469
469
  - !ruby/object:Gem::Version
470
- version: 2.5.0
470
+ version: 2.9.2
471
471
  - !ruby/object:Gem::Dependency
472
472
  name: observer
473
473
  requirement: !ruby/object:Gem::Requirement
@@ -510,6 +510,34 @@ dependencies:
510
510
  - - "~>"
511
511
  - !ruby/object:Gem::Version
512
512
  version: '2.0'
513
+ - !ruby/object:Gem::Dependency
514
+ name: smarter_csv
515
+ requirement: !ruby/object:Gem::Requirement
516
+ requirements:
517
+ - - "~>"
518
+ - !ruby/object:Gem::Version
519
+ version: '1.13'
520
+ type: :runtime
521
+ prerelease: false
522
+ version_requirements: !ruby/object:Gem::Requirement
523
+ requirements:
524
+ - - "~>"
525
+ - !ruby/object:Gem::Version
526
+ version: '1.13'
527
+ - !ruby/object:Gem::Dependency
528
+ name: csv
529
+ requirement: !ruby/object:Gem::Requirement
530
+ requirements:
531
+ - - "~>"
532
+ - !ruby/object:Gem::Version
533
+ version: 3.3.3
534
+ type: :runtime
535
+ prerelease: false
536
+ version_requirements: !ruby/object:Gem::Requirement
537
+ requirements:
538
+ - - "~>"
539
+ - !ruby/object:Gem::Version
540
+ version: 3.3.3
513
541
  - !ruby/object:Gem::Dependency
514
542
  name: rexml
515
543
  requirement: !ruby/object:Gem::Requirement
@@ -586,42 +614,42 @@ dependencies:
586
614
  requirements:
587
615
  - - "~>"
588
616
  - !ruby/object:Gem::Version
589
- version: 6.4.4
617
+ version: 6.5.1
590
618
  type: :development
591
619
  prerelease: false
592
620
  version_requirements: !ruby/object:Gem::Requirement
593
621
  requirements:
594
622
  - - "~>"
595
623
  - !ruby/object:Gem::Version
596
- version: 6.4.4
624
+ version: 6.5.1
597
625
  - !ruby/object:Gem::Dependency
598
626
  name: faker
599
627
  requirement: !ruby/object:Gem::Requirement
600
628
  requirements:
601
629
  - - "~>"
602
630
  - !ruby/object:Gem::Version
603
- version: 3.5.1
631
+ version: 3.6.1
604
632
  type: :development
605
633
  prerelease: false
606
634
  version_requirements: !ruby/object:Gem::Requirement
607
635
  requirements:
608
636
  - - "~>"
609
637
  - !ruby/object:Gem::Version
610
- version: 3.5.1
638
+ version: 3.6.1
611
639
  - !ruby/object:Gem::Dependency
612
640
  name: gem-release
613
641
  requirement: !ruby/object:Gem::Requirement
614
642
  requirements:
615
643
  - - "~>"
616
644
  - !ruby/object:Gem::Version
617
- version: '2.0'
645
+ version: 2.2.4
618
646
  type: :development
619
647
  prerelease: false
620
648
  version_requirements: !ruby/object:Gem::Requirement
621
649
  requirements:
622
650
  - - "~>"
623
651
  - !ruby/object:Gem::Version
624
- version: '2.0'
652
+ version: 2.2.4
625
653
  - !ruby/object:Gem::Dependency
626
654
  name: puma
627
655
  requirement: !ruby/object:Gem::Requirement
@@ -642,14 +670,14 @@ dependencies:
642
670
  requirements:
643
671
  - - "~>"
644
672
  - !ruby/object:Gem::Version
645
- version: 1.2.1
673
+ version: 1.3.0
646
674
  type: :development
647
675
  prerelease: false
648
676
  version_requirements: !ruby/object:Gem::Requirement
649
677
  requirements:
650
678
  - - "~>"
651
679
  - !ruby/object:Gem::Version
652
- version: 1.2.1
680
+ version: 1.3.0
653
681
  - !ruby/object:Gem::Dependency
654
682
  name: rspec-collection_matchers
655
683
  requirement: !ruby/object:Gem::Requirement
@@ -762,6 +790,46 @@ dependencies:
762
790
  - - "~>"
763
791
  - !ruby/object:Gem::Version
764
792
  version: '3.5'
793
+ - !ruby/object:Gem::Dependency
794
+ name: better_errors
795
+ requirement: !ruby/object:Gem::Requirement
796
+ requirements:
797
+ - - "~>"
798
+ - !ruby/object:Gem::Version
799
+ version: '2.10'
800
+ - - ">="
801
+ - !ruby/object:Gem::Version
802
+ version: 2.10.1
803
+ type: :development
804
+ prerelease: false
805
+ version_requirements: !ruby/object:Gem::Requirement
806
+ requirements:
807
+ - - "~>"
808
+ - !ruby/object:Gem::Version
809
+ version: '2.10'
810
+ - - ">="
811
+ - !ruby/object:Gem::Version
812
+ version: 2.10.1
813
+ - !ruby/object:Gem::Dependency
814
+ name: binding_of_caller
815
+ requirement: !ruby/object:Gem::Requirement
816
+ requirements:
817
+ - - "~>"
818
+ - !ruby/object:Gem::Version
819
+ version: '1.0'
820
+ - - ">="
821
+ - !ruby/object:Gem::Version
822
+ version: 1.0.1
823
+ type: :development
824
+ prerelease: false
825
+ version_requirements: !ruby/object:Gem::Requirement
826
+ requirements:
827
+ - - "~>"
828
+ - !ruby/object:Gem::Version
829
+ version: '1.0'
830
+ - - ">="
831
+ - !ruby/object:Gem::Version
832
+ version: 1.0.1
765
833
  description: Koalagator is an open source community calendaring platform
766
834
  email:
767
835
  - info@calagator.org
@@ -835,6 +903,7 @@ files:
835
903
  - app/assets/stylesheets/calagator/typography.scss
836
904
  - app/assets/stylesheets/calagator/utils.scss
837
905
  - app/assets/stylesheets/calagator/variables.scss
906
+ - app/controllers/calagator/admin/bulk_imports_controller.rb
838
907
  - app/controllers/calagator/admin/curations_controller.rb
839
908
  - app/controllers/calagator/admin/users_controller.rb
840
909
  - app/controllers/calagator/admin_controller.rb
@@ -863,6 +932,7 @@ files:
863
932
  - app/javascript/calagator/calendar/lib/components.js
864
933
  - app/javascript/calagator/calendar/lib/utils.js
865
934
  - app/models/calagator/application_record.rb
935
+ - app/models/calagator/bulk_import.rb
866
936
  - app/models/calagator/curation.rb
867
937
  - app/models/calagator/event.rb
868
938
  - app/models/calagator/event/browse.rb
@@ -888,6 +958,7 @@ files:
888
958
  - app/models/calagator/venue/search_engine/sql.rb
889
959
  - app/models/concerns/calagator/event_filterable.rb
890
960
  - app/observers/calagator/cache_observer.rb
961
+ - app/views/calagator/admin/bulk_imports/new.html.erb
891
962
  - app/views/calagator/admin/curations/_form.html.erb
892
963
  - app/views/calagator/admin/curations/_index.html.erb
893
964
  - app/views/calagator/admin/curations/edit.html.erb
@@ -913,6 +984,8 @@ files:
913
984
  - app/views/calagator/events/_subnav.html.erb
914
985
  - app/views/calagator/events/_subnav_custom.html.erb
915
986
  - app/views/calagator/events/_table.html.erb
987
+ - app/views/calagator/events/_tabular.html.erb
988
+ - app/views/calagator/events/_tabular_table.html.erb
916
989
  - app/views/calagator/events/duplicates.html.erb
917
990
  - app/views/calagator/events/edit.html.erb
918
991
  - app/views/calagator/events/index.atom.builder
@@ -921,6 +994,7 @@ files:
921
994
  - app/views/calagator/events/new.html.erb
922
995
  - app/views/calagator/events/search.html.erb
923
996
  - app/views/calagator/events/show.html.erb
997
+ - app/views/calagator/events/tabular.html.erb
924
998
  - app/views/calagator/shared/_calendar.html.erb
925
999
  - app/views/calagator/shared/_subnav_curations.html.erb
926
1000
  - app/views/calagator/shared/_subnav_pinned_venues.html.erb
@@ -1031,6 +1105,7 @@ files:
1031
1105
  - lib/calagator/duplicate_checking/duplicate_finder.rb
1032
1106
  - lib/calagator/duplicate_checking/duplicate_squasher.rb
1033
1107
  - lib/calagator/engine.rb
1108
+ - lib/calagator/import_events_csv.rb
1034
1109
  - lib/calagator/machine_tag.rb
1035
1110
  - lib/calagator/strip_whitespace.rb
1036
1111
  - lib/calagator/url_prefixer.rb
@@ -1080,14 +1155,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
1080
1155
  requirements:
1081
1156
  - - ">="
1082
1157
  - !ruby/object:Gem::Version
1083
- version: 2.6.0
1158
+ version: 3.1.0
1084
1159
  required_rubygems_version: !ruby/object:Gem::Requirement
1085
1160
  requirements:
1086
1161
  - - ">="
1087
1162
  - !ruby/object:Gem::Version
1088
1163
  version: '0'
1089
1164
  requirements: []
1090
- rubygems_version: 3.5.3
1165
+ rubygems_version: 3.5.22
1091
1166
  signing_key:
1092
1167
  specification_version: 4
1093
1168
  summary: A calendar for communities