From 7ebe434a73aebfd4ac4af1a2ff02bc978d2a1f13 Mon Sep 17 00:00:00 2001 From: Tyler Lemburg <trlemburg@gmail.com> Date: Fri, 17 Jun 2016 13:21:32 -0500 Subject: [PATCH] Events and Resources --- Gemfile | 1 + Gemfile.lock | 11 + app.rb | 126 ++++-- .../20160608113900_add_session_store.rb | 13 + .../20160608134500_add_creation_method.rb | 7 + .../20160608140300_add_space_permissions.rb | 16 + models/event.rb | 11 +- models/permission.rb | 1 + models/reservation.rb | 1 + models/service_space.rb | 22 +- models/user.rb | 57 +-- models/user_has_permission.rb | 5 + routes/admin.rb | 43 ++ routes/admin/events.rb | 181 +++++++++ routes/events.rb | 76 ++++ routes/resources.rb | 373 ++++++++++++++++++ routes/space.rb | 23 ++ views/admin/events.erb | 110 ++++++ views/admin/home.erb | 62 +++ views/admin/new_event.erb | 177 +++++++++ views/calendar.erb | 4 +- views/event_details.erb | 62 +++ views/home.erb | 16 + views/reserve.erb | 187 +++++++++ views/resources.erb | 34 ++ views/space_home.erb | 85 ++++ views/template_partials/navigation.erb | 29 +- 27 files changed, 1651 insertions(+), 82 deletions(-) create mode 100644 db/migrate/20160608113900_add_session_store.rb create mode 100644 db/migrate/20160608134500_add_creation_method.rb create mode 100644 db/migrate/20160608140300_add_space_permissions.rb create mode 100644 routes/admin.rb create mode 100644 routes/admin/events.rb create mode 100644 routes/events.rb create mode 100644 routes/resources.rb create mode 100644 routes/space.rb create mode 100644 views/admin/events.erb create mode 100644 views/admin/home.erb create mode 100644 views/admin/new_event.erb create mode 100644 views/event_details.erb create mode 100644 views/home.erb create mode 100644 views/reserve.erb create mode 100644 views/resources.erb create mode 100644 views/space_home.erb diff --git a/Gemfile b/Gemfile index f57b58d..1beab10 100644 --- a/Gemfile +++ b/Gemfile @@ -10,6 +10,7 @@ gem 'bcrypt' gem 'unicorn' gem 'pony' gem 'rest-client' +gem 'rack-cas' group :development do gem 'shotgun' diff --git a/Gemfile.lock b/Gemfile.lock index aee2907..6ffb151 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -14,6 +14,7 @@ GEM minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) + addressable (2.4.0) arel (6.0.3) backports (3.6.8) bcrypt (3.1.11) @@ -57,14 +58,19 @@ GEM mime-types (>= 1.16, < 4) method_source (0.8.2) mime-types (2.99.2) + mini_portile2 (2.1.0) minitest (5.9.0) multi_json (1.12.1) mysql (2.9.1) nenv (0.3.0) netrc (0.11.0) + nokogiri (1.6.8) + mini_portile2 (~> 2.1.0) + pkg-config (~> 1.1.7) notiffany (0.1.0) nenv (~> 0.1) shellany (~> 0.0) + pkg-config (1.1.7) pony (1.11) mail (>= 2.0) pry (0.10.3) @@ -72,6 +78,10 @@ GEM method_source (~> 0.8.1) slop (~> 3.4) rack (1.6.4) + rack-cas (0.13.0) + addressable (~> 2.3) + nokogiri (~> 1.5) + rack (~> 1.3) rack-protection (1.5.3) rack rack-test (0.6.3) @@ -130,6 +140,7 @@ DEPENDENCIES guard-less mysql pony + rack-cas rest-client shotgun sinatra diff --git a/app.rb b/app.rb index 1dfd310..928b944 100644 --- a/app.rb +++ b/app.rb @@ -1,15 +1,28 @@ require 'sinatra' +require 'rest-client' +require 'rack/session/abstract/id' +require 'rack/cas' +require 'rack-cas' +require 'rack-cas/session_store/active_record' require 'models/user' require 'models/service_space' -use Rack::Session::Cookie, :key => 'rack.session', - :path => '/', - :domain => (ENV['RACK_ENV'] == 'development' ? nil : 'resource.unl.edu'), - :secret => '420terrace12qmemorialpinnaclehawks', - :old_secret => '420terrace12qmemorialpinnaclehawks' +module Rack + module Session + class RackCASActiveRecordStore < Rack::Session::Abstract::ID + include RackCAS::ActiveRecordStore + end + end +end + +use Rack::Session::RackCASActiveRecordStore +use Rack::CAS, server_url: 'https://login.unl.edu/cas', + session_store: RackCAS::ActiveRecordStore Time.zone = "America/Chicago" +DIRECTORY_URL = 'http://directory.unl.edu/' + # this gives the user messages def flash(type, header, message) session["notice"] ||= [] @@ -21,28 +34,35 @@ def flash(type, header, message) end before do - # site defaults - @title = 'UNL Resource Scheduler' - @breadcrumbs = [ - { - :href => 'http://www.unl.edu/', - :text => 'UNL', - :title => 'University of Nebraska–Lincoln' - }, - { - :href => '/', - :text => 'UNL Resource Scheduler' - } - ] - - session[:init] = true - - # check if the user is currently logged in - if session.has_key?(:user_id) - @user = (User.includes(:permissions).find(session[:user_id]) rescue nil) - else - @user = nil; - end + require_login + + # site defaults + @title = 'UNL Resource Scheduler' + @breadcrumbs = [ + { + :href => 'http://www.unl.edu/', + :text => 'UNL', + :title => 'University of Nebraska–Lincoln' + }, + { + :href => '/', + :text => 'UNL Resource Scheduler' + } + ] +end + +def calculate_time(date_string, hour, minute, am_pm) + hour ||= 0 + minute ||= 0 + am_pm ||= 'am' + + hour = hour.to_i % 12 + hour = hour + 12 if am_pm == 'pm' + + date_strings = date_string.split('/') + date_string = "#{date_strings[2]}-#{date_strings[0]}-#{date_strings[1]}" + date = Time.parse(date_string) + Time.new(date.year, date.month, date.day, hour, minute, 0) end helpers do @@ -51,12 +71,43 @@ helpers do space = ServiceSpace.find_by(:url_name => url_name) raise Sinatra::NotFound if space.nil? @space = space + + if !@user.in_space(@space) + flash :error, 'Unauthorized', 'Sorry, you don\'t have access to this service space.' + redirect '/' + end + + @breadcrumbs << {:text => @space.name, :href => @space.href} end def require_login - if @user.nil? - flash(:alert, 'You Must Login', 'That page requires you to be logged in. If you don\'t have an account, please sign up for <a href="/new_members/">New Member Orientation</a>.') - redirect '/login/' + if session['cas'].nil? || session['cas']['user'].nil? + halt 401 + else + # check if the user already exists in this app's db + @user = User.find_by(:username => session['cas']['user'], :creation_method => 'CAS') + if @user.nil? + # get this user's info from UNL Directory + RestClient.get("#{DIRECTORY_URL}?uid=#{session['cas']['user']}&format=json") do |response, request, result| + case response.code + when 200 + info = JSON.parse(response.body) + # create this user + @user = User.create( + username: session['cas']['user'], + email: info['mail'][0], + first_name: info['givenName'][0], + last_name: info['sn'][0], + date_created: Time.now, + creation_method: 'CAS', + is_admin: false + ) + else + flash :error, 'User Not Found in UNL Directory', "This user was not found in the UNL Directory." + redirect back + end + end + end end end end @@ -74,7 +125,20 @@ end get '/' do @breadcrumbs << {:text => 'Home'} - erb 'Home', :layout => :fixed + # get the service spaces that this user has access to + spaces = @user.service_spaces + erb :home, :layout => :fixed, :locals => { + spaces: spaces + } +end + +get '/images/:event_id/?' do + event = Event.find_by(:id => params[:event_id]) + if event.nil? || event.imagedata.nil? + raise Sinatra::NotFound + end + + return event.imagedata end Dir.glob("#{ROOT}/routes/*.rb") { |file| require file } \ No newline at end of file diff --git a/db/migrate/20160608113900_add_session_store.rb b/db/migrate/20160608113900_add_session_store.rb new file mode 100644 index 0000000..711932b --- /dev/null +++ b/db/migrate/20160608113900_add_session_store.rb @@ -0,0 +1,13 @@ +require 'active_record' + +class AddSessionStore < ActiveRecord::Migration + def change + create_table :sessions do |t| + t.string :cas_ticket + t.string :session_id + t.text :data + t.datetime :created_at + t.datetime :updated_at + end + end +end \ No newline at end of file diff --git a/db/migrate/20160608134500_add_creation_method.rb b/db/migrate/20160608134500_add_creation_method.rb new file mode 100644 index 0000000..4acbf33 --- /dev/null +++ b/db/migrate/20160608134500_add_creation_method.rb @@ -0,0 +1,7 @@ +require 'active_record' + +class AddCreationMethod < ActiveRecord::Migration + def change + add_column :users, :creation_method, :string + end +end \ No newline at end of file diff --git a/db/migrate/20160608140300_add_space_permissions.rb b/db/migrate/20160608140300_add_space_permissions.rb new file mode 100644 index 0000000..0fe2f83 --- /dev/null +++ b/db/migrate/20160608140300_add_space_permissions.rb @@ -0,0 +1,16 @@ +require 'active_record' +require 'models/permission' + +class AddSpacePermissions < ActiveRecord::Migration + def up + Permission.create(:name => 'User Access', :id => 8) + + add_column :user_has_permissions, :service_space_id, :integer, :default => 1 + end + + def down + remove_column :user_has_permissions, :service_space_id, :integer, :default => 1 + + Permission.find_by(:id => 8).delete + end +end \ No newline at end of file diff --git a/models/event.rb b/models/event.rb index 04cb86d..81f2731 100644 --- a/models/event.rb +++ b/models/event.rb @@ -5,6 +5,7 @@ class Event < ActiveRecord::Base has_one :reservation, :dependent => :destroy belongs_to :location belongs_to :event_type + belongs_to :service_space alias_method :type, :event_type alias_method :signups, :event_signups @@ -29,16 +30,11 @@ class Event < ActiveRecord::Base end def info_link - case type.description - when 'New Member Orientation' - "/new_members/sign_up/#{id}/" - else - "/events/#{id}/" - end + "/#{service_space.url_name}/events/#{id}/" end def edit_link - "/admin/events/#{id}/edit/" + "/#{service_space.url_name}/admin/events/#{id}/edit/" end def has_reservation @@ -57,7 +53,6 @@ class Event < ActiveRecord::Base self.event_type_id = params[:type] self.location_id = params[:location] self.max_signups = params[:limit_signups] == 'on' ? params[:max_signups].to_i : nil - self.service_space_id = 1 self.save end diff --git a/models/permission.rb b/models/permission.rb index f11c56c..4c59768 100644 --- a/models/permission.rb +++ b/models/permission.rb @@ -8,4 +8,5 @@ class Permission < ActiveRecord::Base MANAGE_SPACE_HOURS = 5 MANAGE_EVENTS = 6 SEE_AGENDA = 7 + USER_ACCESS = 8 end \ No newline at end of file diff --git a/models/reservation.rb b/models/reservation.rb index 24e1998..357973d 100644 --- a/models/reservation.rb +++ b/models/reservation.rb @@ -1,4 +1,5 @@ require 'active_record' +require 'models/resource' class Reservation < ActiveRecord::Base belongs_to :resource diff --git a/models/service_space.rb b/models/service_space.rb index 149b127..c3ccd2c 100644 --- a/models/service_space.rb +++ b/models/service_space.rb @@ -1,5 +1,25 @@ require 'active_record' class ServiceSpace < ActiveRecord::Base - + has_many :users + + def href + "/#{self.url_name}/" + end + + def calendar_href + "/#{self.url_name}/calendar/" + end + + def resources_href + "/#{self.url_name}/resources/" + end + + def admin_href + "/#{self.url_name}/admin/" + end + + def admin_events_href + "/#{self.url_name}/admin/events/" + end end \ No newline at end of file diff --git a/models/user.rb b/models/user.rb index fd8c0b5..c4f8656 100644 --- a/models/user.rb +++ b/models/user.rb @@ -4,13 +4,20 @@ require 'models/resource_authorization' require 'models/event_signup' require 'models/permission' require 'models/user_has_permission' +require 'models/service_space' require 'classes/emailer' class User < ActiveRecord::Base has_many :resource_authorizations has_many :event_signups has_many :user_has_permissions - has_many :permissions, through: :user_has_permissions + has_many :permissions, through: :user_has_permissions, source: :permission + + def service_spaces + ServiceSpace.all.select do |space| + self.in_space(space) + end + end def authorized_resource_ids self.resource_authorizations.map {|res_auth| res_auth.resource_id} @@ -24,30 +31,24 @@ class User < ActiveRecord::Base self.event_signups.map {|event_signup| event_signup.event_id} end - include BCrypt - - # now decides based on whether they have any admin permissions - def is_admin? - !self.permissions.empty? + # we decide if the user is an admin if they have any permissions in the space (besides the user access permission, which indicates they are a basic user) + def is_admin?(space) + !self.user_has_permissions.where(:service_space_id => space.id).where.not(:permission_id => Permission::USER_ACCESS).empty? end alias_method :admin?, :is_admin? - def is_super_user? - self.permissions.include?(Permission.find(Permission::SUPER_USER)) + def is_super_user?(space) + !self.user_has_permissions.where(:service_space_id => space.id, :permission_id => Permission::SUPER_USER).empty? end alias_method :super_user?, :is_super_user? - def has_permission?(id) - self.permissions.include?(Permission.find(id)) + def has_permission?(id, space) + !self.user_has_permissions.where(:service_space_id => space.id, :permission_id => id).empty? end - def password - @password ||= Password.new(password_hash) - end - - def password=(new_password) - @password = Password.create(new_password) - self.password_hash = @password + # just notes whether they have any permissions in the space, and can access it at all + def in_space(space) + !self.user_has_permissions.where(:service_space_id => space.id).empty? end def full_name @@ -57,26 +58,4 @@ class User < ActiveRecord::Base def sortable_name "#{last_name}, #{first_name}" end - - def send_reset_password_email - token = '' - begin - token = String.token - end while User.find_by(:reset_password_token => token) != nil - self.reset_password_token = token - self.reset_password_expiry = Time.now + 1.day - self.save - -body = <<EMAIL -<p>We received a request to reset your Innovation Studio Manager password. Please click the link below to reset your password.</p> - -<p><a href="http://#{ENV['RACK_ENV'] == 'development' ? 'localhost:9393' : 'innovationstudio-manager.unl.edu'}/reset_password/#{token}/">http://#{ENV['RACK_ENV'] == 'development' ? 'localhost:9393' : 'innovationstudio-manager.unl.edu'}/reset_password/#{token}/</a></p> - -<p>This link will only be active for 24 hours. If you did not request to reset your password, you may safely disregard this email.</p> - -<p>Nebraska Innovation Studio</p> -EMAIL - - Emailer.mail(self.email, 'Nebraska Innovation Studio password reset', body) - end end \ No newline at end of file diff --git a/models/user_has_permission.rb b/models/user_has_permission.rb index 8759fd1..24ec5aa 100644 --- a/models/user_has_permission.rb +++ b/models/user_has_permission.rb @@ -3,4 +3,9 @@ require 'active_record' class UserHasPermission < ActiveRecord::Base belongs_to :user belongs_to :permission + belongs_to :service_space + + scope :in_space, ->(space) { + where(:service_space_id => space.id) + } end \ No newline at end of file diff --git a/routes/admin.rb b/routes/admin.rb new file mode 100644 index 0000000..758377f --- /dev/null +++ b/routes/admin.rb @@ -0,0 +1,43 @@ +require 'models/user' +require 'models/event' +require 'models/resource' +require 'models/space_hour' +require 'models/permission' + +before '/:service_space_url_name/admin*' do + load_service_space + + raise Sinatra::NotFound unless !@user.nil? && @user.is_admin?(@space) +end + +get '/:service_space_url_name/admin/?' do + @breadcrumbs << {:text => 'Admin Home'} + user_count = User.where(:service_space_id => @space.id).count + upcoming_event_count = Event.where(:service_space_id => @space.id).where('start_time >= ?', Time.now).count + resource_count = Resource.where(:service_space_id => @space.id).count + + date = Time.now.midnight + # get the hours for this day to show + hours = SpaceHour.where(:service_space_id => @space.id) + .where('effective_date <= ?', date.utc.strftime('%Y-%m-%d %H:%M:%S')) + .order(:effective_date => :desc, :id => :desc).all.to_a + + correct_hour = nil + + hours.each do |space_hour| + if date.wday == space_hour.day_of_week && (space_hour.effective_date.in_time_zone.midnight == date.in_time_zone.midnight || (!space_hour.one_off && space_hour.effective_date.in_time_zone.midnight <= date.in_time_zone.midnight)) + correct_hour = space_hour + break + end + end + + erb :'admin/home', :layout => :fixed, :locals => { + :user_count => user_count, + :upcoming_event_count => upcoming_event_count, + :resource_count => resource_count, + :space_hour => correct_hour, + :date => date + } +end + +Dir.glob("#{ROOT}/routes/admin/*.rb") { |file| require file } \ No newline at end of file diff --git a/routes/admin/events.rb b/routes/admin/events.rb new file mode 100644 index 0000000..3bd265a --- /dev/null +++ b/routes/admin/events.rb @@ -0,0 +1,181 @@ +require 'rest-client' +require 'models/event' +require 'models/event_type' +require 'models/location' +require 'models/resource' + +before '/:service_space_url_name/admin/events*' do + unless @user.has_permission?(Permission::MANAGE_EVENTS, @space) + raise Sinatra::NotFound + end +end + +get '/:service_space_url_name/admin/events/?' do + @breadcrumbs << {:text => 'Admin Events'} + page = params[:page] + page = page.to_i >= 1 ? page.to_i : 1 + page_size = 10 + tab = ['upcoming', 'past'].include?(params[:tab]) ? params[:tab] : 'upcoming' + + case tab + when 'past' + where_clause = 'start_time < ?', Time.now + order_clause = {:start_time => :desc} + else + where_clause = 'start_time >= ?', Time.now + order_clause = {:start_time => :asc} + end + + iterator = Event.includes(:event_signups).where(:service_space_id => @space.id).where(where_clause) + + erb :'admin/events', :layout => :fixed, :locals => { + :events => iterator.order(order_clause).limit(page_size).offset((page-1)*page_size).all, + :total_pages => (iterator.count.to_f / page_size).ceil, + :page => page, + :tab => tab + } +end + +get '/:service_space_url_name/admin/events/:event_id/signup_list/?' do + @breadcrumbs << {:text => 'Admin Events', :href => '/admin/events/'} << {text: 'Signup List'} + event = Event.includes(:event_signups).find_by(:id => params[:event_id], :service_space_id => @space.id) + if event.nil? + # that event does not exist + flash(:danger, 'Not Found', 'That event does not exist') + redirect @space.admin_events_href + end + + erb :'admin/signup_list', :layout => :fixed, :locals => { + :event => event + } +end + +get '/:service_space_url_name/admin/events/create/?' do + @breadcrumbs << {:text => 'Admin Events', :href => '/admin/events/'} << {text: 'Create Event'} + erb :'admin/new_event', :layout => :fixed, :locals => { + :event => Event.new, + :types => EventType.where(:service_space_id => @space.id).all, + :locations => Location.where(:service_space_id => @space.id).all, + :resources => Resource.where(:service_space_id => @space.id, :is_reservable => true).all, + :on_unl_events => false, + :on_main_calendar => false + } +end + +post '/:service_space_url_name/admin/events/create/?' do + if params[:location] == 'new' + # this is a new location, we must create it! + location = Location.create(params[:new_location].merge({ + :service_space_id => @space.id + })) + params[:location] = location.id + end + + event = Event.new + event.set_image_data(params) + event.set_data(params) + event.service_space_id = @space.id + + if params.has_key?('reserve_resource') && params['reserve_resource'] == 'on' + # we need to create a reservation for the resource on the appropriate time + Reservation.create( + :resource_id => params[:resource], + :event_id => event.id, + :start_time => event.start_time, + :end_time => event.end_time, + :is_training => true, + :user_id => nil + ) + end + + # notify that it worked + flash(:success, 'Event Created', "Your #{event.type.description}: #{event.title} has been created.") + redirect @space.admin_events_href +end + +get '/:service_space_url_name/admin/events/:event_id/edit/?' do + @breadcrumbs << {:text => 'Admin Events', :href => '/admin/events/'} << {text: 'Edit Event'} + event = Event.includes(:event_type, :location, :reservation => :resource).find_by(:id => params[:event_id], :service_space_id => @space.id) + if event.nil? + # that event does not exist + flash(:danger, 'Not Found', 'That event does not exist') + redirect @space.admin_events_href + end + + erb :'admin/new_event', :layout => :fixed, :locals => { + :event => event, + :types => EventType.where(:service_space_id => @space.id).all, + :locations => Location.where(:service_space_id => @space.id).all, + :resources => Resource.where(:service_space_id => @space.id, :is_reservable => true).all + } +end + +post '/:service_space_url_name/admin/events/:event_id/edit/?' do + event = Event.find_by(:id => params[:event_id], :service_space_id => @space.id) + if event.nil? + # that event does not exist + flash(:danger, 'Not Found', 'That event does not exist') + redirect @space.admin_events_href + end + + if params[:location] == 'new' + # this is a new location, we must create it! + location = Location.create(params[:new_location].merge({ + :service_space_id => @space.id + })) + params[:location] = location.id + end + + if params.checked?('remove_image') + event.remove_image_data + else + event.set_image_data(params) + end + event.set_data(params) + event.service_space_id = @space.id + + # check the resource reservation for this + checked = params.checked?('reserve_resource') + if event.has_reservation && checked + # update the reservation + event.reservation.update( + :resource_id => params[:resource], + :event_id => event.id, + :start_time => event.start_time, + :end_time => event.end_time, + :is_training => true, + :user_id => nil + ) + elsif event.has_reservation && !checked + # remove the reservation + event.reservation.delete + elsif !event.has_reservation && checked + # create the reservation + Reservation.create( + :resource_id => params[:resource], + :event_id => event.id, + :start_time => event.start_time, + :end_time => event.end_time, + :is_training => true, + :user_id => nil + ) + end + + # notify that it worked + flash(:success, 'Event Updated', "Your #{event.type.description}: #{event.title} has been updated.") + redirect @space.admin_events_href +end + +post '/:service_space_url_name/admin/events/:event_id/delete/?' do + event = Event.find_by(:id => params[:event_id], :service_space_id => @space.id) + if event.nil? + # that event does not exist + flash(:danger, 'Not Found', 'That event does not exist') + redirect @space.admin_events_href + end + + event.destroy + + flash(:success, 'Event Deleted', "Your event #{event.title} has been deleted. All signups on this event have also been removed, and if a reservation was attached, it also has been removed.") + redirect @space.admin_events_href +end \ No newline at end of file diff --git a/routes/events.rb b/routes/events.rb new file mode 100644 index 0000000..43bfbbc --- /dev/null +++ b/routes/events.rb @@ -0,0 +1,76 @@ +require 'models/event' +require 'models/event_signup' + +get '/:service_space_url_name/events/:event_id/?' do + load_service_space + + # this is an event details page + begin + event = Event.includes(:location, :event_type, :event_signups).find(params[:event_id]) + rescue ActiveRecord::RecordNotFound => e + not_found + end + + @breadcrumbs << {:text => event.title} + erb :event_details, :layout => :fixed, :locals => { + :event => event + } +end + +post '/:service_space_url_name/events/:event_id/sign_up/?' do + load_service_space + + # check that is a valid event + event = Event.includes(:event_type).find_by(:service_space_id => @space.id, :id => params[:event_id]) + + if event.nil? + # that event does not exist + flash(:danger, 'Not Found', 'That event does not exist') + redirect '/calendar/' + end + + if !event.max_signups.nil? && event.signups.count >= event.max_signups + # that event is full + flash(:danger, 'Event Full', 'Sorry, that event is full.') + redirect back + end + + EventSignup.create( + :event_id => params[:event_id], + :name => @user.full_name, + :user_id => @user.id, + :email => @user.email + ) + + if event.type.description != 'Free Event' + # flash a message that this works + flash(:success, "You're signed up!", "Thanks for signing up! Don't forget, #{event.title} is #{event.start_time.in_time_zone.strftime('%A, %B %d at %l:%M %P')}.") + redirect back + else + # flash a message that this works + flash(:success, "Event Marked", "This free event has been marked on your hoempage.") + redirect back + end +end + +post '/:service_space_url_name/events/:event_id/remove_signup/?' do + load_service_space + + # get the event + event = Event.includes(:event_type).where(:id => params[:event_id]).first + + # check that the signup exists + signup = EventSignup.where(:event_id => params[:event_id], :user_id => @user.id).first + + if signup.nil? + flash :alert, 'Not Found', 'That signup was not found.' + redirect '/home/' + end + signup.delete + + header = event.type.description == 'Free Event' ? 'Event Removed' : 'Signup Removed' + message = event.type.description == 'Free Event' ? "#{event.title} has been removed from your calendar." : "Your signup for #{event.title} has been removed." + + flash :success, header, message + redirect '/home/' +end \ No newline at end of file diff --git a/routes/resources.rb b/routes/resources.rb new file mode 100644 index 0000000..4c86d4a --- /dev/null +++ b/routes/resources.rb @@ -0,0 +1,373 @@ +require 'models/resource' +require 'models/reservation' +require 'models/event' +require 'models/event_type' +require 'models/event_signup' +require 'models/space_hour' + +get '/:service_space_url_name/resources/?' do + load_service_space + @breadcrumbs << {:text => 'Resources'} + + # show resources that the user is authorized to use, as well as all those that do not require authorization + resources = Resource.where(:service_space_id => @space.id).all.to_a + resources.reject! {|resource| resource.needs_authorization && !@user.authorized_resource_ids.include?(resource.id)} + + erb :resources, :layout => :fixed, :locals => { + :available_resources => resources + } +end + +# form for reserving a resource +get '/:service_space_url_name/resources/:resource_id/reserve/?' do + load_service_space + @breadcrumbs << {:text => 'Resources', :href => @space.resources_href} << {:text => 'Reserve'} + + # check that the user has authorization to reserve this resource, if resource requires auth + resource = Resource.find_by(:service_space_id => @space.id, :id => params[:resource_id]) + if resource.nil? + flash(:alert, 'Not Found', 'That resource does not exist.') + redirect @space.resources_href + end + + date = params[:date].nil? ? Time.now.midnight.in_time_zone : Time.parse(params[:date]).midnight.in_time_zone + # get the studio's hours for this day + # is there a one_off + space_hour = SpaceHour.where(:service_space_id => @space.id) + .where('effective_date = ?', date.utc.strftime('%Y-%m-%d %H:%M:%S')) + .where(:day_of_week => date.wday).where(:one_off => true).first + if space_hour.nil? + space_hour = SpaceHour.where(:service_space_id => @space.id) + .where('effective_date <= ?', date.utc.strftime('%Y-%m-%d %H:%M:%S')) + .where(:day_of_week => date.wday).where(:one_off => false) + .order(:effective_date => :desc, :id => :desc).first + end + + available_start_times = [] + # calculate the available start times for reservation + if space_hour.nil? + start = 0 + while start + (resource.minutes_per_reservation || 15) <= 1440 + available_start_times << start + start += (resource.minutes_per_reservation || 15) + end + else + space_hour.hours.sort{|x,y| x[:start] <=> y[:start]}.each do |record| + if record[:status] == 'open' + start = record[:start] + while start + (resource.minutes_per_reservation || 15) <= record[:end] + available_start_times << start + start += (resource.minutes_per_reservation || 15) + end + end + end + end + + # filter out times when resource is reserved + reservations = Reservation.includes(:event).where(:resource_id => resource.id).in_day(date).all + available_start_times = available_start_times - reservations.map{|res|res.start_time.in_time_zone.minutes_after_midnight} + + erb :reserve, :layout => :fixed, :locals => { + :resource => resource, + :reservations => reservations, + :available_start_times => available_start_times, + :space_hour => space_hour, + :day => date, + :reservation => nil + } +end + +# submit form for reserving a resource +post '/:service_space_url_name/resources/:resource_id/reserve/?' do + load_service_space + + resource = Resource.find_by(:service_space_id => @space.id, :id => params[:resource_id]) + if resource.nil? + flash(:alert, 'Not Found', 'That resource does not exist.') + redirect @space.resources_href + end + + if params[:start_minutes].nil? + flash(:alert, 'Please specify a start time', 'Please specify a start time for your reservation.') + redirect back + end + + hour = (params[:start_minutes].to_i / 60).floor + am_pm = hour >= 12 ? 'pm' : 'am' + hour = hour % 12 + hour += 12 if hour == 0 + minutes = params[:start_minutes].to_i % 60 + + start_time = calculate_time(params[:date], hour, minutes, am_pm) + end_time = start_time + params[:length].to_i.minutes + + date = start_time.midnight + # validate that the requested time slot falls within the open hours of the day + # get the studio's hours for this day + # is there a one_off + space_hour = SpaceHour.where(:service_space_id => @space.id) + .where('effective_date = ?', date.utc.strftime('%Y-%m-%d %H:%M:%S')) + .where(:day_of_week => date.wday).where(:one_off => true).first + if space_hour.nil? + space_hour = SpaceHour.where(:service_space_id => @space.id) + .where('effective_date <= ?', date.utc.strftime('%Y-%m-%d %H:%M:%S')) + .where(:day_of_week => date.wday).where(:one_off => false) + .order(:effective_date => :desc, :id => :desc).first + end + + unless space_hour.nil? + # figure out where the closed sections need to be + # we can assume that all records in this space_hour are non-intertwined + closed_start = 0 + closed_end = 0 + starts = space_hour.hours.map{|record| record[:start]} + ends = space_hour.hours.map{|record| record[:end]} + closeds = [] + (0..1439).each do |j| + if starts.include?(j) + closed_end = j + closeds << {:status => 'closed', :start => closed_start, :end => closed_end} + closed_start = 0 + closed_end = 0 + end + if ends.include?(j) + closed_start = j + end + end + closed_end = 1440 + closeds << {:status => 'closed', :start => closed_start, :end => closed_end} + + # for each record, ensure that the time does not overlap if the record is not "open" + (space_hour.hours + closeds).each do |record| + if record[:status] != 'open' + start_time_minutes = 60 * start_time.hour + start_time.min + end_time_minutes = 60 * end_time.hour + end_time.min + if (record[:start]+1..record[:end]-1).include?(start_time_minutes) || (record[:start]+1..record[:end]-1).include?(end_time_minutes) || + (start_time_minutes < record[:start] && end_time_minutes > record[:end]) + # there is an overlap, this time is invalid + flash :alert, 'Invalid Time Slot', 'Sorry, that time slot is invalid for reservations.' + redirect back + end + end + end + end + # if no record studio is open + + # check for possible other reservations during this time period + other_reservations = Reservation.where(:resource_id => params[:resource_id]).in_day(date).all + other_reservations.each do |reservation| + if (start_time >= reservation.start_time && start_time < reservation.end_time) || + (end_time > reservation.start_time && end_time <= reservation.end_time) || + (start_time < reservation.start_time && end_time > reservation.end_time) + flash :alert, "Tool is being used.", "Sorry, that resource is reserved during that time period. Please try another time slot." + redirect back + elsif reservation.user_id == @user.id + flash :alert, "Over Limit", "Sorry, you can only reserve this resource once per day. Please try reserving another time slot on another day." + redirect back + end + end + + Reservation.create( + :resource_id => resource.id, + :event_id => nil, + :start_time => start_time, + :end_time => end_time, + :is_training => false, + :user_id => @user.id + ) + + flash(:success, 'Reservation Created', "You have successfully reserved #{resource.name} for #{params[:length]} minutes at #{start_time.in_time_zone.strftime('%A, %B %d at %l:%M %P')}") + redirect @space.resources_href +end + + +get '/:service_space_url_name/resources/:resource_id/edit_reservation/:reservation_id/?' do + load_service_space + @breadcrumbs << {:text => 'Resources', :href => @space.resources_href} << {:text => 'Edit Reservation'} + + resource = Resource.find_by(:service_space_id => @space.id, :id => params[:resource_id]) + if resource.nil? + flash(:alert, 'Not Found', 'That resource does not exist.') + redirect @space.resources_href + end + + # check that this reservation exists + reservation = Reservation.find(params[:reservation_id]) + if reservation.nil? + flash(:alert, 'Not Found', 'That reservation does not exist.') + redirect back + end + + date = params[:date].nil? ? reservation.start_time.in_time_zone.midnight : Time.parse(params[:date]).midnight.in_time_zone + # get the studio's hours for this day + # is there a one_off + space_hour = SpaceHour.where(:service_space_id => @space.id) + .where('effective_date = ?', date.utc.strftime('%Y-%m-%d %H:%M:%S')) + .where(:day_of_week => date.wday).where(:one_off => true).first + if space_hour.nil? + space_hour = SpaceHour.where(:service_space_id => @space.id) + .where('effective_date <= ?', date.utc.strftime('%Y-%m-%d %H:%M:%S')) + .where(:day_of_week => date.wday).where(:one_off => false) + .order(:effective_date => :desc, :id => :desc).first + end + + available_start_times = [] + # calculate the available start times for reservation + if space_hour.nil? + start = 0 + while start + (resource.minutes_per_reservation || 15) <= 1440 + available_start_times << start + start += (resource.minutes_per_reservation || 15) + end + else + space_hour.hours.sort{|x,y| x[:start] <=> y[:start]}.each do |record| + if record[:status] == 'open' + start = record[:start] + while start + (resource.minutes_per_reservation || 15) <= record[:end] + available_start_times << start + start += (resource.minutes_per_reservation || 15) + end + end + end + end + + # filter out times when resource is reserved + reservations = Reservation.includes(:event).where(:resource_id => resource.id).in_day(date).all + available_start_times = (available_start_times - reservations.map{|res|res.start_time.in_time_zone.minutes_after_midnight}) + if date == reservation.start_time.in_time_zone.midnight + available_start_times = available_start_times + [reservation.start_time.in_time_zone.minutes_after_midnight] + end + available_start_times.sort! + + erb :reserve, :layout => :fixed, :locals => { + :resource => resource, + :reservations => reservations, + :available_start_times => available_start_times, + :space_hour => space_hour, + :day => date, + :reservation => reservation + } +end + +post '/:service_space_url_name/resources/:resource_id/edit_reservation/:reservation_id/?' do + load_service_space + + resource = Resource.find_by(:service_space_id => @space.id, :id => params[:resource_id]) + if resource.nil? + flash(:alert, 'Not Found', 'That resource does not exist.') + redirect @space.resources_href + end + + # check that this reservation exists + reservation = Reservation.find(params[:reservation_id]) + if reservation.nil? + flash(:alert, 'Not Found', 'That reservation does not exist.') + redirect back + end + + hour = (params[:start_minutes].to_i / 60).floor + am_pm = hour >= 12 ? 'pm' : 'am' + hour = hour % 12 + hour += 12 if hour == 0 + minutes = params[:start_minutes].to_i % 60 + + start_time = calculate_time(params[:date], hour, minutes, am_pm) + end_time = start_time + params[:length].to_i.minutes + + date = start_time.midnight + # validate that the requested time slot falls within the open hours of the day + # get the studio's hours for this day + # is there a one_off + space_hour = SpaceHour.where(:service_space_id => @space.id) + .where('effective_date = ?', date.utc.strftime('%Y-%m-%d %H:%M:%S')) + .where(:day_of_week => date.wday).where(:one_off => true).first + if space_hour.nil? + space_hour = SpaceHour.where(:service_space_id => @space.id) + .where('effective_date <= ?', date.utc.strftime('%Y-%m-%d %H:%M:%S')) + .where(:day_of_week => date.wday).where(:one_off => false) + .order(:effective_date => :desc, :id => :desc).first + end + + unless space_hour.nil? + # figure out where the closed sections need to be + # we can assume that all records in this space_hour are non-intertwined + closed_start = 0 + closed_end = 0 + starts = space_hour.hours.map{|record| record[:start]} + ends = space_hour.hours.map{|record| record[:end]} + closeds = [] + (0..1439).each do |j| + if starts.include?(j) + closed_end = j + closeds << {:status => 'closed', :start => closed_start, :end => closed_end} + closed_start = 0 + closed_end = 0 + end + if ends.include?(j) + closed_start = j + end + end + closed_end = 1440 + closeds << {:status => 'closed', :start => closed_start, :end => closed_end} + + # for each record, ensure that the time does not overlap if the record is not "open" + (space_hour.hours + closeds).each do |record| + if record[:status] != 'open' + start_time_minutes = 60 * start_time.hour + start_time.min + end_time_minutes = 60 * end_time.hour + end_time.min + if (record[:start]+1..record[:end]-1).include?(start_time_minutes) || (record[:start]+1..record[:end]-1).include?(end_time_minutes) || + (start_time_minutes < record[:start] && end_time_minutes > record[:end]) + # there is an overlap, this time is invalid + flash :alert, 'Invalid Time Slot', 'Sorry, that time slot is invalid for reservations.' + redirect back + end + end + end + end + # if no record studio is open + + # check for possible other reservations during this time period + other_reservations = Reservation.where(:resource_id => params[:resource_id]).where.not(:id => reservation.id).in_day(date).all + other_reservations.each do |reservation| + if (start_time >= reservation.start_time && start_time < reservation.end_time) || + (end_time >= reservation.start_time && end_time < reservation.end_time) || + (start_time < reservation.start_time && end_time > reservation.end_time) + flash :alert, "Tool is being used.", "Sorry, that resource is reserved during that time period. Please try another time slot." + redirect back + elsif reservation.user_id == @user.id + flash :alert, "Over Limit", "Sorry, you can only reserve this resource once per day. Please try reserving another time slot on another day." + redirect back + end + end + + reservation.update( + :start_time => start_time, + :end_time => end_time + ) + + flash(:success, 'Reservation Updated', "You have successfully updated your reservation for #{resource.name}: it is now for #{params[:length]} minutes at #{start_time.in_time_zone.strftime('%A, %B %d at %l:%M %P')}") + redirect back +end + +post '/:service_space_url_name/resources/:resource_id/cancel/:reservation_id/?' do + load_service_space + + # check that the user requesting cancel is the same as the one on the reservation + reservation = Reservation.find(params[:reservation_id]) + if reservation.nil? + flash :alert, 'Not Found', 'That reservation was not found.' + redirect back + end + + if reservation.user_id != @user.id + flash :alert, 'Unauthorized', 'That is not your reservation.' + redirect back + end + + reservation.delete + + flash :success, 'Reservation Cancelled', 'Your reservation has been removed.' + redirect back +end + + diff --git a/routes/space.rb b/routes/space.rb new file mode 100644 index 0000000..3aa6969 --- /dev/null +++ b/routes/space.rb @@ -0,0 +1,23 @@ +require 'models/service_space' +require 'models/reservation' +require 'models/event' + +get '/:service_space_url_name/?' do + load_service_space + + reservations = Reservation.joins(:resource).includes(:event). + where(:resources => {:service_space_id => @space.id}). + where(:user_id => @user.id). + where('end_time >= ?', Time.now). + order(:start_time).all + + events = Event.includes(:event_type).joins(:event_signups). + where(:event_signups => {:user_id => @user.id}, :service_space_id => @space.id). + where('end_time >= ?', Time.now). + order(:start_time).all + + erb :space_home, :layout => :fixed, :locals => { + :reservations => reservations, + :events => events + } +end \ No newline at end of file diff --git a/views/admin/events.erb b/views/admin/events.erb new file mode 100644 index 0000000..f5070f7 --- /dev/null +++ b/views/admin/events.erb @@ -0,0 +1,110 @@ +<section class="wdn-grid-set reverse"> + <div class="bp2-wdn-col-three-fourths"> + <div id="pagetitle"> + <h3>Events</h3> + </div> + <ul class="wdn_tabs"> + <li class="<%='selected' if tab == 'upcoming' %>"><a href="?tab=upcoming">Upcoming</a></li> + <li class="<%='selected' if tab == 'past' %>"><a href="?tab=past">Past</a></li> + </ul> + <div class="wdn_tabs_content"> + <div class="event-page"> + <table class="event-list"> + <thead> + <tr> + <th>Title</th> + <th>Date/Location</th> + <th>Signups</th> + <th>Actions</th> + </tr> + </thead> + <tbody> + <% events.each do |event| %> + <tr> + <td class="small-hidden"> + <a href="<%= @space.admin_events_href %><%= event.id %>/edit/"><%= event.title %></a> + </td> + <td> + <ul> + <li> + <%= event.start_time.in_time_zone.strftime('%m/%d/%Y @ %l:%M %P') %><br> + <%= event.location.name %> + </li> + </ul> + </td> + <td> + <a href="<%= @space.admin_events_href %><%= event.id %>/signup_list/"><%= event.signups.count %> signed up</a> + <% unless event.max_signups.nil? %> + <br><%= event.max_signups %> total slots + <% end %> + </td> + <td> + <form class="delete-event delete-form" action="<%= @space.admin_events_href %><%=event.id%>/delete/" method="POST"> + <button type="submit" class="wdn-button">Delete</button> + </form> + </td> + </tr> + <% end %> + </tbody> + </table> + </div> + <% if total_pages > 1 %> + <script type="text/javascript"> + WDN.loadCSS(WDN.getTemplateFilePath('css/modules/pagination.css')); + </script> + <div style="text-align: center;"> + <div style="display: inline-block;"> + <ul id="pending-pagination" class="wdn_pagination" data-tab="pending" style="padding-left: 0;"> + <% if page != 1 %> + <li class="arrow prev"><a href="?tab=<%= tab %>&page=<%= page-1 %>" title="Go to the previous page">← prev</a></li> + <% end %> + <% before_ellipsis_shown = false; after_ellipsis_shown = false %> + <% (1..total_pages).each do |i| %> + <% if i == page %> + <li class="selected"><span><%= i %></span></li> + <% elsif (i <= 3 || i >= total_pages - 2 || i == page - 1 || + i == page - 2 || i == page + 1 || $i == page + 2) %> + <li><a href="?tab=<%= tab %>&page=<%= i %>" title="Go to page <%= i %>"><%= i %></a></li> + <% elsif (i < page && !before_ellipsis_shown) %> + <li><span class="ellipsis">...</span></li> + <% before_ellipsis_shown = true %> + <% elsif (i > page && !after_ellipsis_shown) %> + <li><span class="ellipsis">...</span></li> + <% after_ellipsis_shown = true %> + <% end %> + <% end %> + <% if page != total_pages %> + <li class="arrow next"><a href="?tab=<%= tab %>&page=<%= page+1 %>" title="Go to the next page">next →</a></li> + <% end %> + </ul> + </div> + </div> + <% end %> + </div> + </div> + <nav class="bp2-wdn-col-one-fourth"> + <div class="toolbox"> + <h3>Toolbox</h3> + <div class="tools"> + <div style="text-align: center; margin-bottom: .8em"> + <a class="wdn-button wdn-button-brand" href="<%= @space.admin_events_href %>create/"> + <span style="font-size: 2em; vertical-align: middle; font-weight: 600">+</span> + <span style="vertical-align: middle;">New Event</span> + </a> + </div> + </div> + </div> + </nav> +</section> + +<script type="text/javascript"> +require(['jquery'], function($) { + $(document).ready(function() { + $('.delete-event').submit(function (submit) { + if (!window.confirm('Are you sure you want to delete this event?')) { + submit.preventDefault(); + } + }); + }); +}); +</script> \ No newline at end of file diff --git a/views/admin/home.erb b/views/admin/home.erb new file mode 100644 index 0000000..d4240e1 --- /dev/null +++ b/views/admin/home.erb @@ -0,0 +1,62 @@ +<div id="pagetitle"> + <h3>NIS Manager Administration</h3> + <span class="wdn-subhead">Hello, <%= @user.full_name %> (<%= @user.username %>) <% if @user.is_super_user?(@space) %><br>Super User<% end %></span> +</div> + +<table> + <tbody> + <% if @user.has_permission?(Permission::MANAGE_USERS, @space) || @user.has_permission?(Permission::SUPER_USER, @space) %> + <tr> + <td><strong>Users</strong></td> + <td><%= user_count %> users</td> + <td><a class="wdn-button wdn-button-brand" href="/admin/users/">Manage</a></td> + </tr> + <% end %> + <% if @user.has_permission?(Permission::MANAGE_EVENTS, @space) %> + <tr> + <td><strong>Events</strong></td> + <td><%= upcoming_event_count %> upcoming events</td> + <td><a class="wdn-button wdn-button-brand" href="<%= @space.admin_events_href %>">Manage</a></td> + </tr> + <% end %> + <% if @user.has_permission?(Permission::MANAGE_RESOURCES, @space) %> + <tr> + <td><strong>Resources</strong></td> + <td><%= resource_count %> resources</td> + <td><a class="wdn-button wdn-button-brand" href="/admin/tools/">Manage</a></td> + </tr> + <% end %> + <% if @user.has_permission?(Permission::MANAGE_SPACE_HOURS, @space) %> + <tr> + <td><strong>Hours</strong></td> + <td>Today: +<% unless space_hour.nil? %> + <%= space_hour.hours.map do |record| + start_time = date + record[:start].minutes + end_time = date + record[:end].minutes + "#{record[:status].capitalize_all}: #{start_time.in_time_zone.strftime('%l:%M %P')} - #{end_time.in_time_zone.strftime('%l:%M %P')}" + end.join(', ') %> +<% else %> + The space is open all day. +<% end %> + </td> + <td><a class="wdn-button wdn-button-brand" href="/admin/hours/">Manage</a></td> + </tr> + <% end %> + <% if @user.has_permission?(Permission::MANAGE_EMAILS, @space) %> + <tr> + <td><strong>Emails</strong></td> + <td></td> + <td><a class="wdn-button wdn-button-brand" href="/admin/emails/">Manage</a></td> + </tr> + <% end %> + <% if @user.has_permission?(Permission::SEE_AGENDA, @space) %> + <tr> + <td><strong>Agenda</strong></td> + <td></td> + <td><a class="wdn-button wdn-button-brand" href="/admin/agenda/">View</a></td> + </tr> + <% end %> + </tbody> +</table> + diff --git a/views/admin/new_event.erb b/views/admin/new_event.erb new file mode 100644 index 0000000..1bfa18d --- /dev/null +++ b/views/admin/new_event.erb @@ -0,0 +1,177 @@ +<div class="wdn-grid-set"> + <form id="create-event-form" action="" method="POST" enctype="multipart/form-data"> + <div class="bp3-wdn-col-two-thirds"> + <fieldset> + <legend style="margin-top: 0">Event Details</legend> + <label for="title"><span class="required">*</span> Title</label> + <input type="text" id="title" name="title" value="<%= event.title %>"/> + + <label for="description">Description</label> + <textarea rows="4" id="description" name="description"><%= event.description %></textarea> + + <label for="type">Type</label> + <select id="type" name="type" class="use-select2" style="width: 100%;"> + <% types.each do |type| %> + <option <%= 'selected="selected"' if !event.type.nil? && event.type.id == type.id %> value="<%= type.id %>"><%= type.description %></option> + <% end %> + </select> + <br><br> + <div> + <input type="checkbox" <%= 'checked="checked"' unless event.max_signups.nil? %> id="limit-signups" name="limit_signups"><label for="limit-signups">Limit signups for this event to: </label> + <input value="<%= event.max_signups %>" type="number" id="max-signups" name="max_signups" style="width: 100px;" /> + </div> + </fieldset> + + <fieldset> + <legend style="font-size: 1.6em">Location, Date, and Time</legend> + <label for="location"><span class="required">*</span> Location</label> + <select id="location" name="location" class="use-select2" style="width: 100%;"> + <% locations.each do |location| %> + <option <%= 'selected="selected"' if !event.location.nil? && event.location.id == location.id %> value="<%= location.id %>"><%= location.name %></option> + <% end %> + <option value="new">-- New Location --</option> + </select> + + <div style="display: none;" class="offset-field-group" id="new-location-details"> + <label for="location-name"><span class="required">*</span> Name</label> + <input type="text" id="location-name" name="new_location[name]" /> + + <label for="location-address">Address</label> + <input type="text" id="location-address" name="new_location[streetaddress]" /> + + <label for="location-address2">Address 2</label> + <input type="text" id="location-address2" name="new_location[streetaddress2]" /> + + <label for="location-city">City</label> + <input type="text" id="location-city" name="new_location[city]" /> + + <label for="location-state">State</label> + <input type="text" id="location-state" name="new_location[state]" /> + + <label for="location-zip">Zip</label> + <input type="text" id="location-zip" name="new_location[zip]" /> + + <label for="location-additionalinfo">Additional Info</label> + <input type="text" id="location-additionalinfo" name="new_location[additionalinfo]" /> + + <label>* This location will be saved for future use</label> + </div> + + <div> + <input type="checkbox" <%= 'checked="checked"' if !event.reservation.nil? && !event.reservation.resource.nil? %> id="reserve-resource" name="reserve_resource"><label for="reserve-resource">Reserve a resource for this event</label> + <div id="resources-for-reserving" style="<%= 'display: none;' if event.reservation.nil? || event.reservation.resource.nil? %>"> + <label for="resource">Resource</label> + <select id="resource" name="resource" class="use-select2" style="width: 100%;"> + <% resources.each do |resource| %> + <option <%= 'selected="selected"' if !event.reservation.nil? && !event.reservation.resource.nil? && event.reservation.resource.id == resource.id %> value="<%= resource.id %>"><%= "#{resource.name} - #{resource.model}" %></option> + <% end %> + </select> + </div> + </div> + <br> + + <label for="start-date" ><span class="required">*</span> Start Date & Time</label> + <div class="date-time-select"><span class="wdn-icon-calendar"></span> + <input id="start-date" value="<%= event.start_time.in_time_zone.strftime('%m/%d/%Y') if !event.start_time.nil? %>" name="start_date" title="Start Date" type="text" class="datepicker" /><br class="hidden small-block"> @ + <select id="start-time-hour" name="start_time_hour" title="Start Time Hour"> + <option value=""></option> + <% (1..12).each do |i| %> + <option <%= 'selected="selected"' if !event.start_time.nil? && event.start_time.in_time_zone.hour.to_i % 12 == i % 12 %> value="<%= i %>"><%= i %></option> + <% end %> + </select> : + + <select id="start-time-minute" name="start_time_minute" title="Start Time Minute"> + <option value=""></option> + <% (0..11).each do |i| %> + <option <%= 'selected="selected"' if !event.start_time.nil? && event.start_time.in_time_zone.min == i*5 %> value="<%= i * 5 %>"><%= (i*5).to_s.rjust(2, '0') %></option> + <% end %> + </select> + + <div id="start-time-am-pm" class="am_pm"> + <input <%= 'checked="checked"' if event.start_time.nil? || event.start_time.in_time_zone.hour < 12 %> id="start-time-am-pm-am" title="AM" type="radio" value="am" name="start_time_am_pm">AM<br> + <input <%= 'checked="checked"' if !event.start_time.nil? && event.start_time.in_time_zone.hour >= 12 %> id="start-time-am-pm-pm" title="PM" type="radio" value="pm" name="start_time_am_pm">PM + </div> + </div> + + <label for="end-date">End Date & Time (optional)</label> + <div class="date-time-select"><span class="wdn-icon-calendar"></span> + <input id="end-date" value="<%= event.end_time.in_time_zone.strftime('%m/%d/%Y') if !event.end_time.nil? %>" name="end_date" title="End Date" type="text" class="datepicker" /><br class="hidden small-block"> @ + <select id="end-time-hour" name="end_time_hour" title="End Time Hour"> + <option value=""></option> + <% (1..12).each do |i| %> + <option <%= 'selected="selected"' if !event.end_time.nil? && event.end_time.in_time_zone.hour.to_i % 12 == i % 12 %> value="<%= i %>"><%= i %></option> + <% end %> + </select> : + + <select id="end-time-minute" name="end_time_minute" title="End Time Minute"> + <option value=""></option> + <% (0..11).each do |i| %> + <option <%= 'selected="selected"' if !event.end_time.nil? && event.end_time.in_time_zone.min == i*5 %> value="<%= i * 5 %>"><%= (i*5).to_s.rjust(2, '0') %></option> + <% end %> + </select> + + <div id="end-time-am-pm" class="am_pm"> + <input <%= 'checked="checked"' if event.end_time.nil? || event.end_time.in_time_zone.hour < 12 %> id="end-time-am-pm-am" title="AM" type="radio" value="am" name="end_time_am_pm">AM<br> + <input <%= 'checked="checked"' if !event.end_time.nil? && event.end_time.in_time_zone.hour >= 12 %> id="end-time-am-pm-pm" title="PM" type="radio" value="pm" name="end_time_am_pm">PM + </div> + </div> + </fieldset> + + </div> + <div class="bp3-wdn-col-one-third"> + <div class="visual-island"> + <div class="vi-header"> + <label>Image</label> + </div> + + <div class="details"> + <% unless event.imagedata.nil? %> + <img src="<%= event.image_src %>" alt="Image for Event <%= event.title %>"> + <br> + <input type="checkbox" name="remove_image" id="remove-image"> + <label for="remove-image">Remove Image</label> + <% end %> + <input style="font-size: 10px;" type="file" name="imagedata" id="imagedata" title="Event Image"> + </div> + </div> + </div> + + <div class="bp1-wdn-col-two-thirds"> + <button class="wdn-button wdn-button-brand wdn-pull-left" type="submit"><%= event.id.nil? ? 'Create' : 'Save' %> Event</button> + </div> + </form> +</div> +<br> + +<script type="text/javascript"> +WDN.initializePlugin('jqueryui', [function() { + $ = require('jquery'); + $('.datepicker').datepicker(); + $("LINK[href^='//unlcms.unl.edu/wdn/templates_4.0/scripts/plugins/ui/css/jquery-ui.min.css']").remove(); + + $('#reserve-resource').click(function(click) { + if ($('#reserve-resource').is(':checked')) { + $('#resources-for-reserving').show(); + } else { + $('#resources-for-reserving').hide(); + } + }); + + $('#location').change(function (change) { + if ($(this).val() == 'new') { + $('#new-location-details').show(); + } else { + $('#new-location-details').hide(); + } + }).change(); + + $('#export-to-unl-events').change(function (change) { + if ($('#export-to-unl-events').is(':checked')) { + $('#consider-for-unl-main').removeAttr('disabled'); + } else { + $('#consider-for-unl-main').attr('checked', false); + $('#consider-for-unl-main').attr('disabled', 'disabled'); + } + }).change(); +}]); +</script> \ No newline at end of file diff --git a/views/calendar.erb b/views/calendar.erb index cf013cf..b574847 100644 --- a/views/calendar.erb +++ b/views/calendar.erb @@ -13,8 +13,8 @@ end %> <h4 style="text-align: center; margin: 0;"> <%= month = sunday.strftime('%B %Y') %><%= (month2 = (sunday+6.days).strftime('%B %Y')) == month ? '' : " - #{month2}" %> </h4> -<a href="/calendar/?date=<%= (date-7.days).strftime('%Y-%m-%d') %>" class="wdn-button wdn-button-triad" id="prev-week">< PREV</a> -<a href="/calendar/?date=<%= (date+7.days).strftime('%Y-%m-%d') %>" class="wdn-button wdn-button-triad" style="float: right;" id="next-week">NEXT ></a> +<a href="/<%= @space.url_name %>/calendar/?date=<%= (date-7.days).strftime('%Y-%m-%d') %>" class="wdn-button wdn-button-triad" id="prev-week">< PREV</a> +<a href="/<%= @space.url_name %>/calendar/?date=<%= (date+7.days).strftime('%Y-%m-%d') %>" class="wdn-button wdn-button-triad" style="float: right;" id="next-week">NEXT ></a> </div> <div class="calendar-container"> diff --git a/views/event_details.erb b/views/event_details.erb new file mode 100644 index 0000000..82ab061 --- /dev/null +++ b/views/event_details.erb @@ -0,0 +1,62 @@ +<div class="event-details"> + <div> + <h3> + <%= event.title %><span class="wdn-subhead"><%= event.type.description %></span> + </h3> + </div> + <div> + <span class="date-wrapper eventicon-calendar-empty"> + <time class="dtstart"><%= event.start_time.in_time_zone.strftime('%b %d, %Y') %></time> + </span> + <span class="time-wrapper eventicon-clock"> + <%= event.start_time.in_time_zone.strftime('%l:%M %P') %>–<%= event.end_time.in_time_zone.strftime('%l:%M %P') %> + </span> + <div class="location eventicon-location"> + <%= event.location.name %> + </div> + <% unless event.imagedata.nil? %> + <div class="inset-image"> + <img src="<%= event.image_src %>" alt="Image for Event: <%= event.title %>"> + </div> + <% end %> + <div class="description"> + <%= event.description.nl2br.force_encoding("UTF-8") %> + </div> + </div> +</div> + +<% # free events do not require signup but users can mark it on their homepage %> +<% if @user && event.signups.map(&:user_id).include?(@user.id) %> + <% # the user is already signed up %> + <% if event.type.description == 'Free Event' %> + This event is noted on your homepage.<br> + <a href="<%= @space.href %>" class="wdn-button wdn-button-triad">View Homepage</a> + <% else %> + You have signed up for this event.<br> + <a href='<%= @space.href %>' class="wdn-button wdn-button-triad">View Homepage</a> + <% end %> +<% elsif @user %> + <% # the user is logged in but not signed up %> + <% if event.max_signups.nil? || event.signups.count < event.max_signups %> + <form action="/<%= @space.url_name %>/events/<%= event.id %>/sign_up/" method="POST"> + <button type="submit" class="wdn-button wdn-button-brand"> + <% if event.type.description == 'Free Event' %> + Note event on my homepage + <% else %> + Sign up for this event + <% end %> + </a> + </form> + <% else %> + All slots for this event are filled. + <% end %> +<% else %> + <% # a non user. May still sign up for the event UNLESS it is a tool training %> + <% if event.type.description != 'Machine Training' %> + <% if event.max_signups.nil? || event.signups.count < event.max_signups %> + <a class="wdn-button wdn-button-brand" href="/<%= @space.url_name %>/events/<%= event.id %>/sign_up_as_non_member/">Sign up for this event</a> + <% else %> + All slots for this event are filled. + <% end %> + <% end %> +<% end %> \ No newline at end of file diff --git a/views/home.erb b/views/home.erb new file mode 100644 index 0000000..5a6605c --- /dev/null +++ b/views/home.erb @@ -0,0 +1,16 @@ +<div id="pagetitle"> + <h3>Welcome to UNL Resource Scheduler<span class="wdn-subhead"><%= @user.full_name %></span></h3> +</div> + +<% if spaces.empty? %> + Sorry, you don't have any spaces right now. If you'd like to use UNL Resource Scheduler for your organization, please request a service space here. +<% else %> +<h5 class="wdn-center wdn-brand">Your Service Spaces</h5> +<div class="wdn-grid-set-thirds"> +<% spaces.each do |space| %> + <div class="wdn-col"> + <a href="<%= space.href %>"><%= space.name %></a> + </div> +<% end %> +</div> +<% end %> \ No newline at end of file diff --git a/views/reserve.erb b/views/reserve.erb new file mode 100644 index 0000000..ec07065 --- /dev/null +++ b/views/reserve.erb @@ -0,0 +1,187 @@ +<% +start_hour = nil +start_minute = nil +start_am_pm = nil +unless reservation.nil? + start_hour = reservation.start_time.in_time_zone.hour + if start_hour >= 12 + start_hour -= 12 + start_am_pm = 'pm' + else + start_am_pm = 'am' + end + start_hour += 12 if start_hour == 0 + start_minute = reservation.start_time.in_time_zone.min +end %> + +<div id="pagetitle"> + <h3><%= reservation.nil? ? 'Reserve Time for ' : 'Edit Reservation for ' %><%= resource.name %></h3> +</div> + +<form action="" method="POST"> +<section class="wdn-grid-set"> + <div class="bp1-wdn-col-one-third"> + <label for="date">Date</label> + <div class="date-time-select"> + <span class="wdn-icon-calendar"></span> + <input style="width: 90%;" id="date" name="date" title="Reservation Date" type="text" class="datepicker" value="<%= day.strftime('%m/%d/%Y') %>" /> + </div> + </div> + <div class="bp1-wdn-col-one-third"> + <label>Schedule for <span id="current-date"><%= day.strftime('%m/%d/%Y') %></span></label> + <div class="calendar-container individual-day"> + <div class="time-labels"> + <div class="time-chart"> + <% (12..47).each do |j| %> + <div class="calendar-half-hour"> + <label><%= "#{(j / 2) % 12 + (j==24?12:0)} #{j>=24?'PM':'AM'}" if j % 2 == 0 %></label> + </div> + <% end %> + </div> + </div> + <% slots = [0] * 36 %> + <div class="calendar-day" data-day="<%= day.strftime("%Y/%m/%d") %>"> + <label class="day-header"><%= day.strftime("%^a %-m/%d") %></label> + <div class="day-chart" title="Open"> + <% reservations.each do |res| %> + <% end_slot = [(((res.end_time.in_time_zone - day.midnight) / 60 - 360) / 30).floor, 35].min + if end_slot < 0 + next + end + top = (((res.start_time.in_time_zone - day.midnight) / 60 - 360) / 30) * 20 + height = res.length * 20 / 30 + %> + <div class="reservation <%= 'editing' if !reservation.nil? && reservation.id == res.id %>" + style="top: <%= top %>px; height: <%= height %>px;"> + <% if !res.event.nil? %> + <%= res.event.title %> + <% else %> + <%= res.user_id == @user.id ? 'My Reservation' : 'busy' %> + <% end %> + <%= '(Editing)' if !reservation.nil? && reservation.id == res.id %> + </div> + <% end %> + <% unless space_hour.nil? + # figure out where the closed divs need to be + # we can assume that all records in this space_hour are non-intertwined + closed_start = 0 + closed_end = 0 + starts = space_hour.hours.map{|record| record[:start]} + ends = space_hour.hours.map{|record| record[:end]} + %> <% + closeds = [] + (360..1439).each do |j| + if starts.include?(j) + closed_end = j + closeds << [closed_start, closed_end] + closed_start = 0 + closed_end = 0 + end + if ends.include?(j) + closed_start = j + end + end + closed_end = 1440 + closeds << [closed_start, closed_end] + + closeds.each do |closed| + start_time = closed[0] %> + <% end_time = closed[1] %> + <% + if [((end_time - 360) / 30).floor, 35].min < 0 + next + end + top = ((start_time - 360) / 30) * 20 + height = (end_time - start_time) * 20 / 30 + if top < 0 + height += top + top = 0 + end + if top + height > 720 + height = 720 - top + end + %> + <div class="status closed" title="Closed" style="top: <%= top %>px; height: <%= height %>px;"> + + </div> + <% + end %> + <% space_hour.hours.each do |record| %> + <% if record[:status] != 'open' && record[:status] != 'closed' %> + <% start_time = record[:start] %> + <% end_time = record[:end] %> + <% if [((end_time - 360) / 30).floor, 35].min < 0 + next + end + top = ((start_time - 360) / 30) * 20 + height = (end_time - start_time) * 20 / 30 + if top < 0 + height += top + top = 0 + end + if top + height > 720 + height = 720 - top + end + %> + <div title="<%= record[:status].split('_').join(' ').capitalize_all %>" class="status <%= record[:status].downcase.split('_').join('-') %>" style="top: <%= top %>px; height: <%= height %>px;"> + + </div> + <% end %> + <% end %> + <% end %> + <div> + <% (12..47).each do |j| %> + <div class="calendar-half-hour"> + + </div> + <% end %> + </div> + </div> + </div> + </div> + </div> + <div class="bp1-wdn-col-one-third"> + <label for="start-minutes">Start Time</label> + <div class="date-time-select"> + <% if available_start_times.empty? %> + No available times today. + <% else %> + <select id="start-minutes" name="start_minutes" style="width: 90%"> + <% available_start_times.each do |minutes| %> + <option <%='selected="selected"' if !reservation.nil? && reservation.start_time.in_time_zone.midnight == day && reservation.start_time.in_time_zone.minutes_after_midnight == minutes %> value="<%= minutes %>"><%= Time.from_minutes(minutes).strftime("%l:%M %p") %></option> + <% end %> + </select> + <% end %> + </div> + + <label for="reservation-length">Reserve resource for:</label><br> + <% if resource.minutes_per_reservation.nil? %> + <select id="reservation-length" name="length"> + <% (1..4).each do |i| %> + <option value="<%=i*15%>"><%=i*15%> minutes</option> + <% end %> + <option value="90">1.5 hours</option> + <option value="120">2 hours</option> + </select> + <% else %> + <input style="width: 50px" disabled="disabled" value="<%= resource.minutes_per_reservation %>" /> <label>minutes</label> + <input type="hidden" name="length" value="<%= resource.minutes_per_reservation %>" /> + <% end %> + <br><br> + <button type="submit" class="wdn-button wdn-button-brand"><%= reservation.nil? ? 'Reserve' : 'Update' %></button> + </div> +</section> +</form> + +<script type="text/javascript"> +WDN.initializePlugin('jqueryui', [function() { + $ = require('jquery'); + $('.datepicker').datepicker(); + $("LINK[href^='//unlcms.unl.edu/wdn/templates_4.0/scripts/plugins/ui/css/jquery-ui.min.css']").remove(); + + $('#date').change(function () { + var date = $('#date').val().split('/'); + window.location = window.location.href.split('?')[0] + '?date=' + date[2] + '-' + date[0] + '-' + date[1]; + }); +}]); +</script> \ No newline at end of file diff --git a/views/resources.erb b/views/resources.erb new file mode 100644 index 0000000..77821f5 --- /dev/null +++ b/views/resources.erb @@ -0,0 +1,34 @@ +<div id="pagetitle"> +<h3>Resources You Can Reserve</h3> +</div> + +<% if available_resources.count > 0 %> +<table> + <thead> + <tr> + <th>Resource</th> + <th>Actions</th> + </tr> + </thead> + <tbody> + <% available_resources.each do |resource| %> + <tr> + <td> + <%= resource.name %> + </td> + <td class="table-actions"> + <% if resource.is_reservable %> + <a href="/<%= @space.url_name %>/resources/<%= resource.id %>/reserve/" class="wdn-button wdn-button-brand">Reserve Time</a> + <% else %> + Reservation not required + <% end %> + </td> + </tr> + <% end %> + </tbody> +</table> +<% else %> +<p> + There are no resources currently available for you to reserve. +</p> +<% end %> \ No newline at end of file diff --git a/views/space_home.erb b/views/space_home.erb new file mode 100644 index 0000000..7a6f3f6 --- /dev/null +++ b/views/space_home.erb @@ -0,0 +1,85 @@ +<div id="pagetitle"> + <h3>Welcome to <%= @space.name %> Resource Scheduler<span class="wdn-subhead">Hello, <%= @user.full_name %></span></h3> +</div> + +<h4> +My Reservations +</h4> +<% if reservations.empty? %> +You have no upcoming reservations. You can view upcoming trainings to get certified, or check out the list of resources you can reserve.<br> +<a href="<%= @space.resources_href %>" class="wdn-button wdn-button-brand">View Resources</a> +<% else %> +<table> + <thead> + <tr> + <th>Tool</th> + <th>Model</th> + <th>Time</th> + <th>Actions</th> + </tr> + </thead> + <tbody> + <% reservations.each do |reservation| %> + <tr> + <td> + <%= reservation.resource.name %> + <% if !reservation.event.nil? %> + <br><small><%= reservation.event.title %></small> + <% end %> + </td> + <td> + <%= reservation.resource.model %> + </td> + <td> + <%= reservation.start_time.in_time_zone.strftime('%m/%d/%Y @ %l:%M %P') %><br> + <%= reservation.length %> minutes + </td> + <td class="table-actions"> + <a href="/<%= @space.url_name %>/resources/<%= reservation.resource.id %>/edit_reservation/<%= reservation.id %>/" class="wdn-button wdn-button-brand">Edit</a> + <form method="POST" action="/<%= @space.url_name %>/resources/<%= reservation.resource.id %>/cancel/<%= reservation.id %>/" class="delete-form"> + <button class="wdn-button" type="submit">Remove</button> + </form> + </td> + </tr> + <% end %> + </tbody> +</table> +<% end %> + +<h4> +My Events +</h4> +<% if events.empty? %> +You have not signed up for any upcoming events. Why not check out the calendar to find some?<br> +<a href="<%= @space.calendar_href %>" class="wdn-button wdn-button-triad">View Calendar</a> +<% else %> +<table> + <thead> + <tr> + <th>Title</th> + <th>Date/Location</th> + <th>Actions</th> + </tr> + </thead> + <tbody> + <% events.each do |event| %> + <tr> + <td> + <a href="<%= event.info_link %>"><%= event.title %></a> + </td> + <td> + <%= event.start_time.in_time_zone.strftime('%m/%d/%Y @ %l:%M %P') %><br> + <%= event.location.name %> + </td> + <td class="table-actions"> + <form action="/events/<%= event.id %>/remove_signup/" method="POST" class="delete-form"> + <button class="wdn-button" type="submit"> + Remove + </button> + </form> + </td> + </tr> + <% end %> + </tbody> +</table> +<% end %> \ No newline at end of file diff --git a/views/template_partials/navigation.erb b/views/template_partials/navigation.erb index 22c6536..7ba36e9 100644 --- a/views/template_partials/navigation.erb +++ b/views/template_partials/navigation.erb @@ -1,3 +1,30 @@ <ul> - <li><a href="/" title="UNL Resource Scheduler">Home</a> + <li><a href="/" title="UNL Resource Scheduler">Home</a></li> + <% unless @space.nil? %> + <li><a href="<%= @space.calendar_href %>">Calendar</a></li> + <% unless @user.nil? || !@user.is_admin?(@space) %> + <li><a href="<%= @space.admin_href %>" title="Admin">Admin</a> + <ul> + <% if @user.has_permission?(Permission::MANAGE_USERS, @space) || @user.has_permission?(Permission::SUPER_USER, @space) %> + <li><a href="/<%= @space.url_name %>/admin/users/" title="Users">Users</a></li> + <% end %> + <% if @user.has_permission?(Permission::MANAGE_EVENTS, @space) %> + <li><a href="/<%= @space.url_name %>/admin/events/" title="Events">Events</a></li> + <% end %> + <% if @user.has_permission?(Permission::MANAGE_RESOURCES, @space) %> + <li><a href="/<%= @space.url_name %>/admin/resources/" title="Resources">Resources</a></li> + <% end %> + <% if @user.has_permission?(Permission::MANAGE_SPACE_HOURS, @space) %> + <li><a href="/<%= @space.url_name %>/admin/hours/" title="Hours">Hours</a></li> + <% end %> + <% if @user.has_permission?(Permission::MANAGE_EMAILS, @space) %> + <li><a href="/<%= @space.url_name %>/admin/email/" title="Email">Email</a></li> + <% end %> + <% if @user.has_permission?(Permission::SEE_AGENDA, @space) %> + <li><a href="/<%= @space.url_name %>/admin/agenda/" title="Agenda">Agenda</a></li> + <% end %> + </ul> + </li> + <% end %> + <% end %> </ul> -- GitLab