diff --git a/Gemfile b/Gemfile index b80ae29ee..5e80a7497 100644 --- a/Gemfile +++ b/Gemfile @@ -65,6 +65,8 @@ gem 'jquery-ui-rails' gem 'selectize-rails' gem 'highcharts-rails', '~> 6.0' gem 'bootstrap', '~> 4.3.1' +gem 'fullcalendar-rails' +gem 'momentjs-rails' # Markdown parsing gem 'redcarpet' diff --git a/Gemfile.lock b/Gemfile.lock index f310deea9..e5b8607c7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -129,7 +129,7 @@ GEM dotenv (= 2.7.5) railties (>= 3.2, < 6.1) errbase (0.2.0) - erubi (1.9.0) + erubi (1.10.0) erubis (2.7.0) et-orbi (1.2.4) tzinfo @@ -152,6 +152,10 @@ GEM fugit (1.3.6) et-orbi (~> 1.1, >= 1.1.8) raabro (~> 1.3) + fullcalendar-rails (3.9.0.0) + jquery-rails (>= 4.0.5, < 5.0.0) + jquery-ui-rails (>= 5.0.2) + momentjs-rails (>= 2.9.0) globalid (0.4.2) activesupport (>= 4.2.0) groupdate (5.0.0) @@ -205,7 +209,7 @@ GEM rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) ruby_dep (~> 1.2) - loofah (2.7.0) + loofah (2.8.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) lumberjack (1.2.6) @@ -226,6 +230,8 @@ GEM builder minitest (>= 5.0) ruby-progressbar + momentjs-rails (2.20.1) + railties (>= 3.1) msgpack (1.3.3) multi_json (1.15.0) multi_xml (0.6.0) @@ -233,7 +239,7 @@ GEM mustache (1.1.1) mysql2 (0.5.3) nenv (0.3.0) - nio4r (2.5.3) + nio4r (2.5.4) nokogiri (1.10.10) mini_portile2 (~> 2.4.0) notiffany (0.1.3) @@ -386,7 +392,7 @@ GEM sprockets (4.0.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (3.2.1) + sprockets-rails (3.2.2) actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) @@ -402,7 +408,7 @@ GEM turbolinks (5.2.1) turbolinks-source (~> 5.2) turbolinks-source (5.2.0) - tzinfo (1.2.7) + tzinfo (1.2.8) thread_safe (~> 0.1) uglifier (4.2.0) execjs (>= 0.3.0, < 3) @@ -451,6 +457,7 @@ DEPENDENCIES factory_bot_rails file_validators font-awesome-rails (~> 4.0) + fullcalendar-rails groupdate guard guard-minitest @@ -461,6 +468,7 @@ DEPENDENCIES jquery-ui-rails listen (>= 3.0.5, < 3.2) minitest-reporters + momentjs-rails mustache (~> 1.0) mysql2 (>= 0.4.4, < 0.6.0) omniauth-mlh (~> 0.4.1) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 8a917f699..d12456d3d 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -21,3 +21,6 @@ //= require_directory . //= require_directory ./channels //= require ./vendor/simplemde.min.js +//= require moment +//= require fullcalendar +//= require fullcalendar/locale-all diff --git a/app/assets/javascripts/events.js b/app/assets/javascripts/events.js new file mode 100644 index 000000000..b24d7946f --- /dev/null +++ b/app/assets/javascripts/events.js @@ -0,0 +1,38 @@ +function eventCalendar() { + return $('#calendar').fullCalendar({ + defaultView: 'listYear', + buttonText: { + today: 'Today' + }, + eventRender: function (event, element, view) { + var description = event.description ? event.description : ''; + var location = event.location ? event.location : ''; + element.find('.fc-event-dot').css('display','none'); + element.find('.fc-list-item-title').append('
' + description + ''); + element.find('.fc-list-item-title').append('
' + location + ''); + }, + events: { + url: '/manage/events.json', + success: function(response) { + // due to "end" being a keyword in ruby and what fullcalender uses it is stored as finish and than it is + // converted to "end" when sending it to fullcalendar + response = JSON.parse(JSON.stringify(response).split('"finish":').join('"end":')); + return response; + } + }, + eventClick: function (info) { + window.location = 'events/' + info.id; + }, + height: 'auto', + }); +} + +function clearCalendar() { + $('#calendar').fullCalendar('delete'); + $('#calendar').html(''); +} + +document.addEventListener('turbolinks:load', function () { + eventCalendar(); +}); +document.addEventListener('turbolinks:before-cache', clearCalendar); diff --git a/app/assets/stylesheets/manage.sass b/app/assets/stylesheets/manage.sass index 970171e2d..478d9265c 100644 --- a/app/assets/stylesheets/manage.sass +++ b/app/assets/stylesheets/manage.sass @@ -6,6 +6,7 @@ @import general/variables @import general/css4 @import manage/autocomplete +@import manage/events $grey-dark: #555 $grey-med: #999 @@ -15,6 +16,7 @@ $grey-med: #999 @import vendor/datatables.min @import selectize @import selectize.default +@import fullcalendar .icon-space-r margin-right: 0.5em @@ -137,6 +139,10 @@ $grey-med: #999 height: calc(100vh - 2.25rem) overflow: scroll +.calendar-list + width: 100% + min-width: 500px + margin: auto /* * Datatable changes diff --git a/app/assets/stylesheets/manage/events.sass b/app/assets/stylesheets/manage/events.sass new file mode 100644 index 000000000..1d4ec926a --- /dev/null +++ b/app/assets/stylesheets/manage/events.sass @@ -0,0 +1,9 @@ +.fc-list-item + cursor: pointer !important + +.event_start select:first-child, .event_finish select:first-child + margin-left: 0 !important + +.event_start select:last-child, .event_finish select:last-child + margin-right: 0 !important + diff --git a/app/controllers/manage/events_controller.rb b/app/controllers/manage/events_controller.rb new file mode 100644 index 000000000..1fef9a636 --- /dev/null +++ b/app/controllers/manage/events_controller.rb @@ -0,0 +1,54 @@ +class Manage::EventsController < Manage::ApplicationController + before_action :require_director + respond_to :html, :json + + def index + @start_date = HackathonConfig['event_start_date'] + respond_to do |format| + format.html + format.json { render json: Event.all } + end + end + + def new + @event = ::Event.new + end + + def create + @event = ::Event.new(event_params) + if @event.save + redirect_to(manage_events_path) + else + render('new') + end + end + + def show + @event = Event.find_by_id(params[:id]) + respond_with(:manage, @event) + end + + def update + @event = Event.find_by_id(params[:id]) + if @event.update(event_params) + redirect_to(manage_events_path) + else + render('show') + end + end + + def destroy + @event = Event.find_by_id(params[:id]) + if @event.destroy + redirect_to(manage_events_path) + else + render('show') + end + end + + def event_params + params.require(:event).permit( + :title, :description, :location, :public, :start, :finish, owner: [] + ) + end +end diff --git a/app/models/event.rb b/app/models/event.rb new file mode 100644 index 000000000..30d57d85e --- /dev/null +++ b/app/models/event.rb @@ -0,0 +1,12 @@ +class Event < ApplicationRecord + validates_presence_of :title, :start + + validate :finish_before_start + + def finish_before_start + return if finish.nil? + unless finish > start + errors.add(:finish, 'time must be after start time') + end + end +end diff --git a/app/views/layouts/manage/application.html.haml b/app/views/layouts/manage/application.html.haml index 763ca84a0..ebcaf19d5 100644 --- a/app/views/layouts/manage/application.html.haml +++ b/app/views/layouts/manage/application.html.haml @@ -68,6 +68,10 @@ = active_link_to manage_checkins_path, class: "nav-link" do .fa.fa-drivers-license-o.fa-fw.icon-space-r-half = t(:title, scope: 'pages.manage.check-in') + %li.nav-item + = active_link_to manage_events_path, class: "nav-link" do + .fa.fa-calendar.fa-fw.icon-space-r-half + = t(:title, scope: 'pages.manage.events') %li.nav-item = active_link_to manage_stats_path, class: "nav-link" do .fa.fa-table.fa-fw.icon-space-r-half diff --git a/app/views/manage/data_exports/_form.html.haml b/app/views/manage/data_exports/_form.html.haml index 015ad70d3..6d108063f 100644 --- a/app/views/manage/data_exports/_form.html.haml +++ b/app/views/manage/data_exports/_form.html.haml @@ -1,11 +1,9 @@ .form-container = bs_horizontal_simple_form_for @data_export, url: url_for(action: @data_export.new_record? ? "create" : "update", controller: "data_exports") do |f| = f.error_notification - - .form-inputs - = f.input :export_type, as: :select, collection: DataExport::POSSIBLE_TYPES.map { |x| [x.titleize, x] }, include_blank: false - - .form-actions.mb-3.mt-3 - = f.button :submit, class: 'btn-primary' + .form-inputs + = f.input :export_type, as: :select, collection: DataExport::POSSIBLE_TYPES.map { |x| [x.titleize, x] }, include_blank: false + .form-actions.mb-3.mt-3 + = f.button :submit, class: 'btn-primary' .mb-4 diff --git a/app/views/manage/events/_form.html.haml b/app/views/manage/events/_form.html.haml new file mode 100644 index 000000000..a46cbe00c --- /dev/null +++ b/app/views/manage/events/_form.html.haml @@ -0,0 +1,11 @@ += bs_horizontal_simple_form_for @event, url: url_for(action: @event.new_record? ? "create" : "update", controller: "events") do |f| + = f.error_notification + .form-inputs + = f.input :title + = f.input :description + = f.input :location + = f.input :start + = f.input :finish, include_blank: true + .center + //TODO figure out why you need to add save and it doesn't work automatically like other forms + = f.button :submit, 'Save', value: ( @event.new_record? ? 'Create' : 'Save' ), class: 'btn-primary' diff --git a/app/views/manage/events/index.html.haml b/app/views/manage/events/index.html.haml new file mode 100644 index 000000000..ec20c6204 --- /dev/null +++ b/app/views/manage/events/index.html.haml @@ -0,0 +1,4 @@ += render "layouts/manage/page_title", title: t(:title, scope: 'pages.manage.events') do + = link_to "Add Event", new_manage_event_path, class: "btn btn-sm btn-outline-secondary" + +.calendar-list#calendar diff --git a/app/views/manage/events/new.html.haml b/app/views/manage/events/new.html.haml new file mode 100644 index 000000000..c195ba28c --- /dev/null +++ b/app/views/manage/events/new.html.haml @@ -0,0 +1,4 @@ += render "layouts/manage/page_title", title: "New Event" + +.form-container + = render "form" diff --git a/app/views/manage/events/show.html.haml b/app/views/manage/events/show.html.haml new file mode 100644 index 000000000..ece45ff02 --- /dev/null +++ b/app/views/manage/events/show.html.haml @@ -0,0 +1,6 @@ += render "layouts/manage/page_title", title: "Edit Event", subtitle: @event.title do + .btn-group + = link_to 'Cancel', manage_events_path, class: 'btn btn-sm btn-outline-secondary' + = link_to 'Delete', manage_event_path(@event), method: :delete, data: { confirm: 'Are you sure? This action is irreversible.' }, class: 'btn btn-sm btn-outline-secondary' + += render "form" diff --git a/config/locales/en.yml b/config/locales/en.yml index e942e663f..158e3dce3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -132,11 +132,13 @@ en: title: Statistics trackable-tags: title: Trackable Tags + events: + title: Schedule bus-lists: title: Bus Lists schools: title: Schools - users: + users: title: Users & Staff users: All Users staff: "%{hackathon_name} Staff" diff --git a/config/routes.rb b/config/routes.rb index 55820e411..66b01e931 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -72,6 +72,8 @@ resources :checkins do post :datatable, on: :collection end + resources :events do + end resources :messages do get :preview, on: :member get :live_preview, on: :collection diff --git a/db/migrate/20200529142141_create_events.rb b/db/migrate/20200529142141_create_events.rb new file mode 100644 index 000000000..b9c06debc --- /dev/null +++ b/db/migrate/20200529142141_create_events.rb @@ -0,0 +1,13 @@ +class CreateEvents < ActiveRecord::Migration[5.2] + def change + create_table :events do |t| + t.string :title + t.string :description + t.string :location + t.datetime :start + t.datetime :finish + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index f50a277c0..a7eb1e2fb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -140,6 +140,16 @@ t.datetime "updated_at", null: false end + create_table "events", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + t.string "title" + t.string "description" + t.string "location" + t.datetime "start" + t.datetime "finish" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "fips", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| t.string "fips_code" t.string "city" diff --git a/test/controllers/manage/events_controller_test.rb b/test/controllers/manage/events_controller_test.rb new file mode 100644 index 000000000..88941587e --- /dev/null +++ b/test/controllers/manage/events_controller_test.rb @@ -0,0 +1,216 @@ +require 'test_helper' + +class Manage::EventsControllerTest < ActionController::TestCase + setup do + @event = create(:event) + end + + context "while not authenticated" do + should "redirect to sign in page on manage_events#index" do + get :index + assert_response :redirect + assert_redirected_to new_user_session_path + end + + should "not allow access to manage_events#new" do + get :new + assert_response :redirect + assert_redirected_to new_user_session_path + end + + should "not allow access to manage_events#show" do + get :show, params: { id: @event } + assert_response :redirect + assert_redirected_to new_user_session_path + end + + should "not allow access to manage_events#create" do + post :create, params: { event: { title: "should not exist title" } } + assert_response :redirect + assert_redirected_to new_user_session_path + end + + should "not allow access to manage_events#update" do + patch :update, params: { id: @event, title: "not allowed altered title" } + assert_response :redirect + assert_redirected_to new_user_session_path + end + + should "not allow access to manage_events#destroy" do + patch :destroy, params: { id: @event } + assert_response :redirect + assert_redirected_to new_user_session_path + end + end + + context "while authenticated as an user" do + setup do + @user = create(:user) + @request.env["devise.mapping"] = Devise.mappings[:user] + sign_in @user + end + + should "not allow access to manage_events#index" do + get :index + assert_response :redirect + assert_redirected_to root_path + end + + should "not allow access to manage_events#new" do + get :new + assert_response :redirect + assert_redirected_to root_path + end + + should "not allow access to manage_events#show" do + get :show, params: { id: @event } + assert_response :redirect + assert_redirected_to root_path + end + + should "not allow access to manage_events#create" do + post :create, params: { event: { title: "should not exist title" } } + assert_response :redirect + assert_redirected_to root_path + end + + should "not allow access to manage_events#update" do + patch :update, params: { id: @event, title: "not allowed altered title" } + assert_response :redirect + assert_redirected_to root_path + end + + should "not allow access to manage_events#destroy" do + patch :destroy, params: { id: @event } + assert_response :redirect + assert_redirected_to root_path + end + end + + context "while authenticated as an volunteer" do + setup do + @user = create(:volunteer) + @request.env["devise.mapping"] = Devise.mappings[:user] + sign_in @user + end + + should "not allow access to manage_events#index" do + get :index + assert_response :redirect + assert_redirected_to manage_checkins_path + end + + should "not allow access to manage_events#show" do + get :show, params: { id: @event } + assert_response :redirect + assert_redirected_to manage_checkins_path + end + + should "not allow access to manage_events#new" do + get :new + assert_response :redirect + assert_redirected_to manage_events_path + end + + should "not allow access to manage_events#create" do + post :create, params: { event: { title: "should not exist title" } } + assert_response :redirect + assert_redirected_to manage_events_path + end + + should "not allow access to manage_events#update" do + patch :update, params: { id: @event, title: "not allowed altered title" } + assert_response :redirect + assert_redirected_to manage_events_path + end + + should "not allow access to manage_events#destroy" do + patch :destroy, params: { id: @event } + assert_response :redirect + assert_redirected_to manage_events_path + end + end + + context "while authenticated as an organizer" do + setup do + @user = create(:organizer) + @request.env["devise.mapping"] = Devise.mappings[:user] + sign_in @user + end + + should "not allow access to manage_events#index" do + get :index + assert_response :redirect + assert_redirected_to manage_root_path + end + + should "not allow access to manage_events#show" do + get :show, params: { id: @event } + assert_response :redirect + assert_redirected_to manage_root_path + end + + should "not allow access to manage_events#new" do + get :new + assert_response :redirect + assert_redirected_to manage_events_path + end + + should "not allow access to manage_events#create" do + post :create, params: { event: { title: "should not exist title" } } + assert_response :redirect + assert_redirected_to manage_events_path + end + + should "not allow access to manage_events#update" do + patch :update, params: { id: @event, title: "not allowed altered title" } + assert_response :redirect + assert_redirected_to manage_events_path + end + + should "not allow access to manage_events#destroy" do + patch :destroy, params: { id: @event } + assert_response :redirect + assert_redirected_to manage_events_path + end + end + + context "while authenticated as an director" do + setup do + @user = create(:director) + @request.env["devise.mapping"] = Devise.mappings[:user] + sign_in @user + end + + should "allow access to manage_events#index" do + get :index + assert_response :success + end + + should "allow access to manage_events#new" do + get :new + assert_response :success + end + + should "allow access to manage_events#show" do + get :show, params: { id: @event } + assert_response :success + end + + should "allow access to manage_events#create" do + post :create, params: { event: { title: "test new title", start: Date.today - 1.hour, finish: Date.today } } + assert_redirected_to manage_events_path + end + + should "allow access to manage_events#update" do + patch :update, params: { id: @event, event: { title: "test update title" } } + assert_redirected_to manage_events_path + end + + should "allow access to manage_events#destroy" do + patch :destroy, params: { id: @event } + assert_response :redirect + assert_redirected_to manage_events_path + end + end +end diff --git a/test/factories/event.rb b/test/factories/event.rb new file mode 100644 index 000000000..ecb8cb57d --- /dev/null +++ b/test/factories/event.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :event do + title { "Testing" } + description { "This is fun to test" } + start { Date.today - 1.hour } + finish { Date.today } + location { "GOL-1455" } + end +end diff --git a/test/fixtures/events.yml b/test/fixtures/events.yml new file mode 100644 index 000000000..9ffbd5d90 --- /dev/null +++ b/test/fixtures/events.yml @@ -0,0 +1,15 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + title: MyString + description: MyString + location: MyString + start: 2020-05-29 10:21:41 + finish: 2020-05-29 10:21:41 + +two: + title: MyString + description: MyString + location: MyString + start: 2020-05-29 10:21:41 + finish: 2020-05-29 10:21:41 diff --git a/test/models/event_test.rb b/test/models/event_test.rb new file mode 100644 index 000000000..9b73cff83 --- /dev/null +++ b/test/models/event_test.rb @@ -0,0 +1,6 @@ +require 'test_helper' + +class EventTest < ActiveSupport::TestCase + should validate_presence_of :title + should validate_presence_of :start +end diff --git a/vendor/cache/erubi-1.10.0.gem b/vendor/cache/erubi-1.10.0.gem new file mode 100644 index 000000000..31894cf32 Binary files /dev/null and b/vendor/cache/erubi-1.10.0.gem differ diff --git a/vendor/cache/erubi-1.9.0.gem b/vendor/cache/erubi-1.9.0.gem deleted file mode 100644 index e169e6582..000000000 Binary files a/vendor/cache/erubi-1.9.0.gem and /dev/null differ diff --git a/vendor/cache/ffi-1.13.1-x64-mingw32.gem b/vendor/cache/ffi-1.13.1-x64-mingw32.gem new file mode 100644 index 000000000..cdf741aa3 Binary files /dev/null and b/vendor/cache/ffi-1.13.1-x64-mingw32.gem differ diff --git a/vendor/cache/fullcalendar-rails-3.9.0.0.gem b/vendor/cache/fullcalendar-rails-3.9.0.0.gem new file mode 100644 index 000000000..f397ddc19 Binary files /dev/null and b/vendor/cache/fullcalendar-rails-3.9.0.0.gem differ diff --git a/vendor/cache/loofah-2.7.0.gem b/vendor/cache/loofah-2.7.0.gem deleted file mode 100644 index 13bd5eb16..000000000 Binary files a/vendor/cache/loofah-2.7.0.gem and /dev/null differ diff --git a/vendor/cache/loofah-2.8.0.gem b/vendor/cache/loofah-2.8.0.gem new file mode 100644 index 000000000..6d89bedae Binary files /dev/null and b/vendor/cache/loofah-2.8.0.gem differ diff --git a/vendor/cache/momentjs-rails-2.20.1.gem b/vendor/cache/momentjs-rails-2.20.1.gem new file mode 100644 index 000000000..bbe07f855 Binary files /dev/null and b/vendor/cache/momentjs-rails-2.20.1.gem differ diff --git a/vendor/cache/msgpack-1.3.3-x64-mingw32.gem b/vendor/cache/msgpack-1.3.3-x64-mingw32.gem new file mode 100644 index 000000000..fdf50d9d6 Binary files /dev/null and b/vendor/cache/msgpack-1.3.3-x64-mingw32.gem differ diff --git a/vendor/cache/mysql2-0.5.3-x64-mingw32.gem b/vendor/cache/mysql2-0.5.3-x64-mingw32.gem new file mode 100644 index 000000000..cd7a490c4 Binary files /dev/null and b/vendor/cache/mysql2-0.5.3-x64-mingw32.gem differ diff --git a/vendor/cache/nio4r-2.5.3.gem b/vendor/cache/nio4r-2.5.3.gem deleted file mode 100644 index 9947d76b0..000000000 Binary files a/vendor/cache/nio4r-2.5.3.gem and /dev/null differ diff --git a/vendor/cache/nio4r-2.5.4.gem b/vendor/cache/nio4r-2.5.4.gem new file mode 100644 index 000000000..f5eaa0c43 Binary files /dev/null and b/vendor/cache/nio4r-2.5.4.gem differ diff --git a/vendor/cache/sassc-2.4.0-x64-mingw32.gem b/vendor/cache/sassc-2.4.0-x64-mingw32.gem new file mode 100644 index 000000000..9b3393d35 Binary files /dev/null and b/vendor/cache/sassc-2.4.0-x64-mingw32.gem differ diff --git a/vendor/cache/sprockets-rails-3.2.1.gem b/vendor/cache/sprockets-rails-3.2.1.gem deleted file mode 100644 index 58921be3a..000000000 Binary files a/vendor/cache/sprockets-rails-3.2.1.gem and /dev/null differ diff --git a/vendor/cache/sprockets-rails-3.2.2.gem b/vendor/cache/sprockets-rails-3.2.2.gem new file mode 100644 index 000000000..8d2c00be6 Binary files /dev/null and b/vendor/cache/sprockets-rails-3.2.2.gem differ diff --git a/vendor/cache/tzinfo-1.2.7.gem b/vendor/cache/tzinfo-1.2.7.gem deleted file mode 100644 index 554971346..000000000 Binary files a/vendor/cache/tzinfo-1.2.7.gem and /dev/null differ diff --git a/vendor/cache/tzinfo-1.2.8.gem b/vendor/cache/tzinfo-1.2.8.gem new file mode 100644 index 000000000..f24bf8e1e Binary files /dev/null and b/vendor/cache/tzinfo-1.2.8.gem differ