diff --git a/Gemfile b/Gemfile index 57c0a5452..c0e9ba0d9 100644 --- a/Gemfile +++ b/Gemfile @@ -52,6 +52,7 @@ gem 'sass-rails' gem 'sassc-rails' gem 'sidekiq' gem 'sidekiq-status' +gem 'simple_calendar', '~> 2.4' gem 'simple_form' gem 'simple_token_authentication' gem 'sitemap-parser' diff --git a/Gemfile.lock b/Gemfile.lock index 3466182ca..4a0331c9b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -636,6 +636,8 @@ GEM sidekiq-status (3.0.3) chronic_duration sidekiq (>= 6.0, < 8) + simple_calendar (2.4.3) + rails (>= 3.0) simple_form (5.2.0) actionpack (>= 5.2) activemodel (>= 5.2) @@ -819,6 +821,7 @@ DEPENDENCIES sassc-rails sidekiq sidekiq-status + simple_calendar (~> 2.4) simple_form simple_token_authentication simplecov @@ -840,4 +843,4 @@ DEPENDENCIES will_paginate BUNDLED WITH - 2.4.19 + 2.4.20 diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index bb6266879..4759354ed 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -71,7 +71,14 @@ function reposition_tiles(container, tileClass){ }); } -document.addEventListener("turbolinks:load", function() { +// Perform an ajax request to load the calendar and replace the contents +window.loadCalendar = function(url) { + req = $.ajax(url); + req.done((res) => eval(res)); + return true; +} + +document.addEventListener("turbolinks:load", function(e) { // Show the tab associated with the window location hash (e.g. "#packages") if (window.location.hash) { var tab = $('ul.nav a[href="' + window.location.hash + '"]'); @@ -116,6 +123,41 @@ document.addEventListener("turbolinks:load", function() { } }); + // Load event calendar when tab is shown for the first time + $('.nav li a[data-calendar]').on("show.bs.tab", function(e) { + data = e.target.dataset + // calendar has already been loaded, only perform the filter sidebar url fragment replacing + if (!data.loaded) { + let url = data.calendar; + if (date = localStorage.getItem('calendar_start_date')) { + // Only use the start date in localstorage if it is in the future + if (Date.parse(date) > Date.now() - 60*60*24*30*1000) url += '&start_date=' + date + } + + loadCalendar(url); + // avoid loading again on the second click + data.loaded = true; + } + }); + + // after switching tabs automatically update the url fragment + $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { + addTabToFilters(e.target.href.split('#').pop()); + // and reposition masonry tiles + reposition_tiles('masonry', 'masonry-brick'); + }); + + // Manually trigger bootstrap tab history (we should probably remove the dependency and reimplement in a turbolink-compatible way) + // Specialised form of https://github.com/mnarayan01/bootstrap-tab-history/blob/master/vendor/assets/javascripts/bootstrap-tab-history.js + // go through the tabs to find one which has ah ref identical to the page we have just moved to and show it + $('[data-toggle="tab"]').each(function() { + if (("#" + this.href.split("#").pop()) === window.location.hash) { + if (!("active" in this.parentElement.classList)) { + $(this).tab('show'); + } + } + }) + // Masonry $(".nav-tabs a").on("shown.bs.tab", function(e) { reposition_tiles('masonry', 'masonry-brick'); diff --git a/app/assets/javascripts/map.js b/app/assets/javascripts/map.js index 84263644f..8b145ac5e 100644 --- a/app/assets/javascripts/map.js +++ b/app/assets/javascripts/map.js @@ -42,6 +42,26 @@ var Map = { } } +/* Set all filter links to include the anchor */ +var addTabToFilters = function (tab) { + if (tab) { + $(function () { + $('.active-filters a').attr('href', function (_, oldHref) { + oldHref = oldHref.replace(/\#(.*)/g, "#" + tab); + if (oldHref.indexOf('#') == -1) + oldHref += "#" + tab; + return oldHref; + }) + $('.nav-item a').attr('href', function (_, oldHref) { + oldHref = oldHref.replace(/\#(.*)/g, "#" + tab); + if (oldHref.indexOf('#') == -1) + oldHref += "#" + tab; + return oldHref; + }); + }); + } +}; + var EventsMap = { map: null, init: function () { @@ -49,25 +69,6 @@ var EventsMap = { var element = $('[data-role="events-map"]'); if (element.length) { EventsMap.map = null; - /* Set all filter links to include the anchor */ - var addTabToFilters = function (tab) { - if (tab) { - $(function () { - $('.active-filters a').attr('href', function (_, oldHref) { - oldHref = oldHref.replace(/\#(.*)/g, "#" + tab); - if (oldHref.indexOf('#') == -1) - oldHref += "#" + tab; - return oldHref; - }) - $('.nav-item a').attr('href', function (_, oldHref) { - oldHref = oldHref.replace(/\#(.*)/g, "#" + tab); - if (oldHref.indexOf('#') == -1) - oldHref += "#" + tab; - return oldHref; - }); - }); - } - }; var getTab = function () { var tab = window.location.hash; diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index b244fdd26..c1aed5135 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -27,6 +27,7 @@ @import "jquery.qtip.min"; @import "select2"; @import "select2-bootstrap-theme"; +@import "simple_calendar"; // Sticky but not fixed footer // http://cbracco.me/css-sticky-footer-effect/ @@ -984,6 +985,23 @@ div { display: inline-block } +/* Calendar inset numbers */ +td.day { + position: relative; +} +td.day .day-number { + position: absolute; + bottom: 0px; + right: 6px; + font-size: 1.5em; + color: rgb(186, 186, 186); +} +td.day .calendar-text { + font-size: 12px; + line-height: 14px; + margin-bottom: 12px; +} + .source-log { max-height: 20pc; overflow-y: scroll; diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index cd31ba7a4..9a4f4edf1 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -13,12 +13,12 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :exception # Should allow token authentication for API calls - acts_as_token_authentication_handler_for User, except: [:index, :show, :embed, :check_exists, :handle_error, :count, + acts_as_token_authentication_handler_for User, except: [:index, :show, :embed, :calendar, :check_exists, :handle_error, :count, :redirect] #only: [:new, :create, :edit, :update, :destroy] # User auth should be required in the web interface as well; it's here rather than in routes so that it # doesn't override the token auth, above. - before_action :authenticate_user!, except: [:index, :show, :embed, :check_exists, :handle_error, :count, :redirect] + before_action :authenticate_user!, except: [:index, :show, :embed, :calendar, :check_exists, :handle_error, :count, :redirect] before_action :set_current_user # Should prevent forgery errors for JSON posts. diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index c7de6fd6d..33950c7bf 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -27,6 +27,36 @@ def index end end + # GET /events/calendar + # GET /events/calendar.js + def calendar + # set a higher limit so we ensure to have enough results to fill a month + params[:per_page] ||= 200 + @start_date = Date.parse(params.fetch(:start_date, Date.today.to_s)).beginning_of_month + + set_params + + # override @facet_params to get only events relevant for the current month view + @facet_params[:running_during] = "#{Date.today.beginning_of_day}/#{@start_date + 1.month}" + fetch_resources + events_set = @events + + @facet_params[:running_during] = "#{@start_date}/#{@start_date + 1.month}" + fetch_resources + events_set += @events + + @events = events_set.to_set.to_a + + # now customize the list by moving all events longer than 3 days into a separate array + @long_events, @events = @events.partition { |e| e.end.nil? || e.start.nil? || e.start + TeSS::Config.site.fetch(:calendar_event_maxlength, 5).to_i.days < e.end } + + respond_to do |format| + format.js + format.html + end + end + + # GET /events/1 # GET /events/1.json # GET /events/1.ics diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index fded7bf5b..8a8e40ba1 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -282,12 +282,11 @@ def collapsible_panel(title, id, &block) end end - def tab(text, icon, href, disabled: { check: false }, active: false, count: nil, activator: nil) + def tab(text, icon, href, disabled: { check: false }, active: false, count: nil, activator: nil, options: {}) classes = [] classes << 'disabled' if disabled[:check] classes << 'active' if active || activator&.check_tab(href, !disabled[:check]) content_tag(:li, class: classes.join(' ')) do - options = {} if disabled[:check] options['title'] = disabled[:message] options['data-toggle'] = 'tooltip' diff --git a/app/views/events/calendar.html.erb b/app/views/events/calendar.html.erb new file mode 100644 index 000000000..b00b6925e --- /dev/null +++ b/app/views/events/calendar.html.erb @@ -0,0 +1,4 @@ +
+ <%= render("events/partials/calendar", events: @events) %> + <%= render("events/partials/calendar_long_events", events: @long_events) %> +
diff --git a/app/views/events/calendar.js.erb b/app/views/events/calendar.js.erb new file mode 100644 index 000000000..d62c6c6d4 --- /dev/null +++ b/app/views/events/calendar.js.erb @@ -0,0 +1,4 @@ +$("#events_calendar").html("<%= escape_javascript render("events/partials/calendar", events: @events) %>"); +$("#events_calendar_long_events").html("<%= escape_javascript render("events/partials/calendar_long_events", events: @long_events) %>"); +<%# Store the start_date in LocalStorage so we can render the calendar from the correct month %> +localStorage.setItem('calendar_start_date', '<%= @start_date %>'); \ No newline at end of file diff --git a/app/views/events/index.html.erb b/app/views/events/index.html.erb index 62f1b77dc..52741d5c7 100644 --- a/app/views/events/index.html.erb +++ b/app/views/events/index.html.erb @@ -20,7 +20,9 @@ <% end %> <% content_for :display_options do %>