Merge pull request #2 from zyphlar/master

Updating hsl repo
This commit is contained in:
Will Bradley 2014-07-06 17:01:02 -07:00
commit 3cb1d4e730
269 changed files with 10851 additions and 362 deletions

2
.gitignore vendored
View File

@ -21,5 +21,7 @@ tmp/
# Ignore config and database files (passwords)
/config/config.yml
/config/s3.yml
/config/database.yml
/config/initializers/secret_token.rb
.env

1
.ruby-gemset Normal file
View File

@ -0,0 +1 @@
members-hsl

1
.ruby-version Normal file
View File

@ -0,0 +1 @@
ruby-1.9.3-p385

35
.rvmrc
View File

@ -1,35 +0,0 @@
#!/usr/bin/env bash
# This is an RVM Project .rvmrc file, used to automatically load the ruby
# development environment upon cd'ing into the directory
# First we specify our desired <ruby>[@<gemset>], the @gemset name is optional,
# Only full ruby name is supported here, for short names use:
# echo "rvm use 1.8.7" > .rvmrc
environment_id="ruby-1.9.3-p385@members-hsl"
# Uncomment the following lines if you want to verify rvm version per project
# rvmrc_rvm_version="1.18.8 (stable)" # 1.10.1 seams as a safe start
# eval "$(echo ${rvm_version}.${rvmrc_rvm_version} | awk -F. '{print "[[ "$1*65536+$2*256+$3" -ge "$4*65536+$5*256+$6" ]]"}' )" || {
# echo "This .rvmrc file requires at least RVM ${rvmrc_rvm_version}, aborting loading."
# return 1
# }
# First we attempt to load the desired environment directly from the environment
# file. This is very fast and efficient compared to running through the entire
# CLI and selector. If you want feedback on which environment was used then
# insert the word 'use' after --create as this triggers verbose mode.
if [[ -d "${rvm_path:-$HOME/.rvm}/environments"
&& -s "${rvm_path:-$HOME/.rvm}/environments/$environment_id" ]]
then
\. "${rvm_path:-$HOME/.rvm}/environments/$environment_id"
[[ -s "${rvm_path:-$HOME/.rvm}/hooks/after_use" ]] &&
\. "${rvm_path:-$HOME/.rvm}/hooks/after_use" || true
else
# If the environment file has not yet been created, use the RVM CLI to select.
rvm --create "$environment_id" || {
echo "Failed to create RVM environment '${environment_id}'."
return 1
}
fi

View File

@ -3,6 +3,7 @@ source 'https://rubygems.org'
ruby '1.9.3'
gem 'rails', '3.2.8'
gem 'dotenv-rails'
# Bundle edge Rails instead:
# gem 'rails', :git => 'git://github.com/rails/rails.git'
@ -46,7 +47,8 @@ gem 'bcrypt-ruby', '~> 3.0.0'
# To use debugger
#gem 'debugger'
#gem "paperclip", "~> 3.0"
gem "paperclip", "~> 3.0"
gem "aws-sdk"
gem 'gravtastic'
gem 'passenger'

View File

@ -29,9 +29,17 @@ GEM
i18n (~> 0.6)
multi_json (~> 1.0)
arel (3.0.2)
aws-sdk (1.33.0)
json (~> 1.4)
nokogiri (>= 1.4.4)
uuidtools (~> 2.1)
bcrypt-ruby (3.0.1)
builder (3.0.4)
cancan (1.6.10)
climate_control (0.0.3)
activesupport (>= 3.0)
cocaine (0.5.3)
climate_control (>= 0.0.3, < 1.0)
coffee-rails (3.2.2)
coffee-script (>= 2.2.0)
railties (~> 3.2.0)
@ -45,6 +53,9 @@ GEM
orm_adapter (~> 0.1)
railties (~> 3.1)
warden (~> 1.2.1)
dotenv (0.10.0)
dotenv-rails (0.10.0)
dotenv (= 0.10.0)
erubis (2.7.0)
execjs (2.0.2)
gravtastic (3.2.6)
@ -61,8 +72,16 @@ GEM
mime-types (~> 1.16)
treetop (~> 1.4.8)
mime-types (1.25)
mini_portile (0.5.2)
multi_json (1.8.2)
nokogiri (1.6.1)
mini_portile (~> 0.5.0)
orm_adapter (0.4.0)
paperclip (3.5.4)
activemodel (>= 3.0.0)
activesupport (>= 3.0.0)
cocaine (~> 0.5.3)
mime-types
passenger (4.0.19)
daemon_controller (>= 1.1.0)
rack
@ -129,6 +148,7 @@ GEM
uglifier (2.2.1)
execjs (>= 0.3.0)
multi_json (~> 1.0, >= 1.0.2)
uuidtools (2.1.4)
warden (1.2.3)
rack (>= 1.0)
@ -136,13 +156,16 @@ PLATFORMS
ruby
DEPENDENCIES
aws-sdk
bcrypt-ruby (~> 3.0.0)
cancan
coffee-rails (~> 3.2.1)
devise
dotenv-rails
gravtastic
jquery-rails
json
paperclip (~> 3.0)
passenger
pg
rails (= 3.2.8)

View File

@ -5,17 +5,18 @@ via Ethernet ( see: https://github.com/zyphlar/Open_Access_Control_Ethernet )
https://github.com/zyphlar/Open-Source-Access-Control-Web-Interface
Copyright Will Bradley, 2012-2013
Copyright Will Bradley, 2012-2014
Distributed under a Creative Commons Attribution 3.0 license http://creativecommons.org/licenses/by/3.0/
Contributions welcome! Simply send a pull request via Github.
To use:
* Install Imagemagick (for Paperclip / image uploads)
* Install arp-scan (for LAN Mac address scanning)
* Load into a Rails 3 environment
* Copy config/config.yml.example to config/config.yml and edit appropriately
* Copy config/database.yml.example to config/database.yml and edit appropriately
* Copy env.example to .env and edit appropriately for your Amazon S3 account OR adjust the resource.rb and contract.rb model settings to use different storage for picture attachments (via Paperclip)
* Copy config/initializers/secret_token.rb.example to config/config/initializers/secret_token.rb and edit appropriately
* Run bundle install, rake db:migrate, etc.
* Use the Rails console to create a new User with admin rights
* user = User.new
* user.password = "foobar"
* user.admin = true
* user.save
* See/edit db/seeds.rb for the initial admin account info.
* Run bundle install, rake db:migrate, rake db:seed, etc.

0
app/assets/images/logo.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

0
app/assets/images/nil.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 95 B

After

Width:  |  Height:  |  Size: 95 B

0
app/assets/javascripts/application.js Normal file → Executable file
View File

0
app/assets/javascripts/certifications.js.coffee Normal file → Executable file
View File

0
app/assets/javascripts/door_logs.js.coffee Normal file → Executable file
View File

0
app/assets/javascripts/home.js.coffee Normal file → Executable file
View File

0
app/assets/javascripts/ipn.js.coffee Normal file → Executable file
View File

0
app/assets/javascripts/mac_logs.js.coffee Normal file → Executable file
View File

0
app/assets/javascripts/pamela.js.coffee Normal file → Executable file
View File

0
app/assets/javascripts/payments.js.coffee Normal file → Executable file
View File

0
app/assets/javascripts/user_certifications.js.coffee Normal file → Executable file
View File

0
app/assets/javascripts/users.js.coffee Normal file → Executable file
View File

0
app/assets/stylesheets/application.css Normal file → Executable file
View File

0
app/assets/stylesheets/certifications.css.scss Normal file → Executable file
View File

0
app/assets/stylesheets/door_logs.css.scss Normal file → Executable file
View File

0
app/assets/stylesheets/home.css.scss Normal file → Executable file
View File

0
app/assets/stylesheets/ipn.css.scss Normal file → Executable file
View File

0
app/assets/stylesheets/mac_logs.css.scss Normal file → Executable file
View File

0
app/assets/stylesheets/macs.css.scss Normal file → Executable file
View File

0
app/assets/stylesheets/payments.css.scss Normal file → Executable file
View File

11
app/assets/stylesheets/scaffolds.css.scss Normal file → Executable file
View File

@ -19,17 +19,6 @@ pre {
font-size: 11px;
}
a {
color: #000;
&:visited {
color: #666;
}
&:hover {
color: #fff;
background-color: #000;
}
}
div {
&.field, &.actions {
margin-bottom: 10px;

0
app/assets/stylesheets/user_certifications.css.scss Normal file → Executable file
View File

0
app/assets/stylesheets/users.css.scss Normal file → Executable file
View File

25
app/controllers/application_controller.rb Normal file → Executable file
View File

@ -1,6 +1,17 @@
class ApplicationController < ActionController::Base
protect_from_forgery
force_ssl if: :ssl_forced?
def ssl_forced?
# Non-production environments and read-only stuff like the space API and MACs should not require SSL. (APIs hate following 301s).
if Rails.env.development? || Rails.env.test? || ["space_api","macs"].include?(params[:controller])
return false
else
return true
end
end
rescue_from CanCan::AccessDenied do |exception|
if !current_user.nil? && current_user.orientation.blank? then
flash[:alert] = "Sorry, you probably need to complete New Member Orientation before having access to this page. <br/>Please check your email and schedule a New Member Orientation with a volunteer."
@ -13,6 +24,20 @@ class ApplicationController < ActionController::Base
@payment_methods = [[nil],["PayPal"],["Dwolla"],["Bill Pay"],["Check"],["Cash"],["Other"]]
@payment_instructions = {nil => nil, :paypal => "Set up a monthly recurring payment to hslfinances@gmail.com", :dwolla => "Set up a monthly recurring payment to hslfinances@gmail.com", :billpay => "Have your bank send a monthly check to HeatSync Labs Treasurer, 140 W Main St, Mesa AZ 85201", :check => "Mail to HeatSync Labs Treasurer, 140 W Main St, Mesa AZ 85201 OR put in the drop safe at the Lab with a deposit slip firmly attached each month.", :cash => "Put in the drop safe at the Lab with a deposit slip firmly attached each month.", :other => "Hmm... talk to a Treasurer!"}
# Check authorization of a user / sign them in manually
def check_auth(email,password)
resource = User.find_by_email(email)
if resource && resource.valid_password?(password)
resource.remember_me = true
sign_in :user, resource
return true
else
return false
end
end
end
# Add a "fit" function to sanitize inputs for mac history

39
app/controllers/cards_controller.rb Normal file → Executable file
View File

@ -1,6 +1,6 @@
class CardsController < ApplicationController
load_and_authorize_resource
before_filter :authenticate_user!
load_and_authorize_resource except: :authorize
before_filter :authenticate_user!, except: :authorize
# GET /cards
# GET /cards.json
@ -111,6 +111,41 @@ class CardsController < ApplicationController
end
end
def authorize
# Stop unless signed in already, OR if the supplied user/pass params are good.
unless current_user || check_auth(params['user'],params['pass'])
@auth = "bad_user_or_pass"
else
# Stop unless the user can access the door system
unless can? :authorize, Card
@auth = "bad_user_permissions"
Rails.logger.warn "----------\r\nWARNING: CARD AUTH ATTEMPT DENIED. USER #{current_user.inspect}\r\n----------"
else
begin
@card = Card.find(:first, :conditions => ["lower(card_number) = ?", params[:id].downcase])
@auth = @card.inspect
if @card && @card.user
@auth = @card.user.has_certification?(params[:device])
else
@auth = false
end
rescue
@auth = false
end
end
end
if @card && @card.user
username = @card.user.name
else
username = nil
end
render json: [@auth, username]
end
# DELETE /cards/1
# DELETE /cards/1.json
def destroy

0
app/controllers/certifications_controller.rb Normal file → Executable file
View File

View File

@ -0,0 +1,87 @@
class ContractsController < ApplicationController
load_and_authorize_resource :contract
before_filter :authenticate_user!, :load_users
layout 'resources'
def index
if params[:user_id].present?
@contracts = Contract.where(user_id: params[:user_id])
end
respond_to do |format|
format.html
format.json { render :json => @contracts }
end
end
def show
end
def new
end
def edit
end
def create
# if @contract.first_name.blank? && @contract.last_name.blank? && @contract.cosigner.blank? # assume autodetect of filename
# begin
# name_split = params[:contract][:document].original_filename.sub(".jpg","").split
# if name_split.count == 4 # we have one name
# @contract.first_name = name_split[0]
# @contract.last_name = name_split[1]
# # 2 is the hyphen
# @contract.signed_at = Date.parse(name_split[3])
# elsif name_split.count == 7 && name_split[2] == "by" # we have two names
# @contract.first_name = name_split[0]
# @contract.last_name = name_split[1]
# # 2 is "by"
# @contract.cosigner = "#{name_split[3]} #{name_split[4]}"
# # 5 is the hyphen
# @contract.signed_at = Date.parse(name_split[6])
# else
# Rails.logger.info "Couldn't determine name from filename array: #{name_split.inspect}"
# end
# rescue Exception => e
# end
# end
@contract.created_by = current_user
respond_to do |format|
if @contract.save
format.html { redirect_to @contract, :notice => 'Contract was successfully created.' }
format.json { render :json => @contract, :status => :created, :location => @contract }
else
format.html { render :action => "new" }
format.json { render :json => @contract.errors, :status => :unprocessable_entity }
end
end
end
def find_for_user
end
def update
respond_to do |format|
if @contract.update_attributes(params[:contract])
format.html { redirect_to @contract, :notice => 'Contract was successfully updated.' }
format.json { head :no_content }
else
format.html { render :action => "edit" }
format.json { render :json => @contract.errors, :status => :unprocessable_entity }
end
end
end
def destroy
@contract.destroy
respond_to do |format|
format.html { redirect_to contracts_url }
format.json { head :no_content }
end
end
def load_users
@users = User.accessible_by(current_ability).sort_by(&:name)
end
end

30
app/controllers/door_logs_controller.rb Normal file → Executable file
View File

@ -5,7 +5,35 @@ class DoorLogsController < ApplicationController
# GET /door_logs
# GET /door_logs.json
def index
@door_logs = DoorLog.find(:all, :order => "created_at DESC", :limit => 500)
@door_logs = DoorLog.find(:all, :order => "created_at DESC", :limit => 100)
begin
@start_date = DateTime.parse(params[:start])
@end_date = DateTime.parse(params[:end])
rescue
@start_date = DateTime.now - 2.weeks
@end_date = DateTime.now
end
@door_logs_by_hour = DoorLog.where("created_at > ? AND created_at < ? AND (key = ? OR key = ?)", @start_date, @end_date,"door_1_locked","door_2_locked").order("created_at ASC").group_by(&:key)
@door_log_graph = [
@door_logs_by_hour["door_1_locked"].map{|d| [d.created_at.to_time.to_i*1000, 1^d.data.to_i]}, # use XOR to invert 1 into 0 and vice versa
@door_logs_by_hour["door_2_locked"].map{|d| [d.created_at.to_time.to_i*1000, 1^d.data.to_i]}
]
# @door_logs_by_hour.each do |door_log|
# # Add one computer for activate, subtract one for deactivate
# if door_log.data == 1
#
# elsif door_log.data == 0
# mac_running_balance -= 1
# end
# @door_log_graph << [time.to_time.to_i*1000,mac_running_balance]
# end
respond_to do |format|
format.html # index.html.erb

24
app/controllers/home_controller.rb Normal file → Executable file
View File

@ -1,6 +1,22 @@
class HomeController < ApplicationController
layout 'resources'
def index
# Alerts
if user_signed_in? && current_user.orientation.blank? then
flash[:alert] = "There's a lot more to see here, but our records show you haven't completed the new member orientation yet. If that's incorrect, please contact a volunteer."
end
if user_signed_in? && current_user.member_status.between?(2,100) then
flash[:alert] = "<!--
Member: <%= current_user.member.inspect
Level: <%= current_user.member_level.inspect
-->
Looks like we haven't acknowledged a recent payment for you yet. This could be because we're slow, but if in doubt please see your profile for payment instructions, consider updating your membership level to something accurate, or contact us.<br/>Thanks for supporting us!"
end
# Fun Stats
@featured_resource = Resource.where("picture_file_name IS NOT NULL").sample
def index
@num_certs = UserCertification.count
@recent_certs = UserCertification.where("created_at > ?", DateTime.now - 7.days).count
@num_users = User.count
@ -27,12 +43,12 @@ def index
respond_to do |format|
format.html # index.html.erb
end
end
end
def more_info
def more_info
respond_to do |format|
format.html # more_info.html.erb
end
end
end
end

0
app/controllers/ipns_controller.rb Normal file → Executable file
View File

4
app/controllers/mac_logs_controller.rb Normal file → Executable file
View File

@ -4,6 +4,10 @@ before_filter :authenticate_user!
def index
@mac_logs = MacLog.desc.limit(1000)
@macs = {}
@mac_logs.each do |log|
@macs.merge!({log.mac => Mac.find_by_mac(log.mac)})
end
end
end

12
app/controllers/macs_controller.rb Normal file → Executable file
View File

@ -224,7 +224,7 @@ end
def arp_lookup
@ip = request.env['REMOTE_ADDR']
@arp = %x(/usr/sbin/arp -a | grep #{@ip})
@arp = /([0-9A-F]{2}[:-]){5}([0-9A-F]{2})/i.match(%x(arp -a | grep #{@ip}))
end
def scan
@ -292,18 +292,22 @@ Rails.logger.info "starting scan..."
Rails.logger.info "Running [#{command}]"
end
IO.popen(command) { |stdin|
Rails.logger.info "Reading stdin: "+stdin.inspect
stdin.each { |line|
result = stdin.read()
result.lines.each { |line|
Rails.logger.info "Reading stdin: "+line.inspect
next if line !~ /^([\d\.]+)\s+([[:xdigit:]:]+)\s/;
macs[($2).downcase] = ($1).downcase;
}
Rails.logger.info "STDIN:"+result.lines.count.inspect
@macs = macs.dup # make a copy for output in the view
Rails.logger.info "MACS:"+@macs.inspect
}
# Scan the existing macs and update each record as necessary
Mac.find(:all).each { |entry|
mac = entry.mac.downcase
ip = entry.ip
if macs.has_key?(mac)
if macs.has_key?(mac) # if our scan shows this mac
if ! entry.active || ! entry.since
Rails.logger.info "Activating #{mac} at #{ip}" if options[:verbose]
entry.since = Time.now

3
app/controllers/payments_controller.rb Normal file → Executable file
View File

@ -45,8 +45,6 @@ class PaymentsController < ApplicationController
payment_months = @payments.sort_by(&:date).group_by{ |p| p.date.beginning_of_month }
@payments_by_month = []
payment_months.each do |month|
# Only grab the last year from today
if month.first > (Date.today - 1.year) && month.first < Date.today
# Calculate sum of amounts for each month and store at end of month array
@payments_by_month << [month.first.to_time.to_i*1000, month.last.sum{|p|
amount = amount_or_level(p)
@ -61,7 +59,6 @@ class PaymentsController < ApplicationController
end
}]
end
end
return @payments_by_month
end

0
app/controllers/paypal_csvs_controller.rb Normal file → Executable file
View File

0
app/controllers/registrations_controller.rb Normal file → Executable file
View File

View File

@ -0,0 +1,46 @@
class ResourceCategoriesController < ApplicationController
load_and_authorize_resource
layout 'resources'
def create
authorize! :create, @resource_category
respond_to do |format|
if @resource_category.save
format.html { redirect_to resource_categories_path, :notice => "Category was successfully created." }
format.json { head :no_content }
else
format.html { render :action => "new" }
format.json { render :json => @resource_category.errors, :status => :unprocessable_entity }
end
end
end
def update
authorize! :update, @resource_category
respond_to do |format|
if @resource_category.update_attributes(params[:resource_category])
format.html { redirect_to resource_categories_path, :notice => "Category was successfully updated." }
format.json { head :no_content }
else
format.html { render :action => "edit" }
format.json { render :json => @resource_category.errors, :status => :unprocessable_entity }
end
end
end
def destroy
respond_to do |format|
if @resource_category.destroy
format.html { redirect_to resource_categories_path, :notice => "Category was deleted." }
format.json { head :ok }
else
format.html { redirect_to resource_categories_path, :notice => "Category could not be deleted. #{@resource_category.errors.full_messages.first}." }
format.json { render :json => @resource_category.errors, :status => :unprocessable_entity }
end
end
end
end

View File

@ -0,0 +1,63 @@
class ResourcesController < ApplicationController
load_and_authorize_resource
before_filter :load_users
layout 'resources'
def index
@featured_resource = @resources.where("picture_file_name IS NOT NULL").sample
end
def new
# don't get too excited... for some reason this gets set to the current_user
@resource.user_id = nil
end
def create
@resource.modified_by = current_user.id # log who modified this last
authorize! :create, @resource
respond_to do |format|
if @resource.save
format.html { redirect_to resource_path(@resource), :notice => "Resource was successfully created." }
format.json { head :no_content }
else
format.html { render :action => "new" }
format.json { render :json => @resource.errors, :status => :unprocessable_entity }
end
end
end
def update
@resource.modified_by = current_user.id # log who modified this last
@resource.assign_attributes(params[:resource])
authorize! :update, @resource
respond_to do |format|
if @resource.update_attributes(params[:resource])
format.html { redirect_to resource_path(@resource), :notice => "Resource was successfully updated." }
format.json { head :no_content }
else
format.html { render :action => "edit" }
format.json { render :json => @resource.errors, :status => :unprocessable_entity }
end
end
end
def destroy
@resource.destroy
respond_to do |format|
format.html { redirect_to resources_path, :notice => "Resource was deleted." }
format.json { head :ok }
end
end
def load_users
if can? :assign_user, Resource then
@users = User.accessible_by(current_ability).sort_by(&:name)
else
@users = [current_user]
end
end
end

0
app/controllers/settings_controller.rb Normal file → Executable file
View File

29
app/controllers/space_api_controller.rb Normal file → Executable file
View File

@ -38,8 +38,19 @@ class SpaceApiController < ApplicationController
else
unless can? :access_doors_remotely, :door_access
@output = "Sorry, your account isn't able to control doors remotely."
else
@output = "Ready to control doors. Send POST params to this URL as per the HTML form."
end
end
# Render the form again (or result)
respond_to do |format|
format.html
format.json {
response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
render :json => @output
}
end
end
def access_post
@ -78,19 +89,17 @@ class SpaceApiController < ApplicationController
end
end
# Render the form again
# Render the form again (or result)
respond_to do |format|
format.html {
render :access
}
format.json {
response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
render :json => @output
}
end
def check_auth(email,password)
resource = User.find_by_email(email)
if resource && resource.valid_password?(password)
resource.remember_me = true
sign_in :user, resource
return true
else
return false
end
end
end

0
app/controllers/statistics_controller.rb Normal file → Executable file
View File

0
app/controllers/user_certifications_controller.rb Normal file → Executable file
View File

25
app/controllers/users_controller.rb Normal file → Executable file
View File

@ -1,6 +1,7 @@
class UsersController < ApplicationController
load_and_authorize_resource
before_filter :authenticate_user!
layout 'resources'
def sort_by_cert(certs,id)
result = 0
@ -15,6 +16,18 @@ class UsersController < ApplicationController
# GET /users
# GET /users.json
def index
unless params[:full] # by default, show summary
@users = @users.paying + @users.volunteer
#.joins(:payments).where("payments.date > ? OR ", (DateTime.now - 60.days)).uniq
respond_to do |format|
format.html { render 'summary', layout: 'resources' }
format.json { render :json => @users }
end
else # show full
case params[:sort]
when "name"
@users = @users.sort_by(&:name)
@ -23,7 +36,7 @@ class UsersController < ApplicationController
when "orientation"
@users = @users.sort_by{ |u| [-u.orientation.to_i,u.name] }
when "waiver"
@users = @users.sort_by{ |u| [-u.waiver.to_i,u.name] }
@users = @users.sort_by{ |u| [-u.contract_date.to_i,u.name] }
when "member"
@users = @users.sort_by{ |u| [-u.member_status.to_i,u.name] }
when "card"
@ -42,6 +55,7 @@ class UsersController < ApplicationController
format.json { render :json => @users }
end
end
end
# 'Active' users who haven't paid recently
def inactive
@ -73,12 +87,10 @@ class UsersController < ApplicationController
def compose_email
@user = User.find(params[:user_id])
authorize! :read, @user
end
def send_email
@user = User.find(params[:user_id])
authorize! :read, @user
@subject = params[:subject]
@body = params[:body]
if @user.send_email(current_user,@subject,@body)
@ -113,6 +125,9 @@ class UsersController < ApplicationController
# POST /users
# POST /users.json
def create
# update oriented_by only if orientation has been set
@user.oriented_by_id = current_user.id unless @user.orientation.blank?
respond_to do |format|
if @user.save
format.html { redirect_to @user, :notice => 'User was successfully created.' }
@ -127,6 +142,10 @@ class UsersController < ApplicationController
# PUT /users/1
# PUT /users/1.json
def update
# update oriented_by only if it's blank but the (new) orientation isn't blank
# gotta test the params because they don't get applied til below.
@user.oriented_by_id = current_user.id if @user.oriented_by.blank? && (!params[:user]["orientation(1i)"].blank?)
respond_to do |format|
if @user.update_attributes(params[:user])
format.html { redirect_to @user, :notice => 'User was successfully updated.' }

27
app/helpers/application_helper.rb Normal file → Executable file
View File

@ -1,4 +1,31 @@
module ApplicationHelper
@payment_methods = [[nil],["PayPal"],["Dwolla"],["Bill Pay"],["Check"],["Cash"],["Other"]]
@payment_instructions = {nil => nil, :paypal => "Set up a monthly recurring payment to hslfinances@gmail.com", :dwolla => "Set up a monthly recurring payment to hslfinances@gmail.com", :billpay => "Have your bank send a monthly check to HeatSync Labs Treasurer, 140 W Main St, Mesa AZ 85201", :check => "Mail to HeatSync Labs Treasurer, 140 W Main St, Mesa AZ 85201 OR put in the drop safe at the Lab with a deposit slip firmly attached each month.", :cash => "Put in the drop safe at the Lab with a deposit slip firmly attached each month.", :other => "Hmm... talk to a Treasurer!"}
def sort_link(title, column, options = {})
condition = options[:unless] if options.has_key?(:unless)
sort_dir = params[:dir] == 'up' ? 'down' : 'up'
link_to_unless condition, title, request.parameters.merge( {:sort => column, :dir => sort_dir} )
end
def li_link_to(name = nil, options = nil, html_options = nil, &block)
html_options, options, name = options, name, block if block_given?
options ||= {}
html_options = convert_options_to_data_attributes(options, html_options)
url = url_for(options)
html_options['href'] ||= url
if current_page?(url)
content_tag(:li, class: "active") do
content_tag(:a, name || url, html_options, &block)
end
else
content_tag(:li) do
content_tag(:a, name || url, html_options, &block)
end
end
end
end

0
app/helpers/certifications_helper.rb Normal file → Executable file
View File

0
app/helpers/door_logs_helper.rb Normal file → Executable file
View File

0
app/helpers/home_helper.rb Normal file → Executable file
View File

0
app/helpers/ipn_helper.rb Normal file → Executable file
View File

0
app/helpers/mac_logs_helper.rb Normal file → Executable file
View File

0
app/helpers/pamela_helper.rb Normal file → Executable file
View File

0
app/helpers/payments_helper.rb Normal file → Executable file
View File

0
app/helpers/user_certifications_helper.rb Normal file → Executable file
View File

0
app/helpers/users_helper.rb Normal file → Executable file
View File

0
app/mailers/.gitkeep Normal file → Executable file
View File

0
app/mailers/user_mailer.rb Normal file → Executable file
View File

0
app/models/.gitkeep Normal file → Executable file
View File

14
app/models/ability.rb Normal file → Executable file
View File

@ -4,6 +4,8 @@ class Ability
def initialize(user)
can :read, Mac # Anonymous can read mac
can :scan, Mac # Need anonymous so CRON can scan
can :read, Resource
can :read, ResourceCategory
if !user.nil?
@ -13,12 +15,16 @@ class Ability
can :read_details, Mac
can [:update], Mac, :user_id => nil
can [:create,:update], Mac, :user_id => user.id
can [:create,:update,:destroy], Resource, :user_id => user.id
can :read, Payment, :user_id => user.id
can [:read,:new_member_report], User, :id => user.id #TODO: why can users update themselves? Maybe because Devise doesn't check users/edit?
can :read, UserCertification, :user_id => user.id
can :read, User, :id => user.id #TODO: why can users update themselves? Maybe because Devise doesn't check users/edit?
can :compose_email, User
can :send_email, User
if user.card_access_enabled
can :access_doors_remotely, :door_access
can :authorize, Card # used for interlock card/certification auth
end
# Instructors can manage certs and see users
@ -30,8 +36,10 @@ class Ability
end
# Users can see others' stuff if they've been oriented
unless user.orientation.blank?
can :read, User, :hidden => [nil,false]
can [:read,:new_member_report,:activity], User, :hidden => [nil,false]
can :read, UserCertification
can [:create,:update], Resource, :user_id => [nil,user.id]
can [:create,:update,:destroy], ResourceCategory
end
# Accountants can manage payments
@ -46,7 +54,7 @@ class Ability
can :manage, :all
end
# Prevent all destruction for now
# Prevent most destruction for now
#cannot :destroy, User
#cannot :destroy, Card
cannot :destroy, Certification

30
app/models/card.rb Normal file → Executable file
View File

@ -11,21 +11,14 @@ class Card < ActiveRecord::Base
door_access_url = APP_CONFIG['door_access_url']
door_access_password = APP_CONFIG['door_access_password']
# connect to door access system
source = open("#{door_access_url}?e=#{door_access_password}").read
results = source.scan(/ok/)
if(results.size > 0) then
#only continue if we've got an OK login
cardid = self.id.to_s.rjust(3, '0') #TODO: provide ability for
cardid = self.id.to_s.rjust(3, '0')
cardperm = self.card_permissions.to_s.rjust(3, '0')
cardnum = self.card_number.rjust(8, '0')
source = open("#{door_access_url}?m#{cardid}&p#{cardperm}&t#{cardnum}").read
# login and send the command all in one go (auto-logout is a feature of the arduino when used this way)
source = open("#{door_access_url}?m#{cardid}&p#{cardperm}&t#{cardnum}&e=#{door_access_password}").read
results = source.scan(/cur/)
#logout
open("#{door_access_url}?e=0000")
if(results.size > 0) then
#only return true if we got some kind of decent response
return true
@ -33,10 +26,6 @@ class Card < ActiveRecord::Base
# We didn't get a decent response.
return false
end
else
# We didn't get an OK login.
return false
end
end
def self.upload_all_to_door
@ -47,16 +36,13 @@ class Card < ActiveRecord::Base
door_access_url = APP_CONFIG['door_access_url']
door_access_password = APP_CONFIG['door_access_password']
source = open("#{door_access_url}?e=#{door_access_password}").read
results = source.scan(/ok/)
if(results.size > 0) then
@cards.each do |u|
#only continue if we've got an OK login
cardid = u.id.to_s.rjust(3, '0')
cardperm = u.card_permissions.to_s.rjust(3, '0')
cardnum = u.card_number.rjust(8, '0')
source = open("#{door_access_url}?m#{cardid}&p#{cardperm}&t#{cardnum}").read
# login and send the command all in one go (auto-logout is a feature of the arduino when used this way)
source = open("#{door_access_url}?m#{cardid}&p#{cardperm}&t#{cardnum}&e=#{door_access_password}").read
results = source.scan(/cur/)
if(results.size > 0) then
@ -67,12 +53,6 @@ class Card < ActiveRecord::Base
end
end
#logout
open("#{door_access_url}?e=0000")
else
@end_results.push([cardid,"FAIL"])
end
return @end_results
end
end

4
app/models/certification.rb Normal file → Executable file
View File

@ -1,5 +1,7 @@
class Certification < ActiveRecord::Base
attr_accessible :description, :name
attr_accessible :description, :name, :slug
has_many :user_certifications
has_many :users, :through => :user_certifications
validates_presence_of :name, :slug
end

24
app/models/contract.rb Normal file
View File

@ -0,0 +1,24 @@
class Contract < ActiveRecord::Base
belongs_to :user
belongs_to :created_by, :foreign_key => "created_by_id", :class_name => "User"
attr_accessible :user_id, :first_name, :last_name, :cosigner,
:signed_at, :document, :document_file_name, :document_content_type,
:document_file_size, :document_updated_at
# :created_by not accessible for security purposes
validates_presence_of :first_name, :signed_at #, :last_name
has_attached_file :document,
{ :styles =>
{
:medium => "300x300>",
:large => "900x900>"
},
:storage => :s3,
:s3_protocol => :https,
:s3_credentials => { :access_key_id => ENV['S3_KEY'],
:secret_access_key => ENV['S3_SECRET'] },
:path => ":attachment/:id/:style.:extension",
:bucket => ENV['S3_BUCKET']
}
end

0
app/models/door_log.rb Normal file → Executable file
View File

0
app/models/ipn.rb Normal file → Executable file
View File

0
app/models/mac.rb Normal file → Executable file
View File

0
app/models/mac_log.rb Normal file → Executable file
View File

0
app/models/payment.rb Normal file → Executable file
View File

0
app/models/paypal_csv.rb Normal file → Executable file
View File

31
app/models/resource.rb Executable file
View File

@ -0,0 +1,31 @@
class Resource < ActiveRecord::Base
attr_accessible :supercategory, :user_id, :resource_category_id, :name, :serial, :specs, :status, :donatable,
:picture, :picture_file_name, :picture_content_type, :picture_file_size, :picture_updated_at,
:picture2, :picture2_file_name, :picture2_content_type, :picture2_file_size, :picture2_updated_at,
:picture3, :picture3_file_name, :picture3_content_type, :picture3_file_size, :picture3_updated_at,
:picture4, :picture4_file_name, :picture4_content_type, :picture4_file_size, :picture4_updated_at,
:notes, :estimated_value, :disposed_at, :modified_by
belongs_to :owner, :class_name => "ToolshareUser" #TODO: remove owner
belongs_to :user
belongs_to :resource_category
PICTURE_OPTIONS = { :styles => { :medium => "300x300>",
:thumb => "100x100>",
:tiny => "50x50>"},
:storage => :s3,
:s3_protocol => :https,
:s3_credentials => { :access_key_id => ENV['S3_KEY'],
:secret_access_key => ENV['S3_SECRET'] },
:path => ":attachment/:id/:style.:extension",
:bucket => ENV['S3_BUCKET'] }
has_attached_file :picture, PICTURE_OPTIONS
has_attached_file :picture2, PICTURE_OPTIONS
has_attached_file :picture3, PICTURE_OPTIONS
has_attached_file :picture4, PICTURE_OPTIONS
def resource_category_name
resource_category.name if resource_category
end
end

13
app/models/resource_category.rb Executable file
View File

@ -0,0 +1,13 @@
class ResourceCategory < ActiveRecord::Base
has_many :resources
attr_accessible :name
before_destroy :has_resources?
private
def has_resources?
errors.add(:base, "Cannot delete category with associated resources") unless resources.count == 0
errors.blank? #return false, to not destroy the element, otherwise, it will delete.
end
end

0
app/models/setting.rb Normal file → Executable file
View File

4
app/models/toolshare_user.rb Executable file
View File

@ -0,0 +1,4 @@
class ToolshareUser < ActiveRecord::Base
has_many :resources, :foreign_key => "owner_id"
attr_accessible :name, :email
end

26
app/models/user.rb Normal file → Executable file
View File

@ -1,6 +1,6 @@
class User < ActiveRecord::Base
include Gravtastic
gravtastic :size => 120, :default => ""
gravtastic :size => 150, :default => ""
# Include default devise modules. Others available are:
# :token_authenticatable, :confirmable,
@ -9,16 +9,24 @@ class User < ActiveRecord::Base
:recoverable, :rememberable, :trackable, :validatable
# Setup accessible (or protected) attributes for your model
attr_accessible :email, :password, :password_confirmation, :remember_me, :name, :admin, :instructor, :member, :emergency_name, :emergency_phone, :current_skills, :desired_skills, :waiver, :emergency_email, :phone, :payment_method, :orientation, :member_level, :certifications, :hidden, :marketing_source, :payee, :accountant, :exit_reason, :twitter_url, :facebook_url, :github_url, :website_url, :email_visible, :phone_visible #TODO: make admin/instructor/member/etc not accessible
attr_accessible :email, :password, :password_confirmation, :remember_me, :name, :admin, :instructor, :member, :emergency_name, :emergency_phone, :current_skills, :desired_skills, :waiver, :emergency_email, :phone, :payment_method, :orientation, :member_level, :certifications, :hidden, :marketing_source, :payee, :accountant, :exit_reason, :twitter_url, :facebook_url, :github_url, :website_url, :email_visible, :phone_visible, :postal_code #TODO: make admin/instructor/member/etc not accessible
belongs_to :oriented_by, :foreign_key => "oriented_by_id", :class_name => "User"
has_many :cards
has_many :user_certifications
has_many :certifications, :through => :user_certifications
has_many :contracts
has_many :payments
has_many :macs
has_many :resources
scope :volunteer, -> { where('member_level >= 10 AND member_level < 25') }
scope :paying, -> { joins(:payments).where("payments.date > ?", (DateTime.now - 90.days)).uniq }
validates_format_of [:twitter_url, :facebook_url, :github_url, :website_url], :with => URI::regexp(%w(http https)), :allow_blank => true
# disable # validates_presence_of :postal_code
after_create :send_new_user_email
def absorb_user(user_to_absorb)
@ -130,7 +138,19 @@ class User < ActiveRecord::Base
Rails.logger.info UserMailer.email(self,from_user,subject,body).deliver
end
private
def has_certification?(cert_slug)
if self.certifications.find_by_slug(cert_slug)
true
else
false
end
end
def contract_date
self.contracts.first.signed_at unless self.contracts.blank?
end
private
def send_new_user_email
Rails.logger.info UserMailer.new_user_email(self).deliver

0
app/models/user_certification.rb Normal file → Executable file
View File

0
app/views/cards/_form.html.erb Normal file → Executable file
View File

0
app/views/cards/edit.html.erb Normal file → Executable file
View File

6
app/views/cards/index.html.erb Normal file → Executable file
View File

@ -6,11 +6,11 @@
<%= link_to 'Space API', space_api_path, :class => "btn" %>
<%= link_to 'Remote Door Access', space_api_access_path, :class => "btn" if can? :access_doors_remotely, :door_access %>
<p>
<b>Most Active Card Last Month:</b> <%= @most_active_card.name unless @most_active_card.blank? %> (<%= @most_active_card.accesses_this_week unless @most_active_card.blank? %> days)
<b>Most Active Card Last Month:</b> <%= @most_active_card.user.name unless @most_active_card.user.blank? %> (<%= @most_active_card.accesses_this_week unless @most_active_card.blank? %> days)
</p>
<p>
<% unless @runner_up_card.blank? %>
<b>Runner Up:</b> <%= @runner_up_card.name %> (<%= @runner_up_card.accesses_this_week %> days)
<% unless @runner_up_card.blank? || @runner_up_card.user.blank? %>
<b>Runner Up:</b> <%= @runner_up_card.user.name %> (<%= @runner_up_card.accesses_this_week %> days)
<% end %>
</p>

0
app/views/cards/new.html.erb Normal file → Executable file
View File

0
app/views/cards/show.html.erb Normal file → Executable file
View File

0
app/views/cards/upload.html.erb Normal file → Executable file
View File

0
app/views/cards/upload_all.html.erb Normal file → Executable file
View File

4
app/views/certifications/_form.html.erb Normal file → Executable file
View File

@ -15,6 +15,10 @@
<%= f.label :name %><br />
<%= f.text_field :name %>
</div>
<div class="field">
<%= f.label :slug, "Slug (lowercase, single-word identifier)" %><br />
<%= f.text_field :slug %>
</div>
<div class="field">
<%= f.label :description %><br />
<%= f.text_area :description %>

0
app/views/certifications/edit.html.erb Normal file → Executable file
View File

1
app/views/certifications/index.html.erb Normal file → Executable file
View File

@ -5,6 +5,7 @@
<ul>
<% @certifications.each do |certification| %>
<li><%= link_to certification.name, certification %>
(<%= certification.slug %>)
<% if can? :update, certification %> | <%= link_to 'Edit', edit_certification_path(certification) %><% end %>
<% if can? :destroy, certification %> | <%= link_to 'Destroy', certification, :confirm => 'Are you sure?', :method => :delete %><% end %>
</li>

0
app/views/certifications/new.html.erb Normal file → Executable file
View File

5
app/views/certifications/show.html.erb Normal file → Executable file
View File

@ -3,6 +3,11 @@
<%= @certification.name %>
</p>
<p>
<b>Slug (lowercase, single-word identifier):</b>
<%= @certification.slug %>
</p>
<p>
<b>Description:</b>
<%= simple_format @certification.description %>

View File

@ -0,0 +1,51 @@
<%= form_for(@contract, html: {class: "col-sm-6"}) do |f| %>
<% if @contract.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@contract.errors.count, "error") %> prohibited this contract from being saved:</h2>
<ul>
<% @contract.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="form-group">
<%= f.label :first_name %><br />
<%= f.text_field :first_name, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :last_name %><br />
<%= f.text_field :last_name, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :cosigner %><br />
<%= f.text_field :cosigner, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :user_id, "User" %><br />
<%= collection_select(:contract, :user_id, @users, :id, :name, :include_blank => true) %>
</div>
<div class="form-group">
<%= f.label :signed_at %><br />
<%= f.date_select :signed_at %>
</div>
<div class="form-group">
<%= f.label :document %><br />
<%= link_to "View Existing Document", @contract.document.url, class: "btn btn-default" unless @contract.document.blank? %>
<p>
<%= link_to "Upload New Document", "#", class: "btn btn-default", onclick: "$('#document_upload').removeClass('hidden'); $(this).addClass('hidden')" unless @contract.document.blank? %>
<div id="document_upload" class="<%= "hidden" unless @contract.document.blank? %>">
<%= f.file_field :document, class: "form-control", style: "width: 100%; height: 100px; background-color: #f9f9f9;" %>
</div>
</p>
</div>
<div class="form-group">
<%= f.submit nil, class: "btn btn-primary" %>
</div>
<% end %>

View File

@ -0,0 +1,4 @@
<h1>Edit Contract
<%= link_to 'Back', contracts_path, class: "btn btn-default" %>
</h1>
<%= render 'form' %>

View File

@ -0,0 +1,42 @@
<div class="row">
<h1 class="col-md-8">Contracts
<%= link_to 'Add Contract', new_contract_path, :class => "btn btn-success" if can? :create, Contract %>
</h1>
</div>
<table class="table">
<tr>
<th>Scan?</th>
<th>Name</th>
<th>User</th>
<th>Date</th>
<th></th>
</tr>
<% @contracts.sort_by{|r| (r.last_name) || "" }.each do |contract| %>
<tr>
<td>
<% unless contract.document.blank? %>
<span class="glyphicon glyphicon-ok"></span>
<% end %>
</td>
<td>
<%= contract.last_name %>,
<%= contract.first_name %>
<%= "and #{contract.cosigner}" unless contract.cosigner.blank? %>
</td>
<td>
<%= link_to contract.user.name, contract.user if contract.user %>
</td>
<td>
<%= contract.signed_at.to_date.to_s(:long) %>
</td>
<td>
<%= link_to "View", contract, class: "btn btn-primary" %>
<%= link_to "Edit", edit_contract_path(contract), class: "btn btn-default" %>
</td>
</tr>
<% end %>
</table>
<br />

View File

@ -0,0 +1,80 @@
<h1>New Contract
<%= link_to 'Back', contracts_path, class: "btn btn-default" %>
</h1>
<div class="col-xs-12">
<%= render 'form' %>
</div>
<div class="col-xs-12">
<div class="alert alert-info">Note: if you name your scans in one of the below formats, this form will try to fill itself out automatically when you select the file. (The spaces, hyphen, and jpg or pdf extensions are mandatory for this to work.) <em>Double-check the names, users, and dates, though!!</em><br/>
<strong>first last - date.[jpg|pdf]</strong><br/>
<strong>first last by cosigner name - date.[jpg|pdf]</strong><br/>
</div>
</div>
<script type="text/javascript">
function capitalizeIfIsntCapitalized(str){
if(str[0].toLowerCase() == str[0] ){
return str[0].toUpperCase() + str.slice(1);
}
else{
return str;
}
}
$(function() {
$("#contract_document").change(function (){
doc = document.getElementById("contract_document")
if(doc.files && doc.files.length > 0) {
name_split = doc.files[0].name.replace(".jpg","").replace(".pdf","").split(" ");
$("#contract_user_alert").remove(); // clear any existing alerts
$("#contract_user_spinner").remove(); // clear spinner
if(name_split.length == 4){ // we have one name
$("#contract_first_name").val(capitalizeIfIsntCapitalized(name_split[0]));
$("#contract_last_name").val(capitalizeIfIsntCapitalized(name_split[1]));
// 2 is the hyphen
signed_at = new Date(name_split[3]);
$("#contract_signed_at_1i").val(signed_at.getUTCFullYear()).change();
$("#contract_signed_at_2i").val(signed_at.getUTCMonth()+1).change();
$("#contract_signed_at_3i").val(signed_at.getUTCDate()).change();
}
else if(name_split.length == 7 && name_split[2] == "by"){ // we have two names
$("#contract_first_name").val(capitalizeIfIsntCapitalized(name_split[0]));
$("#contract_last_name").val(capitalizeIfIsntCapitalized(name_split[1]));
// 2 is "by"
$("#contract_cosigner").val(capitalizeIfIsntCapitalized(name_split[3])+" "+capitalizeIfIsntCapitalized(name_split[4]));
// 5 is the hyphen
signed_at = new Date(name_split[6]);
$("#contract_signed_at_1i").val(signed_at.getUTCFullYear());
$("#contract_signed_at_2i").val(signed_at.getUTCMonth()+1);
$("#contract_signed_at_3i").val(signed_at.getUTCDate());
}
// Try and select the relevant user if exists
if( $("#contract_first_name").val().length > 0 && $("#contract_last_name").val().length > 0 ) {
user_id = $('#contract_user_id option').filter(function(){
return $(this).text() == $("#contract_first_name").val() + " " + $("#contract_last_name").val();
}).prop("selected", "true").val();
// Start a spinner before AJAX request
$("#contract_user_id").after("<i id='contract_user_spinner' class='icon-spin icon-refresh'></i>");
// If we found a user, check how many contracts that user already has
$.get("/users/"+user_id+"/contracts.json",function(data){
$("#contract_user_spinner").remove(); // clear spinner
if(data.length > 0){
other_contract_links = $.map(data,function(item){
output = "<a href='/contracts/"+item["id"]+"'>"+item["id"]+"</a> &nbsp;&nbsp; ";
return output;
});
$("#contract_user_id").after("<span id='contract_user_alert' class='label label-danger'>Warning: this user already has "+data.length+" other contract(s). Check the existing one(s) to avoid duplication: "+other_contract_links+"</span>");
}
});
}
}
});
});
</script>

View File

@ -0,0 +1,41 @@
<div class="row">
<h1 class="col-md-8">
Contract
<%= link_to 'Back', contracts_path, :class => "btn btn-default" %>
<%= link_to 'New', new_contract_path, :class => "btn btn-success" %>
<%= link_to 'Edit', edit_contract_path(@contract), :class => "btn btn-primary" %>
<%= link_to 'Delete', contract_path(@contract), {:confirm => 'Are you sure you want to delete this forever?', :method => :delete, :class => "btn btn-danger"} if can? :destroy, @contract %>
</h1>
</div>
<h2>
<%= @contract.first_name %>
<%= @contract.last_name %>
<%= "and #{@contract.cosigner}" unless @contract.cosigner.blank? %>
<%= link_to "(#{@contract.user.name})", @contract.user if @contract.user %>
<small>
signed
<%= @contract.signed_at.to_date.to_s(:long) %>
</small>
</h2>
<% unless @contract.created_by.blank? %>
<p>
<em>Created by <%= @contract.created_by.name %></em>
</p>
<% end %>
<% if @contract.document.blank? %>
<p>No document uploaded</p>
<% else %>
<p><%= link_to "Download Contract", @contract.document.url %>
<div class="col-xs-12">
<% contract_url = (@contract.document.exists?(:large) ? @contract.document.url(:large) : @contract.document.url) %>
<iframe src="<%= contract_url %>" width="100%" height="600"></iframe>
</div>
</p>
<% end %>
<br />

0
app/views/devise/confirmations/new.html.erb Normal file → Executable file
View File

View File

View File

0
app/views/devise/mailer/unlock_instructions.html.erb Normal file → Executable file
View File

0
app/views/devise/passwords/edit.html.erb Normal file → Executable file
View File

Some files were not shown because too many files have changed in this diff Show More