koalagator 4.1.0 → 5.0.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.
Files changed (134) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.md +661 -0
  3. data/README.md +74 -21
  4. data/app/assets/config/calagator/manifest.js +5 -1
  5. data/app/assets/images/external_sites/mastodon.png +0 -0
  6. data/app/assets/images/nav_marker.png +0 -0
  7. data/app/assets/javascripts/calagator/forms.js +7 -0
  8. data/app/assets/stylesheets/calagator/custom/calendar.css +137 -0
  9. data/app/assets/stylesheets/calagator/errors.css +2 -4
  10. data/app/assets/stylesheets/calagator/forms.scss +5 -0
  11. data/app/assets/stylesheets/calagator/layout.scss +31 -9
  12. data/app/assets/stylesheets/calagator/typography.scss +39 -9
  13. data/app/assets/stylesheets/calagator/utils.scss +44 -0
  14. data/app/controllers/calagator/admin/curations_controller.rb +62 -0
  15. data/app/controllers/calagator/admin/users_controller.rb +79 -0
  16. data/app/controllers/calagator/application_controller.rb +29 -5
  17. data/app/controllers/calagator/curations_controller.rb +32 -0
  18. data/app/controllers/calagator/events_controller.rb +6 -0
  19. data/app/controllers/calagator/paper_trail_manager_controller.rb +5 -0
  20. data/app/controllers/calagator/passwords_controller.rb +4 -0
  21. data/app/controllers/calagator/registrations_controller.rb +5 -0
  22. data/app/controllers/calagator/sessions_controller.rb +4 -0
  23. data/app/controllers/calagator/site_controller.rb +10 -0
  24. data/app/controllers/calagator/sources_controller.rb +2 -0
  25. data/app/controllers/calagator/venues_controller.rb +5 -1
  26. data/app/controllers/calagator/versions_controller.rb +1 -1
  27. data/app/controllers/paper_trail_manager/changes_controller.rb +16 -16
  28. data/app/helpers/calagator/application_helper.rb +32 -3
  29. data/app/helpers/calagator/time_range_helper.rb +1 -1
  30. data/app/helpers/paper_trail_manager/changes_helper.rb +7 -7
  31. data/app/javascript/calagator/calendar/calendar.js +82 -0
  32. data/app/javascript/calagator/calendar/event.js +94 -0
  33. data/app/javascript/calagator/calendar/lib/components.js +120 -0
  34. data/app/javascript/calagator/calendar/lib/utils.js +67 -0
  35. data/app/models/calagator/curation.rb +32 -0
  36. data/app/models/calagator/event/browse.rb +3 -2
  37. data/app/models/calagator/event/cloner.rb +1 -1
  38. data/app/models/calagator/event/ical_renderer.rb +1 -1
  39. data/app/models/calagator/event/overview.rb +3 -1
  40. data/app/models/calagator/event/saver.rb +6 -1
  41. data/app/models/calagator/event/search.rb +1 -1
  42. data/app/models/calagator/event/search_engine.rb +1 -1
  43. data/app/models/calagator/event.rb +22 -5
  44. data/app/models/calagator/source/parser/hcal.rb +1 -1
  45. data/app/models/calagator/source/parser.rb +3 -3
  46. data/app/models/calagator/source.rb +23 -6
  47. data/app/models/calagator/user.rb +50 -0
  48. data/app/models/calagator/venue/geocoder.rb +1 -1
  49. data/app/models/calagator/venue/search.rb +1 -1
  50. data/app/models/calagator/venue/search_engine.rb +1 -1
  51. data/app/models/calagator/venue.rb +20 -1
  52. data/app/models/concerns/calagator/event_filterable.rb +22 -0
  53. data/app/views/calagator/admin/curations/_form.html.erb +56 -0
  54. data/app/views/calagator/admin/curations/_index.html.erb +12 -0
  55. data/app/views/calagator/admin/curations/edit.html.erb +2 -0
  56. data/app/views/calagator/admin/curations/index.html.erb +21 -0
  57. data/app/views/calagator/admin/curations/new.html.erb +2 -0
  58. data/app/views/calagator/admin/index.html.erb +6 -2
  59. data/app/views/calagator/admin/users/_form.html.erb +28 -0
  60. data/app/views/calagator/admin/users/edit.html.erb +7 -0
  61. data/app/views/calagator/admin/users/index.html.erb +38 -0
  62. data/app/views/calagator/admin/users/invite.html.erb +19 -0
  63. data/app/views/calagator/admin/users/new.html.erb +3 -0
  64. data/app/views/calagator/curations/show.html.erb +17 -0
  65. data/app/views/calagator/events/_index.html.erb +59 -0
  66. data/app/views/calagator/events/_item.html.erb +10 -3
  67. data/app/views/calagator/events/_subnav.html.erb +7 -2
  68. data/app/views/calagator/events/_subnav_custom.html.erb +0 -0
  69. data/app/views/calagator/events/index.atom.builder +1 -1
  70. data/app/views/calagator/events/index.html.erb +9 -60
  71. data/app/views/calagator/events/show.html.erb +5 -3
  72. data/app/views/calagator/shared/_calendar.html.erb +7 -0
  73. data/app/views/calagator/shared/_subnav_curations.html.erb +5 -0
  74. data/app/views/calagator/shared/_subnav_pinned_venues.html.erb +5 -0
  75. data/app/views/calagator/site/_contact.html.erb +1 -0
  76. data/app/views/calagator/site/_description.html.erb +2 -2
  77. data/app/views/calagator/site/about.html.erb +9 -0
  78. data/app/views/calagator/site/closed_registrations.html.erb +2 -0
  79. data/app/views/calagator/site/embed.html.erb +16 -0
  80. data/app/views/calagator/site/index.html.erb +1 -1
  81. data/app/views/calagator/sources/index.html.erb +1 -1
  82. data/app/views/calagator/sources/show.html.erb +1 -1
  83. data/app/views/calagator/venues/_form.html.erb +5 -1
  84. data/app/views/calagator/venues/_subnav.html.erb +9 -1
  85. data/app/views/calagator/venues/_subnav_custom.html.erb +0 -0
  86. data/app/views/calagator/venues/show.html.erb +1 -1
  87. data/app/views/layouts/calagator/_devise.html.erb +17 -0
  88. data/app/views/layouts/calagator/_footer.html.erb +3 -1
  89. data/app/views/layouts/calagator/_head.html.erb +0 -0
  90. data/app/views/layouts/calagator/_header.html.erb +3 -0
  91. data/app/views/layouts/calagator/application.html.erb +3 -0
  92. data/app/views/layouts/calagator/embed.html.erb +15 -0
  93. data/app/views/paper_trail_manager/changes/_version.html.erb +5 -5
  94. data/app/views/paper_trail_manager/changes/index.atom.builder +12 -12
  95. data/bin/{calagator → koalagator} +12 -14
  96. data/config/importmap.rb +11 -0
  97. data/config/initializers/admin_user.rb +15 -0
  98. data/config/initializers/observers.rb +1 -1
  99. data/config/initializers/paper_trail_manager.rb +1 -1
  100. data/config/locales/devise.en.yml +65 -0
  101. data/config/routes.rb +26 -1
  102. data/db/migrate/20240319042449_devise_create_calagator_users.rb +43 -0
  103. data/db/migrate/20240319061154_add_admin_flag_to_calagator_user.rb +5 -0
  104. data/db/migrate/20240320043535_add_name_to_calagator_user.rb +8 -0
  105. data/db/migrate/20240322035554_add_created_by_to_records.rb +12 -0
  106. data/db/migrate/20240510051940_create_calagator_curations.rb +15 -0
  107. data/db/migrate/20240628055300_add_pinned_to_venue.rb +5 -0
  108. data/db/seeds.rb +49 -0
  109. data/lib/calagator/decode_html_entities_hack.rb +1 -1
  110. data/lib/calagator/engine.rb +16 -1
  111. data/lib/calagator/machine_tag.rb +1 -1
  112. data/lib/calagator/strip_whitespace.rb +1 -1
  113. data/lib/calagator/vcalendar.rb +4 -4
  114. data/lib/calagator/version.rb +4 -1
  115. data/lib/generators/calagator/install_generator.rb +9 -1
  116. data/lib/generators/calagator/templates/app/views/devise/registrations/edit.html.erb +48 -0
  117. data/lib/generators/calagator/templates/app/views/devise/registrations/new.html.erb +29 -0
  118. data/lib/generators/calagator/templates/config/initializers/01_calagator.rb +34 -6
  119. data/lib/generators/calagator/templates/config/initializers/04_devise.rb +314 -0
  120. data/lib/{calagator.rb → koalagator.rb} +15 -3
  121. data/lib/paper_trail_manager.rb +11 -11
  122. data/lib/theme_reader.rb +1 -1
  123. data/rails_template.rb +6 -6
  124. data/vendor/javascript/@event-calendar--core.js +10 -0
  125. data/vendor/javascript/@event-calendar--day-grid.js +2 -0
  126. data/vendor/javascript/@event-calendar--list.js +2 -0
  127. data/vendor/javascript/ical.js.js +2 -0
  128. metadata +145 -92
  129. data/MIT-LICENSE.txt +0 -23
  130. data/app/models/calagator/event/search_engine/apache_sunspot.rb +0 -106
  131. data/app/models/calagator/venue/search_engine/apache_sunspot.rb +0 -85
  132. data/lib/tasks/sunspot_reindex_calagator.rake +0 -20
  133. data/lib/tasks/sunspot_solr_restart_enhancements.rake +0 -20
  134. data/lib/wait_for_solr.rb +0 -26
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Calagator
4
+ module Admin
5
+ class UsersController < Calagator::ApplicationController
6
+ require_admin
7
+ before_action :set_user, only: %i[invite show edit update destroy]
8
+
9
+ def index
10
+ @users = User.order(created_at: :desc).paginate(page: params[:page])
11
+ end
12
+
13
+ def new
14
+ @user = User.new
15
+ end
16
+
17
+ def show
18
+ end
19
+
20
+ def create
21
+ @user = User.new(user_params)
22
+ temp_pass = SecureRandom.base36
23
+ @user.password = temp_pass
24
+ @user.password_confirmation = temp_pass
25
+
26
+ respond_to do |format|
27
+ if @user.save
28
+ token = @user.send_reset_password_instructions
29
+ format.html {
30
+ redirect_to admin_user_invite_path(@user, token: token), notice: "Successfully created user."
31
+ }
32
+ else
33
+ format.html { render :new, status: :unprocessable_entity }
34
+ end
35
+ end
36
+ end
37
+
38
+ def edit
39
+ end
40
+
41
+ def update
42
+ respond_to do |format|
43
+ if @user.update(user_params)
44
+ format.html { redirect_to admin_users_path, notice: "Successfully updated user." }
45
+ else
46
+ format.html { render :edit, status: :unprocessable_entity }
47
+ end
48
+ end
49
+ end
50
+
51
+ def destroy
52
+ respond_to do |format|
53
+ if @user.admin? || @user == current_user
54
+ format.html { redirect_to admin_users_path, notice: "Cannot delete an admin." }
55
+ else
56
+ @user.destroy
57
+ format.html { redirect_to admin_users_path, notice: "Successfully destroyed user." }
58
+ end
59
+ end
60
+ end
61
+
62
+ def invite
63
+ redirect_to admin_users_path unless params[:token].present?
64
+ end
65
+
66
+ private
67
+
68
+ def user_params
69
+ params.require(:user).permit(:email, :name, :display_name, :admin)
70
+ end
71
+
72
+ def set_user
73
+ @user = User.find(params[:user_id] || params[:id])
74
+ rescue ActiveRecord::RecordNotFound
75
+ redirect_to admin_users_path
76
+ end
77
+ end
78
+ end
79
+ end
@@ -4,7 +4,7 @@
4
4
  # Likewise, all the methods added will be available for all controllers.
5
5
 
6
6
  module Calagator
7
- class ApplicationController < ActionController::Base
7
+ class ApplicationController < ::ApplicationController
8
8
  helper Calagator::EventsHelper
9
9
  helper Calagator::ApplicationHelper
10
10
  helper Calagator::EventsHelper
@@ -33,7 +33,8 @@ module Calagator
33
33
  end
34
34
 
35
35
  def self.require_admin(options = {})
36
- return false unless Calagator.admin_username && Calagator.admin_password
36
+ return devise_require_admin(options) if Calagator.devise_enabled
37
+ return false unless Calagator.admin_username && Calagator.admin_password
37
38
  http_basic_authenticate_with(
38
39
  **options.reverse_merge(
39
40
  name: Calagator.admin_username,
@@ -43,6 +44,28 @@ module Calagator
43
44
  end
44
45
  private_class_method :require_admin
45
46
 
47
+ def self.authorize_resource(resource, options = {})
48
+ return false unless Calagator.devise_enabled
49
+ resource = resource.to_s.to_sym
50
+ return devise_require_admin(options) if Calagator.admin_resources.include?(resource)
51
+ before_action(:authenticate_user!, **options) if Calagator.user_resources.include?(resource)
52
+ end
53
+ private_class_method :authorize_resource
54
+
55
+ def self.devise_require_admin(options = {})
56
+ return false unless Calagator.devise_enabled
57
+ before_action :authenticate_user!, **options
58
+ before_action -> {
59
+ render status: 403, html: "403: Access Denied" unless current_user&.admin?
60
+ }, **options
61
+ end
62
+ private_class_method :devise_require_admin
63
+
64
+ def self.nav_section(section, options = {})
65
+ before_action -> { @nav_section = section }, **options
66
+ end
67
+ private_class_method :nav_section
68
+
46
69
  #---[ Helpers ]---------------------------------------------------------
47
70
 
48
71
  # Returns a data structure used for telling the CSS menu which part of the
@@ -50,10 +73,11 @@ module Calagator
50
73
  # and their values are either "active" or nil.
51
74
  def link_class
52
75
  @_link_class_cache ||= {
53
- events: ((controller_name == "events" ||
76
+ events: (controller_name == "events" ||
54
77
  controller_name == "sources" ||
55
- controller_name == "site") && "active"),
56
- venues: (controller_name == "venues" && "active")
78
+ controller_name == "site" ||
79
+ controller_name == "curations") && "active",
80
+ venues: controller_name == "venues" && "active"
57
81
  }
58
82
  end
59
83
  helper_method :link_class
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Calagator
4
+ class CurationsController < Calagator::ApplicationController
5
+ before_action :set_curation, only: %i[show]
6
+
7
+ def show
8
+ @browse = Event::Browse.new(params, @curation.events)
9
+ @events = @browse.events
10
+ @browse.errors.each { |error| append_flash :failure, error }
11
+ render_events @events
12
+ end
13
+
14
+ private
15
+
16
+ def set_curation
17
+ @curation = Curation.find_by_name(params[:id])
18
+ redirect_to root_path unless @curation.present?
19
+ end
20
+
21
+ def render_events(events)
22
+ respond_to do |format|
23
+ format.html # *.html.erb
24
+ format.kml # *.kml.erb
25
+ format.ics { render ics: events || Event.future.non_duplicates }
26
+ format.atom { render template: "calagator/events/index" }
27
+ format.xml { render xml: events.to_xml(root: "events", include: :venue) }
28
+ format.json { render json: events.to_json(include: :venue) }
29
+ end
30
+ end
31
+ end
32
+ end
@@ -10,8 +10,11 @@ module Calagator
10
10
  include Calagator::DuplicateChecking::ControllerActions
11
11
  require_admin only: %i[duplicates squash_many_duplicates]
12
12
 
13
+ authorize_resource :events, only: %i[new edit create update destroy clone]
13
14
  before_action :find_and_redirect_if_locked, only: %i[edit update destroy]
14
15
 
16
+ nav_section :events
17
+
15
18
  # GET /events
16
19
  # GET /events.xml
17
20
  def index
@@ -46,6 +49,9 @@ module Calagator
46
49
  # POST /events.xml
47
50
  def create
48
51
  @event = Event.new
52
+ if Calagator.devise_enabled && current_user.present?
53
+ @event.created_by = current_user
54
+ end
49
55
  create_or_update
50
56
  end
51
57
 
@@ -0,0 +1,5 @@
1
+ module Calagator
2
+ class PaperTrailManagerController < ApplicationController
3
+ authorize_resource :changes
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ module Calagator
2
+ class PasswordsController < Devise::PasswordsController
3
+ end
4
+ end
@@ -0,0 +1,5 @@
1
+ module Calagator
2
+ class RegistrationsController < Devise::RegistrationsController
3
+ before_action -> { devise_parameter_sanitizer.permit(:sign_up, keys: %i[name]) }, only: :create
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ module Calagator
2
+ class SessionsController < Devise::SessionsController
3
+ end
4
+ end
@@ -3,6 +3,9 @@
3
3
  module Calagator
4
4
  class SiteController < Calagator::ApplicationController
5
5
  # Raise exception, mostly for confirming that exception_notification works
6
+
7
+ nav_section :about, only: :about
8
+
6
9
  def omfg
7
10
  raise ArgumentError, "OMFG"
8
11
  end
@@ -12,6 +15,10 @@ module Calagator
12
15
  render plain: "hello"
13
16
  end
14
17
 
18
+ def embed
19
+ render layout: "calagator/embed"
20
+ end
21
+
15
22
  def index
16
23
  @overview = Event::Overview.new
17
24
  respond_to do |format|
@@ -34,5 +41,8 @@ module Calagator
34
41
  @url = params[:url]
35
42
  raise ArgumentError if /^javascript:/.match?(@url)
36
43
  end
44
+
45
+ def closed_registrations
46
+ end
37
47
  end
38
48
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Calagator
4
4
  class SourcesController < Calagator::ApplicationController
5
+ authorize_resource :events, only: %i[new edit create update destroy]
6
+
5
7
  # POST /import
6
8
  # POST /import.xml
7
9
  def import
@@ -8,6 +8,10 @@ module Calagator
8
8
  include DuplicateChecking::ControllerActions
9
9
  require_admin only: %i[duplicates squash_many_duplicates]
10
10
 
11
+ authorize_resource :venues, only: %i[new edit create update destroy]
12
+
13
+ nav_section :venues
14
+
11
15
  def venue
12
16
  @venue ||= params[:id] ? Venue.find(params[:id]) : Venue.new
13
17
  end
@@ -63,7 +67,7 @@ module Calagator
63
67
  private
64
68
 
65
69
  def show_all_if_not_found
66
- return if venue
70
+ nil if venue
67
71
  rescue ActiveRecord::RecordNotFound => e
68
72
  redirect_to venues_path, flash: {failure: e.to_s}
69
73
  end
@@ -8,7 +8,7 @@ module Calagator
8
8
 
9
9
  singular = @record.class.name.singularize.underscore.split("/").last
10
10
  plural = @record.class.name.pluralize.underscore.split("/").last
11
- instance_variable_set("@#{singular}", @record)
11
+ instance_variable_set(:"@#{singular}", @record)
12
12
 
13
13
  if request.xhr?
14
14
  render partial: "calagator/#{plural}/form", locals: {singular.to_sym => @record}
@@ -15,11 +15,11 @@ class PaperTrailManager
15
15
  # List changes
16
16
  def index
17
17
  unless change_index_allowed?
18
- flash[:error] = 'You do not have permission to list changes.'
18
+ flash[:error] = "You do not have permission to list changes."
19
19
  return(redirect_to root_url)
20
20
  end
21
21
 
22
- @versions = PaperTrail::Version.order('created_at DESC, id DESC')
22
+ @versions = PaperTrail::Version.order("created_at DESC, id DESC")
23
23
  @versions = @versions.where(item_type: params[:type]) if params[:type]
24
24
  @versions = @versions.where(item_id: params[:id]) if params[:id]
25
25
 
@@ -31,10 +31,10 @@ class PaperTrailManager
31
31
  @per_page = nil if @per_page.zero?
32
32
 
33
33
  @versions = if defined?(WillPaginate)
34
- @versions.paginate(page: @page, per_page: @per_page)
35
- else
36
- @versions.page(@page).per(@per_page)
37
- end
34
+ @versions.paginate(page: @page, per_page: @per_page)
35
+ else
36
+ @versions.page(@page).per(@per_page)
37
+ end
38
38
 
39
39
  respond_to do |format|
40
40
  format.html # index.html.erb
@@ -48,12 +48,12 @@ class PaperTrailManager
48
48
  begin
49
49
  @version = PaperTrail::Version.find(params[:id])
50
50
  rescue ActiveRecord::RecordNotFound
51
- flash[:error] = 'No such version.'
51
+ flash[:error] = "No such version."
52
52
  return(redirect_to action: :index)
53
53
  end
54
54
 
55
55
  unless change_show_allowed?(@version)
56
- flash[:error] = 'You do not have permission to show that change.'
56
+ flash[:error] = "You do not have permission to show that change."
57
57
  return(redirect_to action: :index)
58
58
  end
59
59
 
@@ -68,16 +68,16 @@ class PaperTrailManager
68
68
  begin
69
69
  @version = PaperTrail::Version.find(params[:id])
70
70
  rescue ActiveRecord::RecordNotFound
71
- flash[:error] = 'No such version.'
71
+ flash[:error] = "No such version."
72
72
  return(redirect_to(changes_path))
73
73
  end
74
74
 
75
75
  unless change_revert_allowed?(@version)
76
- flash[:error] = 'You do not have permission to revert this change.'
76
+ flash[:error] = "You do not have permission to revert this change."
77
77
  return(redirect_to changes_path)
78
78
  end
79
79
 
80
- if @version.event == 'create'
80
+ if @version.event == "create"
81
81
  @record = @version.item_type.constantize.find(@version.item_id)
82
82
  @result = @record.destroy
83
83
  else
@@ -86,11 +86,11 @@ class PaperTrailManager
86
86
  end
87
87
 
88
88
  if @result
89
- if @version.event == 'create'
90
- flash[:notice] = 'Rolled back newly-created record by destroying it.'
89
+ if @version.event == "create"
90
+ flash[:notice] = "Rolled back newly-created record by destroying it."
91
91
  redirect_to changes_path
92
92
  else
93
- flash[:notice] = 'Rolled back changes to this record.'
93
+ flash[:notice] = "Rolled back changes to this record."
94
94
  redirect_to change_item_url(@version)
95
95
  end
96
96
  else
@@ -103,8 +103,8 @@ class PaperTrailManager
103
103
 
104
104
  # Return the URL for the item represented by the +version+, e.g. a Company record instance referenced by a version.
105
105
  def change_item_url(version)
106
- version_type = version.item_type.underscore.split('/').last
107
- send("#{version_type}_url", version.item_id)
106
+ version_type = version.item_type.underscore.split("/").last
107
+ send(:"#{version_type}_url", version.item_id)
108
108
  rescue NoMethodError
109
109
  nil
110
110
  end
@@ -5,6 +5,10 @@ module Calagator
5
5
  module ApplicationHelper
6
6
  include TimeRangeHelper
7
7
 
8
+ def distro_name
9
+ NAME.to_s
10
+ end
11
+
8
12
  # Returns HTML string of an event or venue description for display in a view.
9
13
  def format_description(string)
10
14
  sanitize(auto_link(upgrade_br(markdown(string))))
@@ -19,6 +23,21 @@ module Calagator
19
23
  content.gsub("<br>", "<br />")
20
24
  end
21
25
 
26
+ def display_username(user)
27
+ name = sanitize(user.name)
28
+ display_name = sanitize(user.display_name)
29
+ admin_flag = (user.admin? ? "<span title=\"Administrator\">#{Calagator.admin_icon} </span>" : nil)
30
+ raw "<span title=\"@#{name}\">#{admin_flag}#{display_name}</span>"
31
+ end
32
+
33
+ def nav_section
34
+ @nav_section || :root
35
+ end
36
+
37
+ def active_on(*sections)
38
+ sections.map { |c| c.to_s }.include?(nav_section.to_s) ? "active" : nil
39
+ end
40
+
22
41
  FLASH_TYPES = %i[success failure].freeze
23
42
 
24
43
  def render_flash
@@ -48,11 +67,16 @@ module Calagator
48
67
  else
49
68
  "imported from #{link_to truncate(item.source.name, length: 40), url_for(item.source)}"
50
69
  end
70
+ creator = if item&.created_by_id?
71
+ " by:<br />#{display_username(item.created_by)}"
72
+ elsif item&.created_by_name?
73
+ " by:<br />#{CGI.escapeHTML(item.created_by_name)}"
74
+ end
51
75
  created = " <br /><strong>#{normalize_time(item.created_at, format: :html)}</strong>"
52
76
  updated = if item.updated_at > item.created_at
53
77
  " and last updated <br /><strong>#{normalize_time(item.updated_at, format: :html)}</strong>"
54
78
  end
55
- raw "This item was #{source}#{created}#{updated}."
79
+ raw "This item was #{source}#{creator}#{created}#{updated}."
56
80
  end
57
81
 
58
82
  # Caches +block+ in view only if the +condition+ is true.
@@ -65,10 +89,15 @@ module Calagator
65
89
  end
66
90
  end
67
91
 
68
- def subnav_class_for(controller_name, action_name)
92
+ def subnav_class_for(controller_name, action_name, id_name = nil)
93
+ id_fail = false
94
+ if id_name.present?
95
+ id_fail = (id_name != params[:id])
96
+ end
97
+
69
98
  css_class = "#{controller.controller_name}_#{controller.action_name}_subnav"
70
99
  if [controller.controller_name, controller.action_name] == [controller_name, action_name]
71
- css_class += " active"
100
+ css_class += " active" unless id_fail
72
101
  end
73
102
  css_class
74
103
  end
@@ -155,7 +155,7 @@ module Calagator
155
155
 
156
156
  def text
157
157
  part.parts.reduce("") do |string, (key, value)|
158
- prefix = (PREFIXES[[@last_key, key]] || PREFIXES[key])
158
+ prefix = PREFIXES[[@last_key, key]] || PREFIXES[key]
159
159
  suffix = SUFFIXES[key]
160
160
  @last_key = key
161
161
  "#{string}#{prefix}#{value}#{suffix}"
@@ -5,7 +5,7 @@ class PaperTrailManager
5
5
  # Return HTML representing the +object+, which is either its text or a stylized "nil".
6
6
  def text_or_nil(object)
7
7
  if object.nil?
8
- content_tag('em', 'nil')
8
+ content_tag("em", "nil")
9
9
  else
10
10
  h(object)
11
11
  end
@@ -28,18 +28,18 @@ class PaperTrailManager
28
28
  # }
29
29
  def changes_for(version)
30
30
  case version.event
31
- when 'create', 'update'
31
+ when "create", "update"
32
32
  return {} unless version.changeset
33
33
 
34
34
  version.changeset.inject({}) do |changes, (attr, (prev, curr))|
35
- changes.store(attr, { previous: prev, current: curr }) && changes
35
+ changes.store(attr, {previous: prev, current: curr}) && changes
36
36
  end
37
- when 'destroy'
37
+ when "destroy"
38
38
  record = version_reify(version)
39
39
  return {} unless record
40
40
 
41
41
  record.attributes.reject { |_k, v| v.nil? }.inject({}) do |changes, (attr, value)|
42
- changes.store(attr, { previous: value, current: nil }) && changes
42
+ changes.store(attr, {previous: value, current: nil}) && changes
43
43
  end
44
44
  else
45
45
  raise ArgumentError, "Unknown event: #{version.event}"
@@ -67,9 +67,9 @@ class PaperTrailManager
67
67
  # Returns HTML link for the item stored in the version, e.g. a link to a Company record stored in the version.
68
68
  def change_item_link(version)
69
69
  if (url = change_item_url(version))
70
- link_to(change_title_for(version), url, class: 'change_item')
70
+ link_to(change_title_for(version), url, class: "change_item")
71
71
  else
72
- content_tag(:span, change_title_for(version), class: 'change_item')
72
+ content_tag(:span, change_title_for(version), class: "change_item")
73
73
  end
74
74
  end
75
75
 
@@ -0,0 +1,82 @@
1
+ import {CalendarEvent} from 'calendar/event'
2
+ import {BaseComponent} from 'calendar/lib/components'
3
+ import ICAL from "ical.js";
4
+ import EventCalendar from "@event-calendar/core"
5
+ import ListView from "@event-calendar/list"
6
+ import DayGrid from "@event-calendar/day-grid"
7
+
8
+ class Calendar extends BaseComponent {
9
+ static validViews = ["dayGridMonth", "listMonth"]
10
+ static {
11
+ this.observe("src", "view")
12
+ }
13
+
14
+ calendar
15
+
16
+ constructor() {
17
+ super()
18
+
19
+ this.calendar = this._setupCalendar()
20
+ }
21
+
22
+ onSrcChanged(_, value) { this._loadICAL(value) }
23
+
24
+ onViewChanged(_, value) {
25
+ if (!Calendar.validViews.includes(value)) {
26
+ this.setAttribute('view', validViews[0])
27
+ return
28
+ }
29
+ this.calendar.setOption('view', value)
30
+ }
31
+
32
+ _loadICAL(source) {
33
+ if (source == "") { return; }
34
+ const calendar = this.calendar;
35
+
36
+ fetch(source)
37
+ .then(r => r.text())
38
+ .then(text => this._parseICAL(calendar, text))
39
+ }
40
+
41
+ _parseICAL(calendar, rawText) {
42
+ const jCal = ICAL.parse(rawText)
43
+ const parser = new ICAL.ComponentParser
44
+
45
+ const events = []
46
+ parser.onevent = (event) => {
47
+ events.push(
48
+ {
49
+ id: event.uid,
50
+ start: event.startDate.toJSDate(),
51
+ end: event.endDate.toJSDate(),
52
+ title: event.summary,
53
+ color: event.color || "#59a12d",
54
+ editable: false,
55
+ extendedProps: event
56
+ }
57
+ )
58
+ };
59
+
60
+ parser.oncomplete = () => {
61
+ calendar.setOption("events", events)
62
+ }
63
+ parser.process(jCal)
64
+ }
65
+
66
+ _setupCalendar() {
67
+ return new EventCalendar({
68
+ target: this,
69
+ props: {
70
+ plugins: [ListView, DayGrid],
71
+ options: {
72
+ view: "dayGridMonth",
73
+ events: [],
74
+ firstDay: 1,
75
+ eventContent: info => { return {domNodes: [new CalendarEvent(info.event)]} }
76
+ }
77
+ }
78
+ })
79
+ }
80
+ }
81
+
82
+ window.customElements.define("events-calendar", Calendar)
@@ -0,0 +1,94 @@
1
+ class CalendarEvent extends HTMLElement {
2
+ constructor(event) {
3
+ super()
4
+ this._setupDom(event)
5
+ }
6
+
7
+ _setupDom(event) {
8
+ console.log(event)
9
+ const body = createElement("div", "event-body")
10
+ const title = createElement("h4", "event-title")
11
+ const detailsContainer = createElement("div", "event-details-container")
12
+ const detailsArrow = createElement("div", "event-details-arrow")
13
+
14
+ title.innerText = event.title
15
+
16
+ detailsArrow.append(this._createDetails(event))
17
+ detailsContainer.append(detailsArrow)
18
+
19
+ body.append(this._createTimeLabel(event.start, event.end))
20
+ body.append(title)
21
+ this.append(body)
22
+ this.append(detailsContainer)
23
+
24
+ document.addEventListener("click", () => {
25
+ if (this.matches(":hover")) {
26
+ this.setAttribute("open", "")
27
+ } else {
28
+ this.removeAttribute("open")
29
+ }
30
+ })
31
+ }
32
+
33
+ _createTimeLabel(start, end) {
34
+ const time = createElement("time", "event-time")
35
+ time.setAttribute("datetime", start.toISOString())
36
+
37
+ let innerText = start.toLocaleTimeString(undefined, {timeStyle: "short"})
38
+ if (end) {
39
+ innerText += " - " + end.toLocaleTimeString(undefined, {timeStyle: "short"})
40
+ }
41
+
42
+ time.innerText = innerText
43
+ return time
44
+ }
45
+
46
+ _createDetails(event) {
47
+ const details = createElement("div", "event-details")
48
+ const header = createElement("h4")
49
+ const description = createElement("div", "event-description")
50
+ const link = createElement("a")
51
+
52
+ header.innerText = event.title
53
+ link.setAttribute("href", event.id)
54
+ link.setAttribute("target", "_blank")
55
+ link.innerText = "View Event"
56
+
57
+ details.append(header)
58
+
59
+ if (event.extendedProps.location) {
60
+ const address = createElement("address")
61
+ const addr = event.extendedProps.location.replaceAll("&#13;", "")
62
+ address.innerText = addr;
63
+
64
+ details.append(address)
65
+ }
66
+
67
+ if (event.extendedProps.description) {
68
+ const desc = event.extendedProps.description.replaceAll("&#13;", "").split("\n")
69
+ desc.forEach(line => {
70
+ if (line == "") { return }
71
+ const p = document.createElement("p")
72
+ p.innerText = line
73
+ description.append(p)
74
+ })
75
+ }
76
+
77
+ details.append(description)
78
+ details.append(link)
79
+
80
+ return details
81
+ }
82
+ }
83
+
84
+ function createElement(element, ...classes) {
85
+ const instance = document.createElement(element)
86
+ if (classes.length > 0) {
87
+ instance.classList.add(classes)
88
+ }
89
+ return instance
90
+ }
91
+
92
+ window.customElements.define("calendar-event", CalendarEvent)
93
+
94
+ export {CalendarEvent}