104 Commits
Author SHA1 Message Date
Jason Denney bc6802e5fb Adding famfamfam icons for docker w/ symlink 2016-02-26 18:00:48 -08:00
Jason Denney e48924a228 Fixed bug where rando 401 causes signin redirect 2016-02-26 17:55:24 -08:00
Jason Denney 1887840507 Fix port in dev guide 2016-02-26 16:19:29 -08:00
Jason Denney 0629972dca Update docs again 2016-02-26 16:11:22 -08:00
Jason Denney 6daff093eb Remove comment 2016-02-26 16:10:59 -08:00
Jason Denney 43faa79f60 Update the readme & docs 2016-02-26 16:10:47 -08:00
Jason Denney 9e282eda7c Need symlink for Docker to link extjs 2016-02-25 13:54:10 -08:00
Jason Denney 02fba62499 Fix bug in seed_bike factory 2016-02-25 13:36:18 -08:00
Jason Denney 4fe8865aca Dockerize! Startup with docker-compose up
Make sure to docker-compose build first
2016-02-25 13:36:01 -08:00
Jason Denney fd5d4798b0 Merge pull request #103 from spacemunkay/denney-fix-user-add-bike
Fix User being able to add bikes
2014-06-12 22:26:30 -04:00
Jason Denney 3e480981c7 Fix User being able to add bikes 2014-06-12 22:24:00 -04:00
Jason Denney 523135ed9f Merge pull request #102 from spacemunkay/denney-fix-edit-redirect
root path to user edit
2014-06-12 21:25:29 -04:00
Jason Denney 5e95ce542c root path to user edit 2014-06-12 21:24:02 -04:00
Jason Denney a8e0c87d37 Merge pull request #101 from spacemunkay/mobile-ui
Mobile UI
2014-05-04 20:55:41 -04:00
Jason Denney 1b2c0fa378 Submit dates in locale format for time being
Added jquery date format plugin
2014-05-04 20:43:46 -04:00
Jason Denney 785e096aea Giving up on checkin tests for now 2014-05-04 11:26:37 -04:00
Jason Denney 5c4007a1be Show add another bike link after creating bike 2014-05-04 11:14:50 -04:00
Jason Denney b9b6ed5ca0 Remove stray puts in spec 2014-05-04 10:56:29 -04:00
Jason Denney 0605d0f0d9 Fixed creating bike work time entry & added spec 2014-05-04 10:56:14 -04:00
Jason Denney 071815b9fd Fix app layout for mobile devices 2014-05-04 10:34:55 -04:00
Jason Denney ca3026e7c5 Added delete time entry feature spec 2014-05-03 22:53:27 -04:00
Jason Denney 5e0d678ea1 Finish time entry delete JS/html 2014-05-03 22:08:23 -04:00
Jason Denney d94c436ba5 Added api/time_entry/delete method 2014-05-03 22:06:56 -04:00
Jason Denney 24facd4dee Time entry actions had the wrong IDs... 2014-05-03 21:48:35 -04:00
Jason Denney 894423e66f Fixed bug in user credits available
Personal work was counting toward credits, now it’s possible to add a
column to user actions to denote if a user action can count to credits.
2014-05-03 21:41:33 -04:00
Jason Denney 4ce4bd6597 WIP, rigged up JS/html to delete work entries 2014-05-03 19:29:57 -04:00
Jason Denney 2e21e327ca Home page link to time entries 2014-05-03 19:29:22 -04:00
Jason Denney 57d72c04a7 Fix TimeEntry scope 2014-05-03 19:28:56 -04:00
Jason Denney 4224f0de40 Added Time entries index page 2014-04-27 18:59:50 -04:00
Jason Denney 3d71add637 WIP, adding time entry index page 2014-04-27 15:46:42 -04:00
Jason Denney 7dd556d718 Fixed forwarding on time entry creation 2014-04-27 15:46:13 -04:00
Jason Denney 338905878e feature spec for adding new time entry 2014-04-27 15:07:21 -04:00
Jason Denney 7897795e8b Correct spec description 2014-04-27 14:39:32 -04:00
Jason Denney 00511c5b64 Finish Api/TimeEntries spec, fix bike_id handling 2014-04-27 14:39:11 -04:00
Jason Denney 1191014471 WIP, cleaned up api/time_entries, added TimeEntry
* Added scoped TimeEntry model
* Removed setting defaults in TimeEntry controller
* Still working on tests…
2014-04-26 20:06:47 -04:00
Jason Denney c0a9d8c1e5 Return time entry on successful create 2014-04-20 22:56:48 -04:00
Jason Denney 82f38fe35c Actually merge time_entry defaults 2014-04-20 22:56:26 -04:00
Jason Denney 2321197973 If bike selected, forward to bike checklist 2014-04-20 22:56:04 -04:00
Jason Denney 069a11f480 WIP, adding JS and api time entry create 2014-04-19 21:06:29 -04:00
Jason Denney b30203b750 Allow user select bike worked when adding time 2014-04-19 16:48:10 -04:00
Jason Denney 3e56f2eb19 Updating edit task list page w/ home & bike link 2014-04-19 16:25:15 -04:00
Jason Denney 2d65ebb45f JS Formatting 2014-04-19 16:22:54 -04:00
Jason Denney 80b2b7ff60 Added New Time Entry Front End 2014-04-19 16:20:14 -04:00
Jason Denney 98b136e4f4 Add link to view bike check list when showing bike 2014-03-16 13:50:13 -04:00
Jason Denney 9527725e07 Adding makeshift home button, Added view your bike link 2014-03-16 13:34:56 -04:00
Jason Denney 8f637348cf Fix check in/ check out 2014-03-12 23:35:09 -04:00
Jason Denney 062ed9c2d1 Added tests for user checkin/checkout on index 2014-03-12 21:52:37 -04:00
Jason Denney 124ce5901d Update spec tests for registration page 2014-03-12 21:29:01 -04:00
Jason Denney 1f9322ff3d Fixed weird spec sign_in issues, and render json 2014-03-09 22:49:30 -04:00
Jason Denney b1ddcfb867 Redirect correctly after creating a bike 2014-03-09 22:48:59 -04:00
Jason Denney 64e907b6f7 Should authenticate by user username, not email 2014-03-09 22:48:35 -04:00
Jason Denney e2f442889e Added tasks controller, spec, JS, apitesthelper
Had an issue with calling render_views in the specs to render the
jbuilder json templates and also getting the devise sign_in method to
work, ended up adding a helper to add the username/password for a user
2014-03-09 22:30:41 -04:00
Jason Denney 3b904f95f2 Adding api task_lists#show and spec, edit.haml updates 2014-03-09 19:20:00 -04:00
Jason Denney 656f0f12e4 WIP, creating TaskList Controller and edit action & view 2014-03-08 15:34:58 -05:00
Jason Denney 30d818ac87 Draft of the task list api controller 2014-03-08 15:04:27 -05:00
Jason Denney dc0a567a27 Added bike show api method 2014-03-08 15:03:52 -05:00
Jason Denney 523046b042 Add create jbuilder template 2014-03-01 14:15:49 -05:00
Jason Denney 9231c91e6c Update README.md 2014-03-01 11:12:10 -05:00
Jason Denney 14869cf8b6 Modifed bikes create to use jbuilder & follow jsonapi.org 2014-02-26 00:06:25 -05:00
Jason Denney b88bf545d2 Added jbuilder to gemfile 2014-02-26 00:05:02 -05:00
Jason Denney 20964eb268 Update feature for new bike view to pass 2014-02-10 00:09:11 -05:00
Jason Denney b90e101d69 Whoops! copypasta in seed yml 2014-02-09 22:01:57 -05:00
Jason Denney 7b11da9583 Forward url on add bike/ don't display value if nil 2014-02-09 22:01:42 -05:00
Jason Denney 64d4186b57 Bike show view styling
TBD add class for color block
2014-02-09 19:18:16 -05:00
Jason Denney c6a3ed9991 Added bike show and show view 2014-02-09 19:11:08 -05:00
Jason Denney d0c05a0244 Fix validations for creating a bike, view respond to errors 2014-02-09 18:23:35 -05:00
Jason Denney 55974feb4c Parse float for seat tube height 2014-02-09 18:22:33 -05:00
Jason Denney e867366525 Made feature specs for creating a bike 2014-02-08 16:56:34 -05:00
Jason Denney fb02646092 Moved registrations specs to features 2014-02-08 16:56:21 -05:00
Jason Denney 75832a3910 Add poltergeist to gem file, and limited version dependecies 2014-02-08 16:54:50 -05:00
Jason Denney 2d09aba26a Added submit js to bikes/new view
* Added displayFormErrors utility
* Added 4th option to bike styles “OTHER”
2014-02-08 14:21:20 -05:00
Jason Denney e694dfbddd Don't clobber development database, durrrr 2014-02-08 14:17:09 -05:00
Jason Denney e19b80faec Update README.md 2014-02-08 11:46:48 -05:00
Jason Denney 38a716b132 Added User password_reset spec and some refactor
*Make returned error in errors array
*Use constants
2014-02-04 23:47:30 -05:00
Jason Denney 8110baf255 Removing unnecessary render 2014-02-03 00:16:13 -05:00
Jason Denney 9fa0017d36 Fixed error response codes, finished bikes#create spec 2014-02-03 00:14:37 -05:00
Jason Denney e0041662d3 WIP, adding api/bikes_controller#create 2014-02-02 17:11:13 -05:00
Jason Denney c575fface9 Commenting out description on bike form for now 2014-02-02 17:10:32 -05:00
Jason Denney 83a4e4e9c1 Make bike color not required 2014-02-02 17:10:17 -05:00
Jason Denney f17eba810e WTF? Is this needed? Commenting this out for now
users#checkout  copy pasta perhaps?
2014-02-02 17:09:49 -05:00
Jason Denney 4b5133fd02 Fix unauthorized return code 2014-02-02 17:09:13 -05:00
Jason Denney 9edb90203c Remove whitespace 2014-02-02 09:45:18 -05:00
Jason Denney 8e112ca936 Adding logs_controller_spec, finally test coverage!!!!! 2014-02-02 09:45:01 -05:00
Jason Denney 8c5ff986a2 Update spec, remove turnip, to get tests working 2014-02-02 09:44:18 -05:00
Jason Denney f10b2adad6 Convert .rvmrc to ruby version and gemset 2014-02-02 09:43:22 -05:00
Jason Denney bd6872413e Make year dynamically updated 2014-01-21 23:35:11 -05:00
Jason Denney a8bdb52536 Improved Add Bike view
Worked out some bootstrap overrides issues
2014-01-19 19:32:18 -05:00
Jason Denney 0f890e12e9 Added bikes views
*Added new bike template
2014-01-18 18:25:21 -05:00
Jason Denney 61472e0fb6 Hide admin view button on small devices 2014-01-18 15:20:34 -05:00
Jason Denney ecb5083f1c Created new dashboard index
*Added js for logging out button
*WIP
2014-01-18 15:16:23 -05:00
Jason Denney a84b0e0fe7 Moving Netzke Admin view to Panel controller
*Moved custom_netzke_helpers code to panel.js
*Added panel controller and index and netzke layout
2014-01-18 15:14:19 -05:00
Jason Denney 2b863b8d8a Updated Forget Password, other minor changes 2014-01-18 12:11:43 -05:00
Jason Denney a35bd2f22b make sign up mobile friendly 2014-01-15 00:33:32 -05:00
Jason Denney c9d2da3e50 Make sign up links like buttons 2014-01-15 00:00:18 -05:00
Jason Denney df3c29a899 Use twitter bootstrap classes on login 2014-01-12 14:24:57 -05:00
Jason Denney a11f5a1b91 Needed to add task_list_id as attr_accessible
Also improved labels for adding task
2013-11-21 20:36:35 -05:00
Jason Denney 2a6e10ee9d Disabling by default was just annoying 2013-11-21 20:35:23 -05:00
Jason Denney ed1f2e6be4 Update schema.rb to make last migration 2013-11-21 19:38:10 -05:00
Jason Denney 852a00881e Velocipede changed their wording to CLASS
Can't find purpose by name, will have to rely on default ID in case wording is changed.
2013-11-21 19:37:30 -05:00
Jason Denney 8cc72c9f48 Added link to "add a bike" video 2013-11-17 22:49:11 -05:00
Jason Denney 90b3043d67 wording 2013-11-17 16:57:29 -05:00
Jason Denney 2ad9fab243 Move some instructions to proper section 2013-11-17 16:55:00 -05:00
Jason Denney 43db139dc7 Adding information to the about section 2013-11-17 16:51:46 -05:00
Jason Denney 2b72094f00 Update read me with youtube video link 2013-11-16 18:42:23 -05:00
99 changed files with 4308 additions and 375 deletions
-2
View File
@@ -6,7 +6,5 @@ tmp/
.powrc
.rbenv-version
.DS_Store
public/extjs
public/assets
public/images/icons
config/database.yml
-1
View File
@@ -1,2 +1 @@
--colour
-rturnip
+1
View File
@@ -0,0 +1 @@
velocipede
+1
View File
@@ -0,0 +1 @@
ruby-2.1.1
-55
View File
@@ -1,55 +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.
environment_id="ruby-1.9.3-p374@velocipede"
#
# Uncomment following line if you want options to be set only for given project.
#
# PROJECT_JRUBY_OPTS=( --1.9 )
#
# 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"
if [[ -s "${rvm_path:-$HOME/.rvm}/hooks/after_use" ]]
then
. "${rvm_path:-$HOME/.rvm}/hooks/after_use"
fi
else
# If the environment file has not yet been created, use the RVM CLI to select.
if ! rvm --create use "$environment_id"
then
echo "Failed to create RVM environment '${environment_id}'."
return 1
fi
fi
#
# If you use an RVM gemset file to install a list of gems (*.gems), you can have
# it be automatically loaded. Uncomment the following and adjust the filename if
# necessary.
#
# filename=".gems"
# if [[ -s "$filename" ]]
# then
# rvm gemset import "$filename" | grep -v already | grep -v listed | grep -v complete | sed '/^$/d'
# fi
# If you use bundler, this might be useful to you:
# if command -v bundle && [[ -s Gemfile ]]
# then
# bundle install
# fi
+26
View File
@@ -0,0 +1,26 @@
FROM ruby:2.1
# throw errors if Gemfile has been modified since Gemfile.lock
RUN bundle config --global frozen 1
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
RUN apt-get update && apt-get install -y nodejs --no-install-recommends && \
apt-get install -y mysql-client postgresql-client sqlite3 --no-install-recommends && \
apt-get install -y zip unzip --no-install-recommends && \
rm -rf /var/lib/apt/lists/*
COPY Gemfile /usr/src/app/
COPY Gemfile.lock /usr/src/app/
RUN bundle install
COPY install_extjs.rb /usr/src/app/
RUN /usr/src/app/install_extjs.rb
COPY install_famfamfam.rb /usr/src/app/
RUN /usr/src/app/install_famfamfam.rb
EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]
+11 -11
View File
@@ -14,8 +14,9 @@ gem 'decent_exposure', '~> 1.0.1'
gem 'devise', '~> 2.0.4'
gem 'haml-rails', '~> 0.3.4'
gem 'jquery-rails', '~> 2.0'
gem 'pg'
gem 'pg', '~> 0.17.1'
gem 'will_paginate', '~> 3.0.3'
gem 'jbuilder', '~> 2.0.3'
# Gems used only for assets and not required
# in production environments by default.
@@ -30,21 +31,20 @@ group :assets do
end
group :development, :test do
gem 'rspec-rails', '~> 2.8.1'
gem 'rspec-rails', '~> 2.14.0'
gem 'factory_girl_rails', '~> 1.2'
gem 'pry', '~> 0.9.8'
gem 'faker'
gem 'faker', '~> 1.2.0'
end
group :test do
gem 'shoulda-matchers', '~> 1.0.0'
gem 'capybara', '~> 1.1.2'
gem 'turnip', '~> 0.3.0'
gem 'database_cleaner'
gem 'launchy'
gem 'spork'
gem 'capybara', '~> 2.2.1'
gem 'poltergeist', '~> 1.5.0'
gem 'database_cleaner', '~> 1.2.0'
gem 'launchy', '~> 2.4.2'
gem 'spork', '~> 0.9.2'
#guard dependency for Mac OS 10
gem 'rb-fsevent', :require => false if RUBY_PLATFORM =~ /darwin/i
gem 'guard-spork'
gem 'guard-rspec'
gem 'guard-spork', '~> 1.5.1'
gem 'guard-rspec', '~> 4.2.6'
end
+110 -93
View File
@@ -36,42 +36,43 @@ GEM
activesupport (3.2.13)
i18n (= 0.6.1)
multi_json (~> 1.0)
addressable (2.3.3)
arel (3.0.2)
bcrypt-ruby (3.0.1)
bootstrap-will_paginate (0.0.9)
addressable (2.4.0)
arel (3.0.3)
bcrypt (3.1.10)
bcrypt-ruby (3.1.5)
bcrypt (>= 3.1.3)
bootstrap-will_paginate (0.0.10)
will_paginate
builder (3.0.4)
cancan (1.6.9)
capybara (1.1.4)
cancan (1.6.10)
capybara (2.2.1)
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
rack-test (>= 0.5.4)
selenium-webdriver (~> 2.0)
xpath (~> 0.1.4)
childprocess (0.3.9)
xpath (~> 2.0)
childprocess (0.5.9)
ffi (~> 1.0, >= 1.0.11)
coderay (1.0.9)
cliver (0.3.2)
coderay (1.1.1)
coffee-rails (3.2.2)
coffee-script (>= 2.2.0)
railties (~> 3.2.0)
coffee-script (2.2.0)
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.6.2)
commonjs (0.2.6)
database_cleaner (0.9.1)
coffee-script-source (1.10.0)
commonjs (0.2.7)
database_cleaner (1.2.0)
decent_exposure (1.0.2)
devise (2.0.5)
devise (2.0.6)
bcrypt-ruby (~> 3.0)
orm_adapter (~> 0.0.3)
railties (~> 3.1)
warden (~> 1.1.1)
diff-lcs (1.1.3)
diff-lcs (1.2.5)
erubis (2.7.0)
execjs (1.4.0)
multi_json (~> 1.0)
execjs (2.6.0)
factory_girl (2.6.4)
activesupport (>= 2.3.9)
factory_girl_rails (1.7.0)
@@ -79,19 +80,21 @@ GEM
railties (>= 3.0.0)
faker (1.2.0)
i18n (~> 0.5)
ffi (1.6.0)
formatador (0.2.4)
gherkin (2.11.6)
json (>= 1.7.6)
guard (1.7.0)
ffi (1.9.10)
formatador (0.2.5)
guard (2.13.0)
formatador (>= 0.2.4)
listen (>= 0.6.0)
lumberjack (>= 1.0.2)
pry (>= 0.9.10)
thor (>= 0.14.6)
guard-rspec (1.2.1)
guard (>= 1.1)
guard-spork (1.5.0)
listen (>= 2.7, <= 4.0)
lumberjack (~> 1.0)
nenv (~> 0.1)
notiffany (~> 0.0)
pry (>= 0.9.12)
shellany (~> 0.0)
thor (>= 0.18.1)
guard-rspec (4.2.10)
guard (~> 2.1)
rspec (>= 2.14, < 4.0)
guard-spork (1.5.1)
childprocess (>= 0.2.3)
guard (>= 1.1)
spork (>= 0.8.4)
@@ -101,14 +104,17 @@ GEM
activesupport (>= 3.1, < 4.1)
haml (~> 3.1)
railties (>= 3.1, < 4.1)
hike (1.2.1)
hike (1.2.3)
i18n (0.6.1)
jbuilder (2.0.8)
activesupport (>= 3.0.0, < 5)
multi_json (~> 1.2)
journey (1.0.4)
jquery-rails (2.2.1)
jquery-rails (2.3.0)
railties (>= 3.0, < 5.0)
thor (>= 0.14, < 2.0)
json (1.7.7)
launchy (2.2.0)
json (1.8.3)
launchy (2.4.3)
addressable (~> 2.3)
less (2.2.2)
commonjs (~> 0.2.6)
@@ -116,15 +122,18 @@ GEM
actionpack (>= 3.1)
less (~> 2.2.0)
libv8 (3.3.10.4)
listen (0.7.3)
lumberjack (1.0.3)
mail (2.5.3)
i18n (>= 0.4.0)
listen (3.0.6)
rb-fsevent (>= 0.9.3)
rb-inotify (>= 0.9.7)
lumberjack (1.0.10)
mail (2.5.4)
mime-types (~> 1.16)
treetop (~> 1.4.8)
method_source (0.8.1)
mime-types (1.22)
multi_json (1.8.2)
method_source (0.8.2)
mime-types (1.25.1)
mini_portile2 (2.0.0)
multi_json (1.11.2)
nenv (0.3.0)
netzke-basepack (0.8.4)
netzke-core (~> 0.8.2)
netzke-cancan (0.8.2)
@@ -133,20 +142,29 @@ GEM
netzke-core (0.8.4)
execjs
uglifier
nokogiri (1.5.9)
nokogiri (1.6.7.2)
mini_portile2 (~> 2.0.0.rc2)
notiffany (0.0.8)
nenv (~> 0.1)
shellany (~> 0.0)
orm_adapter (0.0.7)
pg (0.15.1)
polyglot (0.3.3)
pry (0.9.12)
coderay (~> 1.0.5)
pg (0.17.1)
poltergeist (1.5.1)
capybara (~> 2.1)
cliver (~> 0.3.1)
multi_json (~> 1.0)
websocket-driver (>= 0.2.0)
polyglot (0.3.5)
pry (0.9.12.6)
coderay (~> 1.0)
method_source (~> 0.8)
slop (~> 3.4)
rack (1.4.5)
rack-cache (1.2)
rack (1.4.7)
rack-cache (1.6.1)
rack (>= 0.4)
rack-ssl (1.3.3)
rack-ssl (1.3.4)
rack
rack-test (0.6.2)
rack-test (0.6.3)
rack (>= 1.0)
rails (3.2.13)
actionmailer (= 3.2.13)
@@ -163,61 +181,60 @@ GEM
rake (>= 0.8.7)
rdoc (~> 3.4)
thor (>= 0.14.6, < 2.0)
rake (10.0.4)
rb-fsevent (0.9.3)
rake (10.5.0)
rb-fsevent (0.9.7)
rb-inotify (0.9.7)
ffi (>= 0.5.0)
rdoc (3.12.2)
json (~> 1.4)
rspec (2.8.0)
rspec-core (~> 2.8.0)
rspec-expectations (~> 2.8.0)
rspec-mocks (~> 2.8.0)
rspec-core (2.8.0)
rspec-expectations (2.8.0)
diff-lcs (~> 1.1.2)
rspec-mocks (2.8.0)
rspec-rails (2.8.1)
rspec (2.14.1)
rspec-core (~> 2.14.0)
rspec-expectations (~> 2.14.0)
rspec-mocks (~> 2.14.0)
rspec-core (2.14.8)
rspec-expectations (2.14.5)
diff-lcs (>= 1.1.3, < 2.0)
rspec-mocks (2.14.6)
rspec-rails (2.14.2)
actionpack (>= 3.0)
activemodel (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
rspec (~> 2.8.0)
rubyzip (0.9.9)
selenium-webdriver (2.31.0)
childprocess (>= 0.2.5)
multi_json (~> 1.0)
rubyzip
websocket (~> 1.0.4)
rspec-core (~> 2.14.0)
rspec-expectations (~> 2.14.0)
rspec-mocks (~> 2.14.0)
shellany (0.0.1)
shoulda-matchers (1.0.0)
slop (3.4.4)
slop (3.6.0)
spork (0.9.2)
sprockets (2.2.2)
sprockets (2.2.3)
hike (~> 1.2)
multi_json (~> 1.0)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
therubyracer (0.10.2)
libv8 (~> 3.3.10)
thor (0.18.1)
tilt (1.3.6)
treetop (1.4.12)
thor (0.19.1)
tilt (1.4.1)
treetop (1.4.15)
polyglot
polyglot (>= 0.3.1)
turnip (0.3.1)
gherkin (>= 2.5)
rspec (~> 2.0)
twitter-bootstrap-rails (2.0.9)
actionpack (>= 3.1)
less-rails (~> 2.2.2)
railties (>= 3.1)
therubyracer (~> 0.10.1)
tzinfo (0.3.38)
uglifier (2.1.2)
tzinfo (0.3.46)
uglifier (2.7.2)
execjs (>= 0.3.0)
multi_json (~> 1.0, >= 1.0.2)
json (>= 1.8.0)
warden (1.1.1)
rack (>= 1.0)
websocket (1.0.7)
will_paginate (3.0.4)
xpath (0.1.4)
websocket-driver (0.6.3)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2)
will_paginate (3.0.7)
xpath (2.0.0)
nokogiri (~> 1.3)
PLATFORMS
@@ -227,29 +244,29 @@ DEPENDENCIES
acts_as_loggable!
bootstrap-will_paginate (~> 0.0.6)
cancan
capybara (~> 1.1.2)
capybara (~> 2.2.1)
coffee-rails (~> 3.2.1)
database_cleaner
database_cleaner (~> 1.2.0)
decent_exposure (~> 1.0.1)
devise (~> 2.0.4)
factory_girl_rails (~> 1.2)
faker
guard-rspec
guard-spork
faker (~> 1.2.0)
guard-rspec (~> 4.2.6)
guard-spork (~> 1.5.1)
haml-rails (~> 0.3.4)
jbuilder (~> 2.0.3)
jquery-rails (~> 2.0)
launchy
launchy (~> 2.4.2)
netzke-basepack (~> 0.8.0)
netzke-cancan
netzke-core (~> 0.8.0)
pg
pg (~> 0.17.1)
poltergeist (~> 1.5.0)
pry (~> 0.9.8)
rails (= 3.2.13)
rb-fsevent
rspec-rails (~> 2.8.1)
rspec-rails (~> 2.14.0)
shoulda-matchers (~> 1.0.0)
spork
turnip (~> 0.3.0)
spork (~> 0.9.2)
twitter-bootstrap-rails (~> 2.0.3)
uglifier (>= 1.0.3)
will_paginate (~> 3.0.3)
+32 -26
View File
@@ -1,38 +1,44 @@
# About
A web application for bicycle collectives to track bicycles, bicycle work history, volunteer hours, volunteer work history, and volunteers currently in the shop.
See an overview video of what the desktop view looks like and how it works here: https://www.youtube.com/watch?v=0-JjM6d9nK4.
## Overview/Instructions/Guides
See [guides](doc/guides.md)
# Developer Setup
1. use rvm
1. allow the .rvmrc file
1. `gem install bundler`
1. `bundle`
1. Install Postgres (Mac OSX instructions below)
1. `rake db:create db:migrate`
1. `rake db:seed`
1. Download extJS 4.1 (A version of 4.1 is hosted here: http://my.jasondenney.com/extjs-4.1.1.zip) Latest versions at http://www.sencha.com/products/extjs. Unzip and place where ever you like.
1. Link to your extJS folder path under `public/extjs`: (From app root) `ln -s /MY/PATH/extjs/ public/extjs`
1. `rails s`
## Running with Docker (recommended)
1. These instructions haven't been tested, please provide corrections!
1. Install Docker Toolbox <https://www.docker.com/toolbox>
1. Make sure you have a machine running: `docker-machine start default && eval "$(docker-machine env default)"`
1. Execute `docker-compose build`
1. Execute `docker-compose run web rake db:create db:migrate`
1. Execute `docker-compose up`
1. If using Docker Toolbox, use `docker-machine ip default` to get the IP where the server is running.
1. Test the Rails server is running with by visiting `<INSERT IP>:8080` in your browser.
# Postgres 9.2 Mac OSX Install
1. Install homebrew `ruby -e "$(curl -fsSL https://raw.github.com/mxcl/homebrew/go)"`
1. `brew install postgres`
1. First time db initialization `initdb /usr/local/var/postgres -E utf8`
1. Start Postgres `pg_ctl -D /usr/local/var/postgres -l /usr/local/var/postgres/server.log start`
1. Create your PG user `createuser -d -P velocipede`
1. Create your database `createdb -U velocipede --owner=velocipede velocipede`
1. Copy over DB config: `cp config/database.yml.example config/database.yml`
1. Update config with your database (velocipede), user (velocipede), and password.
### Developer Workflow
The project directory should already be mounted inside the container, so you should be able to make live changes. However, since the project is running in the 'web' container, you need to prepend commands with `docker-compose run web`.
# Optional
Add icons
You'll likely want to add the following aliases:
```
alias dm='docker-machine'
alias dc='docker-compose'
alias dcrw='docker-compose run web'
```
1. Download icons from http://www.famfamfam.com/lab/icons/silk/
1. Link to the icons under `public/images/icons`: (From app root) `ln -s /MY/PATH/famfamfam_silk_icons/icons public/images/icons`
That way your commands can be shortened to:
```
dcrw rake routes
dcrw rails console
dcrw rspec
```
If there's a better way, I'm all ears. Alternatively you could ssh into the machine with `dcrw bash`.
# License
Velocipede is released under the MIT license (http://opensource.org/licenses/MIT)
<a href="http://madewithloveinbaltimore.org">Made with &hearts; in Baltimore</a>
Testing
+6 -1
View File
@@ -12,4 +12,9 @@
//
//= require jquery
//= require jquery_ujs
//= require custom_netzke_helpers
//= require twitter/bootstrap/bootstrap-button
//= require twitter/bootstrap/bootstrap-modal
//= require utils
//= require bootstrap-datepicker
//= require bootstrap-timepicker
//= require jquery-date-format
+31
View File
@@ -0,0 +1,31 @@
$('.btn').button();
$("#add_bike_submit").click(function(){
json_data = { bikes: [{
serial_number: $("#serial_number").val(),
bike_brand_id: parseInt($("#bike_brand_id").val()),
shop_id: parseInt($("#shop_id").val()),
model: $("#model").val(),
bike_style_id: parseInt($('input[name=bike_style]:checked').val()),
seat_tube_height: parseFloat($("#seat_tube_height").val()),
bike_condition_id: parseInt($('input[name=bike_condition]:checked').val()),
bike_purpose_id: 1,
bike_wheel_size_id: parseInt($("#bike_wheel_size_id").val()),
}]};
$.ajax({
url: $("#add_bike_submit").data("url"),
type: "POST",
data: JSON.stringify(json_data),
contentType: 'application/json',
dataType: "json",
success: function(data, status, xhr){
window.location = data.bikes[0].id + "?add_bike=1";
},
error: function(data, status ){
displayFormErrors(data.responseJSON);
}
});
});
+474
View File
@@ -0,0 +1,474 @@
/* =========================================================
* bootstrap-datepicker.js
* http://www.eyecon.ro/bootstrap-datepicker
* =========================================================
* Copyright 2012 Stefan Petre
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ========================================================= */
!function( $ ) {
// Picker object
var Datepicker = function(element, options){
this.element = $(element);
this.format = DPGlobal.parseFormat(options.format||this.element.data('date-format')||'mm/dd/yyyy');
this.picker = $(DPGlobal.template)
.appendTo('body')
.on({
click: $.proxy(this.click, this)//,
//mousedown: $.proxy(this.mousedown, this)
});
this.isInput = this.element.is('input');
this.component = this.element.is('.date') ? this.element.find('.add-on') : false;
if (this.isInput) {
this.element.on({
focus: $.proxy(this.show, this),
//blur: $.proxy(this.hide, this),
keyup: $.proxy(this.update, this)
});
} else {
if (this.component){
this.component.on('click', $.proxy(this.show, this));
} else {
this.element.on('click', $.proxy(this.show, this));
}
}
this.minViewMode = options.minViewMode||this.element.data('date-minviewmode')||0;
if (typeof this.minViewMode === 'string') {
switch (this.minViewMode) {
case 'months':
this.minViewMode = 1;
break;
case 'years':
this.minViewMode = 2;
break;
default:
this.minViewMode = 0;
break;
}
}
this.viewMode = options.viewMode||this.element.data('date-viewmode')||0;
if (typeof this.viewMode === 'string') {
switch (this.viewMode) {
case 'months':
this.viewMode = 1;
break;
case 'years':
this.viewMode = 2;
break;
default:
this.viewMode = 0;
break;
}
}
this.startViewMode = this.viewMode;
this.weekStart = options.weekStart||this.element.data('date-weekstart')||0;
this.weekEnd = this.weekStart === 0 ? 6 : this.weekStart - 1;
this.onRender = options.onRender;
this.fillDow();
this.fillMonths();
this.update();
this.showMode();
};
Datepicker.prototype = {
constructor: Datepicker,
show: function(e) {
this.picker.show();
this.height = this.component ? this.component.outerHeight() : this.element.outerHeight();
this.place();
$(window).on('resize', $.proxy(this.place, this));
if (e ) {
e.stopPropagation();
e.preventDefault();
}
if (!this.isInput) {
}
var that = this;
$(document).on('mousedown', function(ev){
if ($(ev.target).closest('.datepicker').length == 0) {
that.hide();
}
});
this.element.trigger({
type: 'show',
date: this.date
});
},
hide: function(){
this.picker.hide();
$(window).off('resize', this.place);
this.viewMode = this.startViewMode;
this.showMode();
if (!this.isInput) {
$(document).off('mousedown', this.hide);
}
//this.set();
this.element.trigger({
type: 'hide',
date: this.date
});
},
set: function() {
var formated = DPGlobal.formatDate(this.date, this.format);
if (!this.isInput) {
if (this.component){
this.element.find('input').prop('value', formated);
}
this.element.data('date', formated);
} else {
this.element.prop('value', formated);
}
},
setValue: function(newDate) {
if (typeof newDate === 'string') {
this.date = DPGlobal.parseDate(newDate, this.format);
} else {
this.date = new Date(newDate);
}
this.set();
this.viewDate = new Date(this.date.getFullYear(), this.date.getMonth(), 1, 0, 0, 0, 0);
this.fill();
},
place: function(){
var offset = this.component ? this.component.offset() : this.element.offset();
this.picker.css({
top: offset.top + this.height,
left: offset.left
});
},
update: function(newDate){
this.date = DPGlobal.parseDate(
typeof newDate === 'string' ? newDate : (this.isInput ? this.element.prop('value') : this.element.data('date')),
this.format
);
this.viewDate = new Date(this.date.getFullYear(), this.date.getMonth(), 1, 0, 0, 0, 0);
this.fill();
},
fillDow: function(){
var dowCnt = this.weekStart;
var html = '<tr>';
while (dowCnt < this.weekStart + 7) {
html += '<th class="dow">'+DPGlobal.dates.daysMin[(dowCnt++)%7]+'</th>';
}
html += '</tr>';
this.picker.find('.datepicker-days thead').append(html);
},
fillMonths: function(){
var html = '';
var i = 0
while (i < 12) {
html += '<span class="month">'+DPGlobal.dates.monthsShort[i++]+'</span>';
}
this.picker.find('.datepicker-months td').append(html);
},
fill: function() {
var d = new Date(this.viewDate),
year = d.getFullYear(),
month = d.getMonth(),
currentDate = this.date.valueOf();
this.picker.find('.datepicker-days th:eq(1)')
.text(DPGlobal.dates.months[month]+' '+year);
var prevMonth = new Date(year, month-1, 28,0,0,0,0),
day = DPGlobal.getDaysInMonth(prevMonth.getFullYear(), prevMonth.getMonth());
prevMonth.setDate(day);
prevMonth.setDate(day - (prevMonth.getDay() - this.weekStart + 7)%7);
var nextMonth = new Date(prevMonth);
nextMonth.setDate(nextMonth.getDate() + 42);
nextMonth = nextMonth.valueOf();
var html = [];
var clsName,
prevY,
prevM;
while(prevMonth.valueOf() < nextMonth) {
if (prevMonth.getDay() === this.weekStart) {
html.push('<tr>');
}
clsName = this.onRender(prevMonth);
prevY = prevMonth.getFullYear();
prevM = prevMonth.getMonth();
if ((prevM < month && prevY === year) || prevY < year) {
clsName += ' old';
} else if ((prevM > month && prevY === year) || prevY > year) {
clsName += ' new';
}
if (prevMonth.valueOf() === currentDate) {
clsName += ' active';
}
html.push('<td class="day '+clsName+'">'+prevMonth.getDate() + '</td>');
if (prevMonth.getDay() === this.weekEnd) {
html.push('</tr>');
}
prevMonth.setDate(prevMonth.getDate()+1);
}
this.picker.find('.datepicker-days tbody').empty().append(html.join(''));
var currentYear = this.date.getFullYear();
var months = this.picker.find('.datepicker-months')
.find('th:eq(1)')
.text(year)
.end()
.find('span').removeClass('active');
if (currentYear === year) {
months.eq(this.date.getMonth()).addClass('active');
}
html = '';
year = parseInt(year/10, 10) * 10;
var yearCont = this.picker.find('.datepicker-years')
.find('th:eq(1)')
.text(year + '-' + (year + 9))
.end()
.find('td');
year -= 1;
for (var i = -1; i < 11; i++) {
html += '<span class="year'+(i === -1 || i === 10 ? ' old' : '')+(currentYear === year ? ' active' : '')+'">'+year+'</span>';
year += 1;
}
yearCont.html(html);
},
click: function(e) {
e.stopPropagation();
e.preventDefault();
var target = $(e.target).closest('span, td, th');
if (target.length === 1) {
switch(target[0].nodeName.toLowerCase()) {
case 'th':
switch(target[0].className) {
case 'switch':
this.showMode(1);
break;
case 'prev':
case 'next':
this.viewDate['set'+DPGlobal.modes[this.viewMode].navFnc].call(
this.viewDate,
this.viewDate['get'+DPGlobal.modes[this.viewMode].navFnc].call(this.viewDate) +
DPGlobal.modes[this.viewMode].navStep * (target[0].className === 'prev' ? -1 : 1)
);
this.fill();
this.set();
break;
}
break;
case 'span':
if (target.is('.month')) {
var month = target.parent().find('span').index(target);
this.viewDate.setMonth(month);
} else {
var year = parseInt(target.text(), 10)||0;
this.viewDate.setFullYear(year);
}
if (this.viewMode !== 0) {
this.date = new Date(this.viewDate);
this.element.trigger({
type: 'changeDate',
date: this.date,
viewMode: DPGlobal.modes[this.viewMode].clsName
});
}
this.showMode(-1);
this.fill();
this.set();
break;
case 'td':
if (target.is('.day') && !target.is('.disabled')){
var day = parseInt(target.text(), 10)||1;
var month = this.viewDate.getMonth();
if (target.is('.old')) {
month -= 1;
} else if (target.is('.new')) {
month += 1;
}
var year = this.viewDate.getFullYear();
this.date = new Date(year, month, day,0,0,0,0);
this.viewDate = new Date(year, month, Math.min(28, day),0,0,0,0);
this.fill();
this.set();
this.element.trigger({
type: 'changeDate',
date: this.date,
viewMode: DPGlobal.modes[this.viewMode].clsName
});
}
break;
}
}
},
mousedown: function(e){
e.stopPropagation();
e.preventDefault();
},
showMode: function(dir) {
if (dir) {
this.viewMode = Math.max(this.minViewMode, Math.min(2, this.viewMode + dir));
}
this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).show();
}
};
$.fn.datepicker = function ( option, val ) {
return this.each(function () {
var $this = $(this),
data = $this.data('datepicker'),
options = typeof option === 'object' && option;
if (!data) {
$this.data('datepicker', (data = new Datepicker(this, $.extend({}, $.fn.datepicker.defaults,options))));
}
if (typeof option === 'string') data[option](val);
});
};
$.fn.datepicker.defaults = {
onRender: function(date) {
return '';
}
};
$.fn.datepicker.Constructor = Datepicker;
var DPGlobal = {
modes: [
{
clsName: 'days',
navFnc: 'Month',
navStep: 1
},
{
clsName: 'months',
navFnc: 'FullYear',
navStep: 1
},
{
clsName: 'years',
navFnc: 'FullYear',
navStep: 10
}],
dates:{
days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"],
months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
},
isLeapYear: function (year) {
return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0))
},
getDaysInMonth: function (year, month) {
return [31, (DPGlobal.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]
},
parseFormat: function(format){
var separator = format.match(/[.\/\-\s].*?/),
parts = format.split(/\W+/);
if (!separator || !parts || parts.length === 0){
throw new Error("Invalid date format.");
}
return {separator: separator, parts: parts};
},
parseDate: function(date, format) {
var parts = date.split(format.separator),
date = new Date(),
val;
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
if (parts.length === format.parts.length) {
var year = date.getFullYear(), day = date.getDate(), month = date.getMonth();
for (var i=0, cnt = format.parts.length; i < cnt; i++) {
val = parseInt(parts[i], 10)||1;
switch(format.parts[i]) {
case 'dd':
case 'd':
day = val;
date.setDate(val);
break;
case 'mm':
case 'm':
month = val - 1;
date.setMonth(val - 1);
break;
case 'yy':
year = 2000 + val;
date.setFullYear(2000 + val);
break;
case 'yyyy':
year = val;
date.setFullYear(val);
break;
}
}
date = new Date(year, month, day, 0 ,0 ,0);
}
return date;
},
formatDate: function(date, format){
var val = {
d: date.getDate(),
m: date.getMonth() + 1,
yy: date.getFullYear().toString().substring(2),
yyyy: date.getFullYear()
};
val.dd = (val.d < 10 ? '0' : '') + val.d;
val.mm = (val.m < 10 ? '0' : '') + val.m;
var date = [];
for (var i=0, cnt = format.parts.length; i < cnt; i++) {
date.push(val[format.parts[i]]);
}
return date.join(format.separator);
},
headTemplate: '<thead>'+
'<tr>'+
'<th class="prev">&lsaquo;</th>'+
'<th colspan="5" class="switch"></th>'+
'<th class="next">&rsaquo;</th>'+
'</tr>'+
'</thead>',
contTemplate: '<tbody><tr><td colspan="7"></td></tr></tbody>'
};
DPGlobal.template = '<div class="datepicker dropdown-menu">'+
'<div class="datepicker-days">'+
'<table class=" table-condensed">'+
DPGlobal.headTemplate+
'<tbody></tbody>'+
'</table>'+
'</div>'+
'<div class="datepicker-months">'+
'<table class="table-condensed">'+
DPGlobal.headTemplate+
DPGlobal.contTemplate+
'</table>'+
'</div>'+
'<div class="datepicker-years">'+
'<table class="table-condensed">'+
DPGlobal.headTemplate+
DPGlobal.contTemplate+
'</table>'+
'</div>'+
'</div>';
}( window.jQuery );
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -1,7 +1,7 @@
$(document).ready(function(){
$("#checkin_menu").show();
$("#checkin").click( function(e){
var username = $("#user_email").val();
var username = $("#user_username").val();
var password = $("#user_password").val();
$.ajax({
type: 'POST',
@@ -13,7 +13,7 @@ $(document).ready(function(){
complete: function() { },
success: function(data) {
alert("Checked IN!");
$("#user_email").val('');
$("#user_username").val('');
$("#user_password").val('');
},
error: function(data,textStatus) {
@@ -22,7 +22,7 @@ $(document).ready(function(){
})
});
$("#checkout").click( function(e){
var username = $("#user_email").val();
var username = $("#user_username").val();
var password = $("#user_password").val();
$.ajax({
type: 'POST',
@@ -34,7 +34,8 @@ $(document).ready(function(){
complete: function() { },
success: function(data) {
alert("Checked OUT!");
$("#user_email").val('');
$("#user_username").val('');
$("#user_password").val('');
},
error: function(data,textStatus) {
+445
View File
@@ -0,0 +1,445 @@
var DateFormat = {};
(function($) {
var daysInWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
var shortMonthsInYear = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
var longMonthsInYear = ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'];
var shortMonthsToNumber = { 'Jan': '01', 'Feb': '02', 'Mar': '03', 'Apr': '04', 'May': '05', 'Jun': '06',
'Jul': '07', 'Aug': '08', 'Sep': '09', 'Oct': '10', 'Nov': '11', 'Dec': '12' };
var YYYYMMDD_MATCHER = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.?\d{0,3}[Z\-+]?(\d{2}:?\d{2})?/;
$.format = (function() {
function numberToLongDay(value) {
// 0 to Sunday
// 1 to Monday
return daysInWeek[parseInt(value, 10)] || value;
}
function numberToShortMonth(value) {
// 1 to Jan
// 2 to Feb
var monthArrayIndex = parseInt(value, 10) - 1;
return shortMonthsInYear[monthArrayIndex] || value;
}
function numberToLongMonth(value) {
// 1 to January
// 2 to February
var monthArrayIndex = parseInt(value, 10) - 1;
return longMonthsInYear[monthArrayIndex] || value;
}
function shortMonthToNumber(value) {
// Jan to 01
// Feb to 02
return shortMonthsToNumber[value] || value;
}
function parseTime(value) {
// 10:54:50.546
// => hour: 10, minute: 54, second: 50, millis: 546
// 10:54:50
// => hour: 10, minute: 54, second: 50, millis: ''
var time = value,
values,
subValues,
hour,
minute,
second,
millis = '',
delimited,
timeArray;
if(time.indexOf('.') !== -1) {
delimited = time.split('.');
// split time and milliseconds
time = delimited[0];
millis = delimited[1];
}
timeArray = time.split(':');
if(timeArray.length === 3) {
hour = timeArray[0];
minute = timeArray[1];
// '20 GMT-0200 (BRST)'.replace(/\s.+/, '').replace(/[a-z]/gi, '');
// => 20
// '20Z'.replace(/\s.+/, '').replace(/[a-z]/gi, '');
// => 20
second = timeArray[2].replace(/\s.+/, '').replace(/[a-z]/gi, '');
// '01:10:20 GMT-0200 (BRST)'.replace(/\s.+/, '').replace(/[a-z]/gi, '');
// => 01:10:20
// '01:10:20Z'.replace(/\s.+/, '').replace(/[a-z]/gi, '');
// => 01:10:20
time = time.replace(/\s.+/, '').replace(/[a-z]/gi, '');
return {
time: time,
hour: hour,
minute: minute,
second: second,
millis: millis
};
}
return { time : '', hour : '', minute : '', second : '', millis : '' };
}
function padding(value, length) {
var paddingCount = length - String(value).length;
for(var i = 0; i < paddingCount; i++) {
value = '0' + value;
}
return value;
}
return {
parseDate: function(value) {
var parsedDate = {
date: null,
year: null,
month: null,
dayOfMonth: null,
dayOfWeek: null,
time: null
};
if(typeof value == 'number') {
return this.parseDate(new Date(value));
} else if(typeof value.getFullYear == 'function') {
parsedDate.year = String(value.getFullYear());
// d = new Date(1900, 1, 1) // 1 for Feb instead of Jan.
// => Thu Feb 01 1900 00:00:00
parsedDate.month = String(value.getMonth() + 1);
parsedDate.dayOfMonth = String(value.getDate());
parsedDate.time = parseTime(value.toTimeString());
} else if(value.search(YYYYMMDD_MATCHER) != -1) {
/* 2009-04-19T16:11:05+02:00 || 2009-04-19T16:11:05Z */
values = value.split(/[T\+-]/);
parsedDate.year = values[0];
parsedDate.month = values[1];
parsedDate.dayOfMonth = values[2];
parsedDate.time = parseTime(values[3].split('.')[0]);
} else {
values = value.split(' ');
switch (values.length) {
case 6:
/* Wed Jan 13 10:43:41 CET 2010 */
parsedDate.year = values[5];
parsedDate.month = shortMonthToNumber(values[1]);
parsedDate.dayOfMonth = values[2];
parsedDate.time = parseTime(values[3]);
break;
case 2:
/* 2009-12-18 10:54:50.546 */
subValues = values[0].split('-');
parsedDate.year = subValues[0];
parsedDate.month = subValues[1];
parsedDate.dayOfMonth = subValues[2];
parsedDate.time = parseTime(values[1]);
break;
case 7:
/* Tue Mar 01 2011 12:01:42 GMT-0800 (PST) */
case 9:
/* added by Larry, for Fri Apr 08 2011 00:00:00 GMT+0800 (China Standard Time) */
case 10:
/* added by Larry, for Fri Apr 08 2011 00:00:00 GMT+0200 (W. Europe Daylight Time) */
parsedDate.year = values[3];
parsedDate.month = shortMonthToNumber(values[1]);
parsedDate.dayOfMonth = values[2];
parsedDate.time = parseTime(values[4]);
break;
case 1:
/* added by Jonny, for 2012-02-07CET00:00:00 (Doctrine Entity -> Json Serializer) */
subValues = values[0].split('');
parsedDate.year = subValues[0] + subValues[1] + subValues[2] + subValues[3];
parsedDate.month = subValues[5] + subValues[6];
parsedDate.dayOfMonth = subValues[8] + subValues[9];
parsedDate.time = parseTime(subValues[13] + subValues[14] + subValues[15] + subValues[16] + subValues[17] + subValues[18] + subValues[19] + subValues[20]);
break;
default:
return null;
}
}
parsedDate.date = new Date(parsedDate.year, parsedDate.month - 1, parsedDate.dayOfMonth);
parsedDate.dayOfWeek = String(parsedDate.date.getDay());
return parsedDate;
},
date : function(value, format) {
try {
var parsedDate = this.parseDate(value);
if(parsedDate === null) {
return value;
}
var date = parsedDate.date,
year = parsedDate.year,
month = parsedDate.month,
dayOfMonth = parsedDate.dayOfMonth,
dayOfWeek = parsedDate.dayOfWeek,
time = parsedDate.time;
var pattern = '',
retValue = '',
unparsedRest = '',
inQuote = false;
/* Issue 1 - variable scope issue in format.date (Thanks jakemonO) */
for(var i = 0; i < format.length; i++) {
var currentPattern = format.charAt(i);
// Look-Ahead Right (LALR)
var nextRight = format.charAt(i + 1);
if (inQuote) {
if (currentPattern == "'") {
retValue += (pattern === '') ? "'" : pattern;
pattern = '';
inQuote = false;
} else {
pattern += currentPattern;
}
continue;
}
pattern += currentPattern;
unparsedRest = '';
switch (pattern) {
case 'ddd':
retValue += numberToLongDay(dayOfWeek);
pattern = '';
break;
case 'dd':
if(nextRight === 'd') {
break;
}
retValue += padding(dayOfMonth, 2);
pattern = '';
break;
case 'd':
if(nextRight === 'd') {
break;
}
retValue += parseInt(dayOfMonth, 10);
pattern = '';
break;
case 'D':
if(dayOfMonth == 1 || dayOfMonth == 21 || dayOfMonth == 31) {
dayOfMonth = parseInt(dayOfMonth, 10) + 'st';
} else if(dayOfMonth == 2 || dayOfMonth == 22) {
dayOfMonth = parseInt(dayOfMonth, 10) + 'nd';
} else if(dayOfMonth == 3 || dayOfMonth == 23) {
dayOfMonth = parseInt(dayOfMonth, 10) + 'rd';
} else {
dayOfMonth = parseInt(dayOfMonth, 10) + 'th';
}
retValue += dayOfMonth;
pattern = '';
break;
case 'MMMM':
retValue += numberToLongMonth(month);
pattern = '';
break;
case 'MMM':
if(nextRight === 'M') {
break;
}
retValue += numberToShortMonth(month);
pattern = '';
break;
case 'MM':
if(nextRight === 'M') {
break;
}
retValue += padding(month, 2);
pattern = '';
break;
case 'M':
if(nextRight === 'M') {
break;
}
retValue += parseInt(month, 10);
pattern = '';
break;
case 'y':
case 'yyy':
if(nextRight === 'y') {
break;
}
retValue += pattern;
pattern = '';
break;
case 'yy':
if(nextRight === 'y') {
break;
}
retValue += String(year).slice(-2);
pattern = '';
break;
case 'yyyy':
retValue += year;
pattern = '';
break;
case 'HH':
retValue += padding(time.hour, 2);
pattern = '';
break;
case 'H':
if(nextRight === 'H') {
break;
}
retValue += parseInt(time.hour, 10);
pattern = '';
break;
case 'hh':
/* time.hour is '00' as string == is used instead of === */
hour = (parseInt(time.hour, 10) === 0 ? 12 : time.hour < 13 ? time.hour
: time.hour - 12);
retValue += padding(hour, 2);
pattern = '';
break;
case 'h':
if(nextRight === 'h') {
break;
}
hour = (parseInt(time.hour, 10) === 0 ? 12 : time.hour < 13 ? time.hour
: time.hour - 12);
retValue += parseInt(hour, 10);
// Fixing issue https://github.com/phstc/jquery-dateFormat/issues/21
// retValue = parseInt(retValue, 10);
pattern = '';
break;
case 'mm':
retValue += padding(time.minute, 2);
pattern = '';
break;
case 'm':
if(nextRight === 'm') {
break;
}
retValue += time.minute;
pattern = '';
break;
case 'ss':
/* ensure only seconds are added to the return string */
retValue += padding(time.second.substring(0, 2), 2);
pattern = '';
break;
case 's':
if(nextRight === 's') {
break;
}
retValue += time.second;
pattern = '';
break;
case 'S':
case 'SS':
if(nextRight === 'S') {
break;
}
retValue += pattern;
pattern = '';
break;
case 'SSS':
retValue += time.millis.substring(0, 3);
pattern = '';
break;
case 'a':
retValue += time.hour >= 12 ? 'PM' : 'AM';
pattern = '';
break;
case 'p':
retValue += time.hour >= 12 ? 'p.m.' : 'a.m.';
pattern = '';
break;
case "'":
pattern = '';
inQuote = true;
break;
default:
retValue += currentPattern;
pattern = '';
break;
}
}
retValue += unparsedRest;
return retValue;
} catch (e) {
if(console && console.log) {
console.log(e);
}
return value;
}
},
/*
* JavaScript Pretty Date
* Copyright (c) 2011 John Resig (ejohn.org)
* Licensed under the MIT and GPL licenses.
*
* Takes an ISO time and returns a string representing how long ago the date
* represents
*
* ('2008-01-28T20:24:17Z') // => '2 hours ago'
* ('2008-01-27T22:24:17Z') // => 'Yesterday'
* ('2008-01-26T22:24:17Z') // => '2 days ago'
* ('2008-01-14T22:24:17Z') // => '2 weeks ago'
* ('2007-12-15T22:24:17Z') // => 'more than 5 weeks ago'
*
*/
prettyDate : function(time) {
var date;
var diff;
var day_diff;
if(typeof time === 'string' || typeof time === 'number') {
date = new Date(time);
}
if(typeof time === 'object') {
date = new Date(time.toString());
}
diff = (((new Date()).getTime() - date.getTime()) / 1000);
day_diff = Math.floor(diff / 86400);
if(isNaN(day_diff) || day_diff < 0) {
return;
}
if(diff < 60) {
return 'just now';
} else if(diff < 120) {
return '1 minute ago';
} else if(diff < 3600) {
return Math.floor(diff / 60) + ' minutes ago';
} else if(diff < 7200) {
return '1 hour ago';
} else if(diff < 86400) {
return Math.floor(diff / 3600) + ' hours ago';
} else if(day_diff === 1) {
return 'Yesterday';
} else if(day_diff < 7) {
return day_diff + ' days ago';
} else if(day_diff < 31) {
return Math.ceil(day_diff / 7) + ' weeks ago';
} else if(day_diff >= 31) {
return 'more than 5 weeks ago';
}
},
toBrowserTimeZone : function(value, format) {
return this.date(new Date(value), format || 'MM/dd/yyyy HH:mm:ss');
}
};
}());
}(DateFormat));
;// require dateFormat.js
// please check `dist/jquery.dateFormat.js` for a complete version
(function($) {
$.format = DateFormat.format;
}(jQuery));
@@ -1,6 +1,6 @@
//when signed out, or session expires forward to sign in page
Ext.Ajax.on('requestexception', function(conn, response, options) {
if (response.status === 401) { window.location = '/users/sign_in'; }
if (response.status === 401 && response.statusText === "Unauthorized") { window.location = '/users/sign_in'; }
}, this);
//Override default netzke time entry field
+9
View File
@@ -0,0 +1,9 @@
$("#index_logout").click(function(){
$.ajax({
type: "DELETE",
url: $("#index_logout").data("url"),
complete: function(){
window.location.href="/";
}
});
});
+32
View File
@@ -0,0 +1,32 @@
$(".task_list_task").click(function(){
$("#update_tasks_submit").removeClass("disabled");
});
$("#update_tasks_submit").click(function(){
tasks = [];
$(".task_list_task").each(function(){
tasks.push({
id: parseInt($(this).data("id")),
done: $(this).is(":checked")
});
});
json_data = { tasks: tasks };
$.ajax({
url: $("#update_tasks_submit").data("url"),
type: "PUT",
data: JSON.stringify(json_data),
contentType: 'application/json',
dataType: "json",
success: function(data, status, xhr){
//should re-render via JS, but for now reload
location.reload();
},
error: function(data, status ){
alert("An error occured updating tasks");
//displayFormErrors(data.responseJSON);
}
});
});
+81
View File
@@ -0,0 +1,81 @@
$(document).ready(function(){
var currentdate = new Date();
$("#date_id").datepicker().on('changeDate', function(ev){
$("#date_id").datepicker('hide');
});
$("#date_id").datepicker('setValue', currentdate);
$("#start_time_id").timepicker();
$("#end_time_id").timepicker();
$("#add_time_entry_submit").click(function(){
date = $("#date_id").val();
start_date = new Date(date + " " + $("#start_time_id").val());
end_date = new Date(date + " " + $("#end_time_id").val());
forward = $("#add_time_entry_submit").data("forward");
// If a bike is selected, forward to the bike
// checklist.
bike_id = parseInt($("#bike_id").val());
if( bike_id > 0 ){
forward = "/task_lists/" + bike_id + "/edit";
}
//FIXME: Ideally, we'd submit the dates as ISO, but I can't figure out
// how to get Netzke to render UTC dates correctly (it calls to_json
// somewhere and drops off the timezone). For the time being, save dates
// in locale like Netzke.
json_data = { time_entries: [{
start_date: $.format.date(start_date, "dd-MM-yyyy hh:mm a"),
end_date: $.format.date(end_date, "dd-MM-yyyy hh:mm a"),
log_action_id: parseInt($('input[name=action_id]:checked').val()),
bike_id: bike_id,
description: $("#description_id").val(),
}]};
$.ajax({
url: $("#add_time_entry_submit").data("url"),
type: "POST",
data: JSON.stringify(json_data),
contentType: 'application/json',
dataType: "json",
success: function(data, status, xhr){
window.location = forward;
},
error: function(data, status ){
displayFormErrors(data.responseJSON);
}
});
});
$(".work_entry-delete-btn").click(function(){
row = $(this).closest("tr");
entry_id = row.data("id");
start_date = row.data("start_date");
duration = row.data("duration");
description = row.data("description");
$("#work_entry_start_date").html(start_date);
$("#work_entry_duration").html(duration);
$("#work_entry_description").html(description);
$("#confirmation_delete").data("entry_id", entry_id);
});
$("#confirmation_delete").click(function(){
entry_id = $(this).data("entry_id");
url = $("#confirmation_delete").data("url") + entry_id;
$.ajax({
url: url,
type: "delete",
contentType: 'application/json',
success: function(data, status, xhr){
location.reload();
},
error: function(data, status ){
displayFormErrors(data.responseJSON);
}
});
});
});
+8
View File
@@ -0,0 +1,8 @@
function displayFormErrors(data){
if( data.errors != undefined ){
$.each( data.errors, function( field, errorMsg) {
$("#"+field).parents(".control-group").addClass("error");
$("#"+field).siblings(".help-block").html(errorMsg);
});
}
}
+3 -1
View File
@@ -9,5 +9,7 @@
* compiled file, but it's generally better to create a new file per style scope.
*
*= require_self
*= require_tree .
*= require datepicker
*= require bootstrap-timepicker
*= require bootstrap_and_overrides
*/
+146
View File
@@ -0,0 +1,146 @@
/*!
* Timepicker Component for Twitter Bootstrap
*
* Copyright 2013 Joris de Wit
*
* Contributors https://github.com/jdewit/bootstrap-timepicker/graphs/contributors
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
.bootstrap-timepicker {
position: relative;
}
.bootstrap-timepicker.pull-right .bootstrap-timepicker-widget.dropdown-menu {
left: auto;
right: 0;
}
.bootstrap-timepicker.pull-right .bootstrap-timepicker-widget.dropdown-menu:before {
left: auto;
right: 12px;
}
.bootstrap-timepicker.pull-right .bootstrap-timepicker-widget.dropdown-menu:after {
left: auto;
right: 13px;
}
.bootstrap-timepicker .add-on {
cursor: pointer;
}
.bootstrap-timepicker .add-on i {
display: inline-block;
width: 16px;
height: 16px;
}
.bootstrap-timepicker-widget.dropdown-menu {
padding: 4px;
}
.bootstrap-timepicker-widget.dropdown-menu.open {
display: inline-block;
}
.bootstrap-timepicker-widget.dropdown-menu:before {
border-bottom: 7px solid rgba(0, 0, 0, 0.2);
border-left: 7px solid transparent;
border-right: 7px solid transparent;
content: "";
display: inline-block;
position: absolute;
}
.bootstrap-timepicker-widget.dropdown-menu:after {
border-bottom: 6px solid #FFFFFF;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
content: "";
display: inline-block;
position: absolute;
}
.bootstrap-timepicker-widget.timepicker-orient-left:before {
left: 6px;
}
.bootstrap-timepicker-widget.timepicker-orient-left:after {
left: 7px;
}
.bootstrap-timepicker-widget.timepicker-orient-right:before {
right: 6px;
}
.bootstrap-timepicker-widget.timepicker-orient-right:after {
right: 7px;
}
.bootstrap-timepicker-widget.timepicker-orient-top:before {
top: -7px;
}
.bootstrap-timepicker-widget.timepicker-orient-top:after {
top: -6px;
}
.bootstrap-timepicker-widget.timepicker-orient-bottom:before {
bottom: -7px;
border-bottom: 0;
border-top: 7px solid #999;
}
.bootstrap-timepicker-widget.timepicker-orient-bottom:after {
bottom: -6px;
border-bottom: 0;
border-top: 6px solid #ffffff;
}
.bootstrap-timepicker-widget a.btn,
.bootstrap-timepicker-widget input {
border-radius: 4px;
}
.bootstrap-timepicker-widget table {
width: 100%;
margin: 0;
}
.bootstrap-timepicker-widget table td {
text-align: center;
height: 30px;
margin: 0;
padding: 2px;
}
.bootstrap-timepicker-widget table td:not(.separator) {
min-width: 30px;
}
.bootstrap-timepicker-widget table td span {
width: 100%;
}
.bootstrap-timepicker-widget table td a {
border: 1px transparent solid;
width: 100%;
display: inline-block;
margin: 0;
padding: 8px 0;
outline: 0;
color: #333;
}
.bootstrap-timepicker-widget table td a:hover {
text-decoration: none;
background-color: #eee;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
border-color: #ddd;
}
.bootstrap-timepicker-widget table td a i {
margin-top: 2px;
font-size: 18px;
}
.bootstrap-timepicker-widget table td input {
width: 25px;
margin: 0;
text-align: center;
}
.bootstrap-timepicker-widget .modal-content {
padding: 4px;
}
@media (min-width: 767px) {
.bootstrap-timepicker-widget.modal {
width: 200px;
margin-left: -100px;
}
}
@media (max-width: 767px) {
.bootstrap-timepicker {
width: 100%;
}
.bootstrap-timepicker .dropdown-menu {
width: 100%;
}
}
+30
View File
@@ -1,10 +1,26 @@
@import "twitter/bootstrap/bootstrap";
body {
padding-top: 60px;
padding-left: 20px;
}
@import "twitter/bootstrap/responsive";
// Set the correct sprite paths
@iconSpritePath: asset-path('twitter/bootstrap/glyphicons-halflings.png');
@iconWhiteSpritePath: asset-path('twitter/bootstrap/glyphicons-halflings-white.png');
// Set the Font Awesome (Font Awesome is default. You can disable by commenting below lines)
// Note: If you use asset_path() here, your compiled boostrap_and_overrides.css will not
// have the proper paths. So for now we use the absolute path.
@fontAwesomeEotPath: '/assets/fontawesome-webfont.eot';
@fontAwesomeWoffPath: '/assets/fontawesome-webfont.woff';
@fontAwesomeTtfPath: '/assets/fontawesome-webfont.ttf';
@fontAwesomeSvgPath: '/assets/fontawesome-webfont.svg';
// Font Awesome
@import "fontawesome";
// Your custom LESS stylesheets goes here
//
// Since bootstrap was imported above you have access to its mixins which
@@ -15,3 +31,17 @@
//
// Example:
// @linkColor: #ff0000;
[data-toggle="buttons-radio"] > .btn > input[type="radio"],
[data-toggle="buttons"] > .btn > input[type="checkbox"] {
display: none;
}
.inline-block {
display: inline-block;
}
.control-group.error .btn-group > .btn {
color: #b94a48;
border-color: #b94a48;
}
+182
View File
@@ -0,0 +1,182 @@
/*!
* Datepicker for Bootstrap
*
* Copyright 2012 Stefan Petre
* Licensed under the Apache License v2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
*/
.datepicker {
top: 0;
left: 0;
padding: 4px;
margin-top: 1px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
/*.dow {
border-top: 1px solid #ddd !important;
}*/
}
.datepicker:before {
content: '';
display: inline-block;
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-bottom: 7px solid #ccc;
border-bottom-color: rgba(0, 0, 0, 0.2);
position: absolute;
top: -7px;
left: 6px;
}
.datepicker:after {
content: '';
display: inline-block;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid #ffffff;
position: absolute;
top: -6px;
left: 7px;
}
.datepicker > div {
display: none;
}
.datepicker table {
width: 100%;
margin: 0;
}
.datepicker td,
.datepicker th {
text-align: center;
width: 20px;
height: 20px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
.datepicker td.day:hover {
background: #eeeeee;
cursor: pointer;
}
.datepicker td.day.disabled {
color: #eeeeee;
}
.datepicker td.old,
.datepicker td.new {
color: #999999;
}
.datepicker td.active,
.datepicker td.active:hover {
color: #ffffff;
background-color: #006dcc;
background-image: -moz-linear-gradient(top, #0088cc, #0044cc);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));
background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);
background-image: -o-linear-gradient(top, #0088cc, #0044cc);
background-image: linear-gradient(to bottom, #0088cc, #0044cc);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0044cc', GradientType=0);
border-color: #0044cc #0044cc #002a80;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #0044cc;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
color: #fff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker td.active:hover,
.datepicker td.active:hover:hover,
.datepicker td.active:focus,
.datepicker td.active:hover:focus,
.datepicker td.active:active,
.datepicker td.active:hover:active,
.datepicker td.active.active,
.datepicker td.active:hover.active,
.datepicker td.active.disabled,
.datepicker td.active:hover.disabled,
.datepicker td.active[disabled],
.datepicker td.active:hover[disabled] {
color: #ffffff;
background-color: #0044cc;
*background-color: #003bb3;
}
.datepicker td.active:active,
.datepicker td.active:hover:active,
.datepicker td.active.active,
.datepicker td.active:hover.active {
background-color: #003399 \9;
}
.datepicker td span {
display: block;
width: 47px;
height: 54px;
line-height: 54px;
float: left;
margin: 2px;
cursor: pointer;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
.datepicker td span:hover {
background: #eeeeee;
}
.datepicker td span.active {
color: #ffffff;
background-color: #006dcc;
background-image: -moz-linear-gradient(top, #0088cc, #0044cc);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));
background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);
background-image: -o-linear-gradient(top, #0088cc, #0044cc);
background-image: linear-gradient(to bottom, #0088cc, #0044cc);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0044cc', GradientType=0);
border-color: #0044cc #0044cc #002a80;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #0044cc;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
color: #fff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker td span.active:hover,
.datepicker td span.active:focus,
.datepicker td span.active:active,
.datepicker td span.active.active,
.datepicker td span.active.disabled,
.datepicker td span.active[disabled] {
color: #ffffff;
background-color: #0044cc;
*background-color: #003bb3;
}
.datepicker td span.active:active,
.datepicker td span.active.active {
background-color: #003399 \9;
}
.datepicker td span.old {
color: #999999;
}
.datepicker th.switch {
width: 145px;
}
.datepicker th.next,
.datepicker th.prev {
font-size: 21px;
}
.datepicker thead tr:first-child th {
cursor: pointer;
}
.datepicker thead tr:first-child th:hover {
background: #eeeeee;
}
.input-append.date .add-on i,
.input-prepend.date .add-on i {
display: block;
cursor: pointer;
width: 16px;
height: 16px;
}
@@ -19,6 +19,6 @@
});
},
onChangeAccountInfo: function(){
window.location.href="users/edit";
window.location.href="/users/edit";
}
}
+6 -7
View File
@@ -2,8 +2,6 @@ class Tasks < Netzke::Basepack::Grid
def configure(c)
super
#disable by default, will be enabled once bike is clicked
c.disabled = true
task_list_id = nil
if session[:selected_bike_id]
task_list_id = Bike.find_by_id(session[:selected_bike_id]).task_list.id
@@ -31,13 +29,14 @@ class Tasks < Netzke::Basepack::Grid
end
def default_fields_for_forms
fields = []
fields << { :no_binding => true, :xtype => 'displayfield', :fieldLabel => "No Bike Selected", :value => "Select a Bike First!"}
fields.concat( [
bike = Bike.find_by_id(session[:selected_bike_id])
bike = "Select a Bike First!" if bike.nil?
[
{ :no_binding => true, :xtype => 'displayfield', :fieldLabel => "Bike Selected", :value => "#{bike.to_s}"},
:done,
:task,
:notes
])
:notes,
]
end
#override with nil to remove actions
@@ -33,7 +33,7 @@
Ext.Msg.alert("Success", "New Password: "+data.password);
},
error: function(data,textStatus) {
Ext.Msg.alert( "Error", JSON.parse(data.responseText)["error"]);
Ext.Msg.alert( "Error", JSON.parse(data.responseText)["errors"][0]);
}
});
}
+2 -2
View File
@@ -6,12 +6,12 @@ class Api::V1::BaseController < ActionController::Base
private
def authenticate_user
if params[:username]
user = User.find_for_database_authentication( :email => params[:username] )
user = User.find_for_database_authentication( :username => params[:username] )
@current_user = user if user && user.valid_password?( params[:password] )
if @current_user.nil?
msg = "Username/Password/Token invalid"
render :json => {:error => msg }, :status => 403 and return
render :json => {:error => msg }, :status => 401 and return
end
else
authenticate_user!
@@ -0,0 +1,32 @@
class Api::V1::BikesController < Api::V1::BaseController
CANNOT_MANAGE = "You do not have permission to manage bikes."
EXPECTED_BIKE = "Expected bike in submitted data"
NOT_FOUND = "The bike could not be found."
before_filter :check_bike_permission, except: :show
def create
if params[:bikes] && bike = params[:bikes].first
@bike = Bike.new(bike)
if !@bike.save
render json: { errors: @bike.errors }, status: 422 and return
end
else
render json: { errors: [EXPECTED_BIKE]}, status: 422 and return
end
end
def show
@bike = Bike.find_by_id(params[:id])
if @bike.nil?
render json: { errors: [NOT_FOUND] }, status: 404 and return
end
end
private
def check_bike_permission
if cannot? :manage, Bike
render json: { errors: [CANNOT_MANAGE]}, status: 403 and return
end
end
end
@@ -0,0 +1,24 @@
class Api::V1::TaskListsController < Api::V1::BaseController
CANNOT_MANAGE = "You do not have permission to manage this task list."
NOT_FOUND = "The task list could not be found."
before_filter :get_task_list
before_filter :check_task_list_permission, except: :show
def show
end
private
def get_task_list
@task_list = TaskList.find_by_id(params[:id])
if @task_list.nil?
render json: { errors: [NOT_FOUND] }, status: 404 and return
end
end
def check_task_list_permission
if cannot? :manage, Bike and @task_list.item != current_user.bike
render json: { errors: [CANNOT_MANAGE]}, status: 403 and return
end
end
end
@@ -0,0 +1,64 @@
class Api::V1::TasksController < Api::V1::BaseController
EXPECTED_TASKS = "Expected a list of tasks in submitted data."
CANNOT_MANAGE = "You do not have permission to manage this task."
NOT_FOUND = "The task could not be found."
before_filter :validate_params
before_filter :get_tasks
before_filter :check_task_permission, except: :show
def update
errors = []
@tasks.each do |task_hash|
task = task_hash[:record]
attrs = task_hash[:new_attributes]
task.update_attributes(attrs)
if !task.errors.empty?
errors << { id: task.id, errors: task.errors }
end
end
if !errors.empty?
render json: { errors: errors }, status: 422 and return
end
end
private
def validate_params
if params[:tasks].nil? and not params[:tasks].kind_of?(Array)
render json: { errors: [EXPECTED_TASKS]}, status: 422 and return
end
end
def get_tasks
@tasks = []
errors = []
params[:tasks].each do |task|
t = Task.find_by_id(task[:id])
if t.nil?
errors << { id: task[:id], error: NOT_FOUND }
else
@tasks << { record: t, new_attributes: task }
end
end
if !errors.empty?
render json: { errors: errors }, status: 404 and return
end
end
def check_task_permission
errors = []
@tasks.each do |task_hash|
task = task_hash[:record]
if task.task_list.item != current_user.bike
errors << { id: task[:id], error: CANNOT_MANAGE }
end
end
if cannot? :manage, Bike and !errors.empty?
render json: { errors: errors}, status: 403 and return
end
end
end
@@ -0,0 +1,33 @@
class Api::V1::TimeEntriesController < Api::V1::BaseController
EXPECTED_TIME_ENTRY = "Expected time entry in submitted data"
NOT_FOUND = "Time entry not found"
def create
if params[:time_entries] && time_entry = params[:time_entries].first
time_entry.merge!({ loggable_id: current_user.id,
logger_id: current_user.id })
bike_id = time_entry[:bike_id].to_i
@time_entry = TimeEntry.new(time_entry.except(:bike_id))
if bike_id > 0
@time_entry.copy_to_bike_history(bike_id)
end
if !@time_entry.save
render json: { errors: @time_entry.errors }, status: 422 and return
end
else
render json: { errors: [EXPECTED_TIME_ENTRY]}, status: 422 and return
end
end
def delete
if time_entry = TimeEntry.find_by_id(params[:id])
time_entry.delete
render nothing: true, status: 204 and return
else
render json: { errors: [NOT_FOUND]}, status: 404 and return
end
end
end
+11 -4
View File
@@ -1,21 +1,27 @@
require 'securerandom'
class Api::V1::UsersController < Api::V1::BaseController
CANNOT_MANAGE = "You do not have the permission to manager users"
NOT_FOUND = "User not found"
NOT_ALLOWED = "Not allowed to reset your own password in this fashion"
PASS_LENGTH = 8
def password_reset
if can? :manage, User
user = User.find_by_id(params[:user_id])
render :json => { "error" => "User not found"}, :status => 404 and return if user.nil?
render :json => { "error" => "Not allowed to reset your own password in this fashion."}, :status => 403 and return if user.id == current_user.id
render :json => { "errors" => [NOT_FOUND]}, :status => 404 and return if user.nil?
render :json => { "errors" => [NOT_ALLOWED]}, :status => 403 and return if user.id == current_user.id
new_pass = SecureRandom.hex[0,8]
new_pass = SecureRandom.hex[0,PASS_LENGTH]
user.password = new_pass
user.save
render :json => { "password" => new_pass}, :status => 200 and return
else
render :json => { "error" => "You do not have the permission"}, :status => 403 and return
render :json => { "errors" => [CANNOT_MANAGE]}, :status => 403 and return
end
end
=begin Is this here by accident? Commenting out for now (1/30/14)
def checkout
#must use @current_user since user may not have signed in
if !@current_user.checked_in?
@@ -25,4 +31,5 @@ class Api::V1::UsersController < Api::V1::BaseController
render :nothing => true, :status => 204 and return
end
end
=end
end
+16
View File
@@ -0,0 +1,16 @@
class BikesController < AuthenticatedController
def new
@brands = BikeBrand.all.map{ |b| [b.brand, b.id] }
@brands.unshift( ["Select a brand", -1] )
@wheel_sizes = BikeWheelSize.all.map{ |w| [w.display_string, w.id] }
@wheel_sizes.unshift( ["Select a wheel size", -1] )
end
def show
@bike = Bike.find_by_id(params[:id])
@task_list_id = @bike.task_list.id
@show_add_bike = true if params[:add_bike]
end
end
+7
View File
@@ -0,0 +1,7 @@
class PanelController < ApplicationController
def index
render :inline => "<%=netzke :app_view, :layout => true %>", :layout => "netzke"
end
end
+1 -2
View File
@@ -1,7 +1,6 @@
class SiteController < ApplicationController
def index
render :inline => "<%= netzke :app_view, :layout => true %>", :layout => "application"
@bike = current_user.bike
end
end
+5
View File
@@ -0,0 +1,5 @@
class TaskListsController < AuthenticatedController
def edit
@task_list = TaskList.find_by_id(params[:id])
end
end
@@ -0,0 +1,16 @@
class TimeEntriesController < AuthenticatedController
def new
@bikes = Bike.all.map{ |b| [b.to_s , b.id] }
if bike = current_user.bike
@bikes.unshift( [bike.to_s, bike.id] )
end
@bikes.unshift( ["Non-bike work", -1] )
end
def index
@user_time_entries = TimeEntry.where(loggable_id: current_user.id)
@credits_available = current_user.total_credits
@hours_worked = current_user.total_hours
end
end
+1 -1
View File
@@ -23,7 +23,7 @@ class Ability
def user
can :read, :all
can :manage, Bike, :id => @current_user.bike_id unless @current_user.bike.nil?
can :manage, @current_user.bike unless @current_user.bike.nil?
can :manage, ::ActsAsLoggable::Log, :loggable_type => "Bike", :loggable_id => @current_user.bike_id
can :manage, ::ActsAsLoggable::Log, :loggable_type => "User", :loggable_id => @current_user.id
end
+7 -7
View File
@@ -16,13 +16,13 @@ class Bike < ActiveRecord::Base
validates :shop_id, :presence => true, :uniqueness => true, :numericality => { :only_integer => true }
validates :serial_number, :length => { :minimum => 3 }
validates :model, :length => { :maximum => 50 }
validates :bike_brand_id, :presence => true
validates :color, :presence => true
validates :bike_style_id, :presence => true
validates :seat_tube_height, :presence => true
validates :bike_wheel_size_id, :presence => true
validates :bike_condition_id, :presence => true
validates :bike_purpose_id, :presence => true
validates :bike_brand_id, :presence => true, :numericality => { greater_than: 0, message: "is not a valid brand" }
#validates :color, :presence => true
validates :bike_style_id, :presence => true, :numericality => { greater_than: 0, message: "is not a valid style" }
validates :seat_tube_height, :presence => true, :numericality => true
validates :bike_wheel_size_id, :presence => true, :numericality => { greater_than: 0, message: "is not a valid wheel size" }
validates :bike_condition_id, :presence => true, :numericality => { greater_than: 0, message: "is not a valid condition" }
validates :bike_purpose_id, :presence => true, :numericality => { greater_than: 0, message: "is not a valid purpose" }
self.per_page = 15
+1 -1
View File
@@ -1,5 +1,5 @@
class Task < ActiveRecord::Base
attr_accessible :task, :notes, :done
attr_accessible :task, :notes, :done, :task_list_id
belongs_to :task_list
+1 -1
View File
@@ -4,7 +4,7 @@ class TaskList < ActiveRecord::Base
attr_accessible :item_id, :item_type, :name
belongs_to :item, :polymorphic => true
has_many :tasks
has_many :tasks, order: "id ASC"
after_save :create_default_bike_tasks
+27
View File
@@ -0,0 +1,27 @@
class TimeEntry < ActsAsLoggable::Log
default_scope where( loggable_type: "User",
logger_type: "User",
log_action_type: "ActsAsLoggable::UserAction").where("log_action_id != 4").order("start_date DESC")
def copy_to_bike_history(bike_id)
self.assign_attributes({
copy_log: true,
copy_type: 'Bike',
copy_id: bike_id,
copy_action_type: 'ActsAsLoggable::BikeAction',
copy_action_id: 4
})
end
def duration
end_date - start_date
end
def duration_in_hours
(duration / 1.hour).round(2)
end
def type
log_action.try(:action)
end
end
+27 -19
View File
@@ -47,7 +47,8 @@ class User < ActiveRecord::Base
### TODO methods below probably belong somewhere else
def completed_build_bikes
purpose_id = BikePurpose.find_by_purpose("BUILDBIKE").id
#default BUILDBIKE/CLASS ID is 5
purpose_id = 5
Bike.find_by_sql("
SELECT *
FROM bikes
@@ -64,15 +65,16 @@ class User < ActiveRecord::Base
end
def total_credits_spent
log_action = ::ActsAsLoggable::TransactionAction.find_by_action("TIME")
log_action_id = 1 #TIME
transaction_logs.
where( "log_action_id = ? AND log_action_type = ?",
log_action.id, log_action.class.to_s).
log_action_id, ::ActsAsLoggable::TransactionAction.to_s).
sum{ |r| r.description.to_i }.round(2)
end
def total_earned_credits
log_action = ::ActsAsLoggable::UserAction.find_by_action("CHECKIN")
volunteer_id = 1
staff_id = 3
# Find the first credit conversion which has a created_at date before the
# log's created_at date and join it to the log's row so we can calculate
@@ -81,7 +83,7 @@ class User < ActiveRecord::Base
#
# The DISTINCT ON, and ORDER BY are important to getting the
# single conversion rate that applies to the respective log.
::ActsAsLoggable::Log.find_by_sql("
::ActsAsLoggable::Log.find_by_sql(["
SELECT DISTINCT ON (logs.created_at) start_date, end_date,
conversion.conversion, conversion.created_at
FROM logs
@@ -89,23 +91,26 @@ class User < ActiveRecord::Base
SELECT conversion, created_at
FROM credit_conversions
) AS conversion ON logs.created_at > conversion.created_at
WHERE logs.loggable_id = #{self.id}
WHERE logs.loggable_id = :id
AND logs.loggable_type = 'User'
AND (log_action_id != #{log_action.id} AND log_action_type = '#{log_action.class.to_s}')
ORDER BY logs.created_at, conversion.created_at DESC").
AND (log_action_id IN (:credit_actions) AND log_action_type = :log_action_type)
ORDER BY logs.created_at, conversion.created_at DESC",
{id: self.id,
credit_actions: [volunteer_id, staff_id],
log_action_type: ::ActsAsLoggable::UserAction.to_s}]).
sum{ |l| ((l.end_date - l.start_date)/3600) * l.conversion.to_i}.round(2)
end
def total_hours
log_action = ::ActsAsLoggable::UserAction.find_by_action("CHECKIN")
logs.where("log_action_id != ? AND log_action_type = ?", log_action.id, log_action.class.to_s).sum { |l| (l.end_date - l.start_date)/3600 }.round(2)
log_action_id = 4 #CHECKIN
logs.where("log_action_id != ? AND log_action_type = ?", log_action_id, ::ActsAsLoggable::UserAction.to_s).sum { |l| (l.end_date - l.start_date)/3600 }.round(2)
end
def current_month_hours
log_action = ::ActsAsLoggable::UserAction.find_by_action("CHECKIN")
log_action_id = 4 #CHECKIN
#TODO need to prevent users from saving logs across months, force to create a new log if crossing month
current_month_range = (Time.now.beginning_of_month..Time.now.end_of_month)
logs.where("log_action_id != ? AND log_action_type = ?", log_action.id, log_action.class.to_s)
logs.where("log_action_id != ? AND log_action_type = ?", log_action_id, ::ActsAsLoggable::UserAction.to_s)
.where( :start_date => current_month_range)
.where( :end_date => current_month_range)
.sum { |l| (l.end_date - l.start_date)/3600 }
@@ -113,28 +118,31 @@ class User < ActiveRecord::Base
end
def checked_in?
log_action = ::ActsAsLoggable::UserAction.find_by_action("CHECKIN")
checked = logs.where( log_action_id: log_action.id).
#default CHECKIN log action is id, yea yea should be a constant
log_action_id = 4
checked = logs.where( log_action_id: log_action_id).
where("start_date >= ?", Time.zone.now.beginning_of_day).
where("start_date = end_date")
!checked.empty?
end
def checkin
log_action = ::ActsAsLoggable::UserAction.find_by_action("CHECKIN")
#default CHECKIN log action is id, yea yea should be a constant
log_action_id = 4
time = Time.now
logs.create( logger_id: self.id,
logger_type: self.class.to_s,
start_date: time,
end_date: time,
log_action_id: log_action.id,
log_action_type: log_action.class.to_s)
log_action_id: log_action_id,
log_action_type: ::ActsAsLoggable::UserAction.to_s)
save
end
def checkout
log_action = ::ActsAsLoggable::UserAction.find_by_action("CHECKIN")
checked = logs.where( log_action_id: log_action.id).
#default CHECKIN log action is id, yea yea should be a constant
log_action_id = 4
checked = logs.where( log_action_id: log_action_id).
where("start_date >= ?", Time.zone.now.beginning_of_day).
where("start_date = end_date").first
checked.end_date = Time.now
@@ -0,0 +1,4 @@
json.bikes [@bike] do |bike|
json.array! bike
json.set! :href, api_bike_path(bike)
end
@@ -0,0 +1,3 @@
json.bikes [@bike] do |bike|
json.array! bike
end
@@ -0,0 +1,15 @@
json.task_lists [@task_list] do |tl|
json.array! tl
json.links do
json.bike do
json.href api_bike_path(tl.item)
json.id tl.item_id
end
json.tasks tl.tasks do |task|
json.id task.id
json.done task.done
json.notes task.notes
json.task task.task
end
end
end
@@ -0,0 +1,3 @@
json.tasks [@tasks.map{|x| x[:record]}] do |task|
json.array! task
end
@@ -0,0 +1,4 @@
json.time_entries [@time_entry] do |time_entry|
json.array! time_entry
#json.set! :href, api_time_entry_path(time_entry)
end
+62
View File
@@ -0,0 +1,62 @@
%a{ class: "btn btn-default btn-lg", href: root_path}
%span{ class:"icon-home"}
%h2 Add Bike
%p
.control-group
.controls
%input{id: "shop_id", placeholder: "Shop ID", type: "number", min:0, class: "input-lg" }
.help-block
.control-group
.controls
= select_tag(:bike_brand_id, options_for_select(@brands))
.help-block
.control-group
.controls
%input{id: "model", placeholder: "Model", type: "text", class: "input-lg" }
.help-block
.control-group
.controls
%input{id: "serial_number", placeholder: "Serial Number", type: "text", class: "input-lg" }
.help-block
.control-group
.controls
.btn-group{ "data-toggle" => "buttons-radio"}
%label{ class: "btn btn-default"}
%input{ type: "radio", name: "bike_style", value: 3} RD
%label{ class: "btn btn-default"}
%input{ type: "radio", name: "bike_style", value: 1} MTN
%label{ class: "btn btn-default"}
%input{ type: "radio", name: "bike_style", value: 2} HYB
%label{ class: "btn btn-default"}
%input{ type: "radio", name: "bike_style", value: 4} OTHER
%input{ id: "bike_style_id", type: "hidden"}
.help-block
.control-group
.controls
= select_tag(:bike_wheel_size_id, options_for_select(@wheel_sizes), id: :bike_wheel_size_id)
.help-block
.control-group
.controls
.btn-group{ "data-toggle" => "buttons-radio"}
%label{ class: "btn btn-default"}
%input{ type: "radio", name: "bike_condition", value: 2} Poor
%label{ class: "btn btn-default"}
%input{ type: "radio", name: "bike_condition", value: 3} Fair
%label{ class: "btn btn-default"}
%input{ type: "radio", name: "bike_condition", value: 4} Good
%label{ class: "btn btn-default"}
%input{ type: "radio", name: "bike_condition", value: 5} Excellent
%input{ id: "bike_condition_id", type: "hidden"}
.help-block
.control-group
.controls
%input{id: "seat_tube_height", placeholder: "Seat Tube (cm)", type: "number", min: 0, max: 100, class: "input-lg" }
.help-block
-# Commenting this out until description is added to Bike
%p
%input{id: "bike_description", placeholder: "Short description", type: "text", class: "input-lg" }
.control-group
.controls
%input{id: "add_bike_submit", value: "Add Bike", type: "button", class: "btn btn-lg btn-block btn-primary", "data-url" => "#{api_create_bike_path}"}
+27
View File
@@ -0,0 +1,27 @@
- if @show_add_bike
%p
%a{class: "btn btn-lg btn-block btn-info", href: new_bike_path} Add Another Bike?
%a{ class: "btn btn-default btn-lg", href: root_path}
%span{ class:"icon-home"}
%h2 #{@bike.shop_id}: #{@bike.bike_brand}
%h2 #{@bike.model}
%dl.dl-horizontal
%dt Type
%dd #{@bike.bike_style}
%dt Wheel Size
%dd #{@bike.bike_wheel_size.display_string}
%dt Condition
%dd #{@bike.bike_condition}
%dt Seat Tube (cm)
%dd #{@bike.seat_tube_height}
%dt Purpose
%dd #{@bike.bike_purpose}
- if !@bike.value.nil?
%dt Value
%dd #{@bike.value}
- if !@bike.color.nil?
%dt Color
%dd{ style: "width: 50px; background-color: ##{@bike.color}; border: black; border-width: 1px; border-style: solid;"} #{@bike.color}
%a{class: "btn btn-lg btn-block btn-primary", href: edit_task_list_path(@task_list_id) } View Checklist
+12 -5
View File
@@ -1,21 +1,28 @@
<%- if controller_name != 'sessions' %>
<%= link_to "Sign in", new_session_path(resource_name) %><br />
<p>
<%= link_to "Sign in", new_session_path(resource_name), class: "btn btn-block btn-default" %><br />
</p>
<% end -%>
<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
<%= link_to "Sign up", new_registration_path(resource_name) %><br />
<p>
<%= link_to "Sign up", new_registration_path(resource_name), class: "btn btn-block btn-default" %><br />
</p>
<% end -%>
<%- if devise_mapping.recoverable? && controller_name != 'passwords' %>
<%= link_to "Forgot your password?", new_password_path(resource_name) %><br />
<p>
<%= link_to "Forgot your password?", new_password_path(resource_name), class: "btn btn-block btn-default" %><br />
</p>
<% end -%>
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
<%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %><br />
<p>
<%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name), class: "btn btn-block btn-default" %><br />
</p>
<% end -%>
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %><br />
<% end -%>
<%- if devise_mapping.omniauthable? %>
+8 -4
View File
@@ -1,12 +1,16 @@
<h2>Forgot your password?</h2>
<%= stylesheet_link_tag "bootstrap_and_overrides", :media => "all" %>
<h2>Forgot your password?</h2>
<br>
<br>
<%= form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :post }) do |f| %>
<%= devise_error_messages! %>
<div><%= f.label :email %><br />
<%= f.email_field :email %></div>
<div class="form-group">
<%= f.email_field :email, placeholder: "Email", class: "form-control input-lg" %>
</div>
<div><%= f.submit "Send me reset password instructions" %></div>
<div><%= f.submit "Reset Password", class:"btn btn-lg btn-primary"%></div>
<% end %>
<%= render "links" %>
@@ -1,25 +1,14 @@
%fieldset
.control-group
= f.label :addrStreet1, :class => "control-label"
.controls
= f.text_field :addrStreet1, :class => "control-label"
.control-group
= f.label :addrStreet2, :class => "control-label"
.controls
= f.text_field :addrStreet2, :class => "control-label"
.control-group
= f.label :addrCity, :class => "control-label"
.controls
= f.text_field :addrCity, :class => "control-label"
.control-group
= f.label :addrState, :class => "control-label"
.controls
= f.text_field :addrState, :class => "control-label"
.control-group
= f.label :addrZip, :class => "control-label"
.controls
= f.text_field :addrZip, :class => "control-label"
.control-group
= f.label :phone, :class => "control-label"
.controls
= f.text_field :phone, :class => "control-label"
%p
%fieldset
.form-group
= f.text_field :addrStreet1, placeholder: "Street Address Line 1", :class => "form-control input-lg"
.form-group
= f.text_field :addrStreet2, placeholder: "Street Address Line 2",:class => "form-control input-lg"
.form-group
= f.text_field :addrCity, placeholder: "City", :class => "form-control input-lg"
.form-group
= f.text_field :addrState, placeholder: "State Abbreviation", :class => "form-control input-lg"
.form-group
= f.text_field :addrZip, placeholder: "Zip Code", :class => "form-control input-lg"
.form-group
= f.text_field :phone, placeholder: "Phone", :class => "form-control input-lg"
+15 -28
View File
@@ -1,36 +1,23 @@
= stylesheet_link_tag "bootstrap_and_overrides", :media => "all"
= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :class => "form-horizontal"}) do |f|
= form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f|
= devise_error_messages!
.controls
%h2 Sign up
.control-group
= f.label :username, :class => "control-label"
.controls
= f.text_field :username
.control-group
= f.label :first_name, :class => "control-label"
.controls
= f.text_field :first_name
.control-group
= f.label :last_name, :class => "control-label"
.controls
= f.text_field :last_name
.control-group
= f.label :email, :class => "control-label"
.controls
= f.email_field :email
.form-group
= f.text_field :username, placeholder: "Username", :class => "form-control input-lg"
.form-group
= f.text_field :first_name, placeholder: "First Name", :class => "form-control input-lg"
.form-group
= f.text_field :last_name, placeholder: "Last Name", :class => "form-control input-lg"
.form-group
= f.email_field :email, placeholder: "E-mail", :class => "form-control input-lg"
- profile_builder = resource.user_profiles.empty? ? resource.user_profiles.build : resource.user_profiles
= f.fields_for :user_profiles, profile_builder do |builder|
= render 'user_profile_fields', f: builder
.control-group
= f.label :password, :class => "control-label"
.controls
= f.password_field :password
.control-group
= f.label :password_confirmation, :class => "control-label"
.controls
= f.password_field :password_confirmation
.control-group
.controls
= f.submit "Sign up"
.form-group
= f.password_field :password, placeholder: "Password", :class => "form-control input-lg"
.form-group
= f.password_field :password_confirmation, placeholder: "Password Confirmation", :class => "form-control input-lg"
.form-group
= f.submit "Sign up", class:"btn btn-lg btn-primary"
= render "links"
+25 -15
View File
@@ -5,34 +5,44 @@
<br>
<br>
<%= form_for(resource, :as => resource_name, :url => session_path(resource_name)) do |f| %>
<div><%= f.label :username%><br />
<%= f.text_field :username%></div>
<div class="form-group">
<%= f.text_field :username, placeholder: "Username", class: "form-control input-lg"%>
</div>
<div><%= f.label :password %><br />
<%= f.password_field :password %></div>
<div class="form-group">
<%= f.password_field :password, placeholder: "Password", class: "form-control input-lg" %>
</div>
<div class="form-group">
<% if devise_mapping.rememberable? -%>
<div><%= f.check_box :remember_me %> <%= f.label :remember_me %></div>
<label>
<%= f.check_box :remember_me %> Remember Me
</label>
<% end -%>
</div>
<div><%= f.submit "Sign in" %></div>
<div id="checkin_menu"style="display:none;">
<input id="checkin" name="checkin" type="button" value="CHECK IN">
<input id="checkout" name="checkout" type="button" value="CHECK OUT">
<div class="form-group">
<p>
<%= f.submit "Sign in", class:"btn btn-lg btn-primary" %>
</p>
<p> Or quickly... </p>
<div class="btn-group">
<input id="checkin" name="checkin" type="button" value="CHECK IN" class="btn btn-lg btn-success">
<input id="checkout" name="checkout" type="button" value="CHECK OUT" class="btn btn-lg btn-danger">
</div>
</div>
<% end %>
<%= render "links" %>
<% if Rails.env.development? %>
<p>
<% User.all.each do |user| %>
<%= form_for(resource, :as => resource_name, :url => session_path(resource_name)) do |f| %>
<%= f.hidden_field :username, :value => user.username%></div>
<%= f.hidden_field :password, :value => 'password' %></div>
<div><%= f.submit "Sign in as #{user.username}" %></div>
<%= f.hidden_field :username, :value => user.username%>
<%= f.hidden_field :password, :value => 'password' %>
<%= f.submit "Sign in as #{user.username}", class:"btn btn-info" %>
<% end %>
<% end %>
</p>
<% end %>
+5 -4
View File
@@ -2,14 +2,15 @@
%html{:lang => "en"}
%head
%meta{:charset => "utf-8"}/
%meta{ name: "viewport", content: "width=device-width", "initial-scale" => "1.0"}
%title= content_for?(:title) ? yield(:title) : "Velocipede"
= load_netzke
= csrf_meta_tags
= stylesheet_link_tag "bootstrap_and_overrides", "datepicker", "bootstrap-timepicker", :media => "all"
/[if lt IE 9]
= javascript_include_tag "http://html5shim.googlecode.com/svn/trunk/html5.js"
:css
body {
padding-top: 60px; /* 60px to make the container go all the way to the bottom of the topbar */
padding-top: 5px;
}
.x-boundlist-item {
white-space: nowrap;
@@ -24,11 +25,11 @@
- if flash[:alert]
%p{:class => 'alert'}= flash[:alert]
.row
.span13
.span12
= yield
%footer
%p &copy; Velocipede 2013
%p &copy; BikeShed #{Time.now.year}
= javascript_include_tag "application"
= javascript_include_tag params[:controller]
+11
View File
@@ -0,0 +1,11 @@
!!! 5
%html{:lang => "en"}
%head
%meta{:charset => "utf-8"}/
%title= content_for?(:title) ? yield(:title) : "Velocipede"
= load_netzke
= csrf_meta_tags
%body
= yield
= javascript_include_tag "application"
= javascript_include_tag params[:controller]
+21
View File
@@ -0,0 +1,21 @@
= stylesheet_link_tag "bootstrap_and_overrides", :media => "all"
%h2 Dashboard
%p
%p
%a{class: "btn btn-lg btn-block btn-primary", href: new_time_entry_path} Add Time Entry
%p
%a{class: "btn btn-lg btn-block btn-primary", href: time_entries_path} View Timesheet
- if can? :manage, Bike
%p
%a{class: "btn btn-lg btn-block btn-primary", href: new_bike_path} Add Bike
- if !@bike.nil?
%p
%a{class: "btn btn-lg btn-block btn-primary", href: bike_path(@bike)} View Your Bike
%p
%p
%a{class: "btn btn-lg btn-block btn-primary hidden-xs", href: admin_index_path} Desktop View
%p
%input{id: "index_logout", value: "Logout", type: "button", class: "btn btn-lg btn-block btn-danger", "data-url" => destroy_user_session_path }
+16
View File
@@ -0,0 +1,16 @@
%a{ class: "btn btn-default btn-lg", href: root_path}
%span{ class:"icon-home"}
%h2 Task List
%h3
%a{ href: bike_path(@task_list.item)}#{@task_list.item.shop_id} #{@task_list.item.bike_brand}
%h4 #{@task_list.item.model}
%p
.control-group
- @task_list.tasks.each do |task|
.controls
%input{class: "task_list_task", type: "checkbox", "data-id" => task.id, checked: task.done}
#{task.task}
.control-group
.controls
%input{id: "update_tasks_submit", value: "Save Changes", type: "button", class: "btn btn-lg btn-block btn-primary disabled", "data-url" => "#{api_update_task_path}"}
+44
View File
@@ -0,0 +1,44 @@
%a{ class: "btn btn-default btn-lg", href: root_path}
%span{ class:"icon-home"}
%h2 Your Timesheet
%dl.dl-horizontal
%dd
%span.badge #{@hours_worked}
Total Hours Worked
%dd
%span.badge #{@credits_available}
Total Credits Available
%table.table
%tbody
- @user_time_entries.each do |entry|
%tr{ "data-id" => entry.id, "data-description" => entry.description,
"data-duration" => entry.duration_in_hours,
"data-start_date" => entry.start_date.to_date.to_formatted_s(:rfc822)}
%td #{entry.start_date.to_date.to_formatted_s(:rfc822)}
%td #{entry.duration_in_hours}
%td #{entry.type}
%td #{truncate(entry.description)}
%td
%button{ class: "btn icon-remove btn-danger work_entry-delete-btn", role: "button", "data-toggle" => "modal", "data-target" => "#confirmation" }
%a{class: "btn btn-lg btn-block btn-primary", href: new_time_entry_path } Add Time Entry
.modal{ id: "confirmation", class: "hide", role: "dialog", "aria-labelledby" => "confirmation_title", "aria-hidden" => "true", tabindex: -1 }
.modal-header
%button{ class: "close", "data-dismiss" => "modal", "aria-hidden" => "true"}x
%h4{ id: "confirmation_title" } Are you sure you want to delete?
.modal-body
%div{ id: "work_entry_start_date" }
Start Date
%div{ id: "work_entry_duration" }
Duration
%div{ id: "work_entry_description" }
Description
.modal-footer
%button{ class: "btn", "data-dismiss" => "modal", "aria-hidden" => "true"} Close
%button{ id: "confirmation_delete", class: "btn btn-primary", "data-url" => "api/v1/time_entries/" } Delete
+42
View File
@@ -0,0 +1,42 @@
%a{ class: "btn btn-default btn-lg", href: root_path}
%span{ class:"icon-home"}
%h2 Add Time Entry
%p
.control-group
.controls
%input{id: "date_id", placeholder: "Date", type: "text", class: "datepicker input-small" }
.help-block
.control-group
.controls{ class: "bootstrap-timepicker"}
%label Start
%input{id: "start_time_id", placeholder: "Time ID", type: "text", class: "input-small" }
.hidden{ id: "start_date" }
.help-block
.control-group
.controls
%label End
%input{id: "end_time_id", placeholder: "Time ID", type: "text", class: "input-small" }
.hidden{ id: "end_date" }
.help-block
.control-group
.controls
.btn-group{ "data-toggle" => "buttons-radio"}
%label{ class: "btn btn-default"}
%input{ type: "radio", name: "action_id", value: 1} Volunteer
%label{ class: "btn btn-default"}
%input{ type: "radio", name: "action_id", value: 2} Personal
%label{ class: "btn btn-default"}
%input{ type: "radio", name: "action_id", value: 3} Staff
.hidden{ id: "log_action_id" }
.help-block
.control-group
.controls
%label
= select_tag(:bike_id, options_for_select(@bikes))
.control-group
.controls
%textarea{id: "description_id", placeholder: "Work description", class: "input-lg" }
.control-group
.controls
%input{id: "add_time_entry_submit", value: "Add Time Entry", type: "button", class: "btn btn-lg btn-block btn-primary", "data-url" => "#{api_create_time_entry_path}", "data-forward" => "#{"."}"}
+11 -15
View File
@@ -1,23 +1,19 @@
development:
development: &default
adapter: postgresql
database: velocipede
username: velocipede
password:
host: 127.0.0.1
encoding: unicode
pool: 5
database: <%= ENV['RDS_DB_NAME'] || 'postgres' %>
username: <%= ENV['RDS_USERNAME'] || 'postgres' %>
password: <%= ENV['RDS_PASSWORD'] %>
host: <%= ENV['RDS_HOSTNAME'] || 'db' %>
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
adapter: postgresql
database: velocipede
username: velocipede
password:
host: 127.0.0.1
<<: *default
database: velocipede_test
production:
adapter: postgresql
database: velocipede
username: velocipede
password:
host: 127.0.0.1
<<: *default
database: velocipede_production
+8 -7
View File
@@ -222,11 +222,12 @@ Devise.setup do |config|
# end
end
#Check in the user if they sign in. (Devise uses Warden)
Warden::Manager.after_set_user do |user,auth,opts|
# this essentially gets called after every netzke request, but alas,
# only using the after_authenticaion callback doesn't get fired after
# user creation.
user.checkin unless user.checked_in?
unless Rails.env.test?
#Check in the user if they sign in. (Devise uses Warden)
Warden::Manager.after_set_user do |user,auth,opts|
# this essentially gets called after every netzke request, but alas,
# only using the after_authenticaion callback doesn't get fired after
# user creation.
user.checkin unless user.checked_in?
end
end
+24 -3
View File
@@ -4,13 +4,34 @@ Velocipede::Application.routes.draw do
netzke
root :to => 'site#index'
get 'admin/index', to: 'panel#index', as: "admin_index"
get 'bikes/new', to: 'bikes#new', as: "new_bike"
get 'bikes/:id', to: 'bikes#show', as: "bike"
get 'task_lists/:id/edit' => "task_lists#edit", as: "edit_task_list"
get 'time_entries' => "time_entries#index", as: "time_entries"
get 'time_entries/new' => "time_entries#new", as: "new_time_entry"
###########################
# API Routes
scope 'api', :module => :api do
scope 'api', :module => :api, defaults: {format: :json} do
scope 'v1', :module => :v1 do
post 'checkin' => "logs#checkin", :as => "api_checkin"
post 'checkin' => "logs#checkin", :as => "api_checkin"
post 'checkout' => "logs#checkout", :as => "api_checkout"
post 'reset' => "users#password_reset", :as => "api_password_reset"
post 'reset' => "users#password_reset", :as => "api_password_reset"
get 'bikes/:id' => "bikes#show", as: "api_bike"
post 'bikes/create' => "bikes#create", as: "api_create_bike"
get 'task_lists/:id' => "task_lists#show", as: "api_task_list"
put 'tasks/update' => "tasks#update", as: "api_update_task"
post 'time_entries/create' => "time_entries#create", as: "api_create_time_entry"
delete 'time_entries/:id' => "time_entries#delete", as: "api_delete_time_entry"
end
end
+8 -8
View File
@@ -47,14 +47,14 @@ ActiveRecord::Schema.define(:version => 20131019170248) do
end
create_table "bike_wheel_sizes", :force => true do |t|
t.string "twmm"
t.string "rdmm"
t.string "twin"
t.string "rdin"
t.string "twfr"
t.string "rdfr"
t.string "description"
t.string "tire_common_score"
t.integer "twmm"
t.integer "rdmm"
t.string "twin"
t.string "rdin"
t.string "twfr"
t.string "rdfr"
t.string "description"
t.string "tire_common_score"
end
create_table "bikes", :force => true do |t|
+3
View File
@@ -7,3 +7,6 @@ hybrid:
road:
id: 3
style: ROAD
other:
id: 4
style: OTHER
+1 -1
View File
@@ -41,7 +41,7 @@ if Rails.env.development?
#create fake bikes
if Bike.all.empty?
42.times do |n|
FactoryGirl.create(:bike)
FactoryGirl.create(:seed_bike)
end
end
elsif Rails.env.production?
-1
View File
@@ -1 +0,0 @@
# No API yet
+20
View File
@@ -0,0 +1,20 @@
# Overview
## Guides
### Adding a User
To add a user, a user must sign themselves up from the initial login screen. An admin can later log in to grant that user admin privileges afterward, if desired.
### Adding a Bike
How to add a bike to the database via the desktop view: <https://www.youtube.com/watch?v=1IchWUdMQ90>.
## Views
The app has two different views. One for "core" volunteers which I consider admins, and another for "customer" volunteers which I consider users. Because of the Netzke/ExtJS framework I used, the current UI is non-intuitive and I feel like is only adequate for admins who can take the time to learn the system. I'm in the process of designing a simpler mobile friendly UI with the essential functionality that's intended for users.
## Deployment
Currently, at the Velocipede collective we have it running on a computer in the shop on the local network. The reason for this, is so that it's only accessible from within the shop. I'm weighing out different methods for ensuring that users can only log in from within the shop (and not from home), such as a shop password that only admins can see and would make visible from within the physical bike shop. I also don't have to worry about security issues while the app is in development. In the future, I'd like to see the app hosted much like <http://freehub.bikekitchen.org/>.
## Ideas
At Velocipede, with a mobile friendly UI for users, I'm hoping we can get donations of old, unused smart phones, connected via local wifi, that can be used as stations to log a volunteer's work and hours. They can be secured by gluing cables into the earphone jacks to deter theft if necessary. This might be another way that the collective can promote reuse of materials.
+35
View File
@@ -0,0 +1,35 @@
# Local Developer Setup
1. use rvm
1. allow the .rvmrc file
1. `gem install bundler`
1. `bundle`
1. Install Postgres (Mac OSX instructions below)
1. Copy over DB config: `cp config/database.yml.example config/database.yml`
1. Update config with your database (velocipede), user (velocipede), and password.
1. `rake db:create db:migrate`
1. `rake db:seed`
1. Download extJS 4.1 (A version of 4.1 is hosted here: http://my.jasondenney.com/extjs-4.1.1.zip) Latest versions at http://www.sencha.com/products/extjs. Unzip and place where ever you like.
1. Link to your extJS folder path under `public/extjs`: (From app root) `ln -s /MY/PATH/extjs/ public/extjs`
1. `rails s`
# Postgres 9.2 Mac OSX Install
1. Install homebrew `ruby -e "$(curl -fsSL https://raw.github.com/mxcl/homebrew/go)"`
1. `brew install postgres`
1. First time db initialization `initdb /usr/local/var/postgres -E utf8`
1. Start Postgres `pg_ctl -D /usr/local/var/postgres -l /usr/local/var/postgres/server.log start`
1. Create your PG user `createuser -d -P velocipede`
1. Create your database `createdb -U velocipede --owner=velocipede velocipede`
1. Create your test database `createdb -U velocipede --owner=velocipede velocipede_test`
# Testing
1. Install phantomjs `brew install phantomjs`
1. Run tests with `rspec`
# Optional
Add icons
1. Download icons from http://www.famfamfam.com/lab/icons/silk/
1. Link to the icons under `public/images/icons`: (From app root) `ln -s /MY/PATH/famfamfam_silk_icons/icons public/images/icons`
+12
View File
@@ -0,0 +1,12 @@
db:
image: postgres
ports:
- "5432"
web:
volumes:
- .:/usr/src/app
build: .
ports:
- "8080:3000"
links:
- db
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env ruby
require 'fileutils'
extjs_url = "http://my.jasondenney.com/extjs-4.1.1.zip"
download_dir = '/tmp'
dest_dir ='/usr/lib'
extjs_download_path = File.join(download_dir,'extjs-4.1.1.zip')
files = Dir.glob(File.join(dest_dir, 'extjs', '*'))
if files.empty? and Dir.glob(extjs_download_path).empty?
`wget -P #{download_dir} #{extjs_url}`
raise "Failed downloading #{extjs_url}" if Dir.glob(extjs_download_path).empty?
end
if files.empty?
puts `unzip #{extjs_download_path} -d #{dest_dir}/`
FileUtils.mv(File.join(dest_dir, 'ext-4.1.1a'), File.join(dest_dir, 'extjs'))
end
raise "Failed unzipping #{extjs_download_path}" if Dir.glob(File.join(dest_dir,'extjs', '*')).empty?
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env ruby
require 'fileutils'
fam_url = "http://www.famfamfam.com/lab/icons/silk/famfamfam_silk_icons_v013.zip"
download_dir = '/tmp'
dest_dir = "/usr/lib/famfamfam"
fam_download_path = File.join(download_dir,'famfamfam_silk_icons_v013.zip')
files = Dir.glob(File.join(dest_dir, '*'))
if files.empty? and Dir.glob(fam_download_path).empty?
`wget -P #{download_dir} #{fam_url}`
raise "Failed downloading #{fam_url}" if Dir.glob(fam_download_path).empty?
end
if files.empty?
puts `unzip #{fam_download_path} -d #{dest_dir}/`
end
raise "Failed unzipping #{fam_download_path}" if Dir.glob(File.join(dest_dir, '*')).empty?
+1
View File
@@ -0,0 +1 @@
/usr/lib/extjs
+1
View File
@@ -0,0 +1 @@
/usr/lib/famfamfam/icons/
@@ -0,0 +1,134 @@
require 'spec_helper'
describe Api::V1::BikesController do
render_views
describe "#create" do
context "as a user" do
before(:each) do
@user = FactoryGirl.create(:user)
sign_in @user
end
it "returns 403" do
post :create
expect(@response.code.to_i).to eql 403
end
it "returns an error message" do
post :create
json = JSON.parse(@response.body)
expect(json["errors"].first).to eql Api::V1::BikesController::CANNOT_MANAGE
end
end
context "as a bike admin" do
before(:each) do
@user = FactoryGirl.create(:bike_admin)
sign_in @user
end
context "with no bike in json data" do
it "returns error status" do
post :create
expect(@response.code.to_i).to eql 422
end
it "returns an error message" do
post :create
json = JSON.parse(@response.body)
expect(json["errors"].first).to eql Api::V1::BikesController::EXPECTED_BIKE
end
end
context "with valid bike in json data" do
before(:each) do
bike_data = { bikes: [{
serial_number: "XKCD",
bike_brand_id: 1,
shop_id: 1,
model: "Bike Model",
bike_style_id: 1,
seat_tube_height: 52,
bike_condition_id: 1,
bike_purpose_id: 1,
bike_wheel_size_id: 1,
}]}
#this is necessary because render_views does not work with sign_in devise helper
@submit_json = api_submit_json(@user, bike_data)
#not sure why format: :json not working
request.accept = 'application/json'
end
it "returns 200" do
post :create, @submit_json
expect(@response.code.to_i).to eql 200
end
it "returns the created bike json" do
post :create, @submit_json
json = JSON.parse(@response.body)
expect(json).to have_key("bikes")
expect(json.to_s).to include(@submit_json[:bikes].first[:serial_number])
end
end
context "with invalid bike in json data" do
before(:each) do
@submit_json = { bikes: [{
serial_number: "XKCD",
}]}
end
it "returns 422" do
post :create, @submit_json
expect(@response.code.to_i).to eql 422
end
it "returns the fields with errors" do
post :create, @submit_json
json = JSON.parse(@response.body)
expect(json).to have_key("errors")
expect(json.to_s).to include("can't be blank")
end
end
end
end
describe "#show" do
context "as a user" do
before(:each) do
@user = FactoryGirl.create(:user)
sign_in @user
end
context "no bike exists" do
it "returns 404" do
get :show, id: 999
expect(@response.code.to_i).to eql 404
end
it "returns an error message" do
get :show, id: 999
json = JSON.parse(@response.body)
expect(json["errors"].first).to eql Api::V1::BikesController::NOT_FOUND
end
end
context "a bike exists" do
let!(:bike){ FactoryGirl.create(:bike) }
it "returns 200" do
get :show, id: bike.id, format: :json
expect(@response.code.to_i).to eql 200
end
it "returns the bike json" do
get :show, id: bike.id, format: :json
json = JSON.parse(@response.body)
expect(json).to have_key("bikes")
expect(json.to_s).to include(bike.serial_number)
end
end
end
end
end
@@ -0,0 +1,79 @@
require 'spec_helper'
describe Api::V1::LogsController do
describe "#checkin" do
before(:each) do
@user = FactoryGirl.create(:user)
sign_in @user
controller.stub(:current_user).and_return(@user)
end
context "user is not checked in" do
before(:each) do
@user.stub(:checked_in?).and_return(false)
end
it "returns 204" do
@user.stub(:checkin)
post :checkin
expect(@response.code.to_i).to eql 204
end
it "checks in a user" do
expect(@user).to receive(:checkin)
post :checkin
end
end
context "user is already checked in" do
before(:each) do
@user.stub(:checked_in?).and_return(true)
end
it "returns 404 if the user is already checked in" do
post :checkin
expect(@response.code.to_i).to eql 404
end
end
end
describe "#checkout" do
before(:each) do
@user = FactoryGirl.create(:user)
sign_in @user
controller.stub(:current_user).and_return(@user)
end
context "user is not checked in" do
before(:each) do
@user.stub(:checked_in?).and_return(false)
end
it "returns 404" do
@user.stub(:checkin)
post :checkout
expect(@response.code.to_i).to eql 404
end
end
context "user is already checked in" do
before(:each) do
@user.stub(:checked_in?).and_return(true)
end
it "returns 204" do
@user.stub(:checkout)
post :checkout
expect(@response.code.to_i).to eql 204
end
it "checks out a user" do
expect(@user).to receive(:checkout)
post :checkout
end
end
end
end
@@ -0,0 +1,52 @@
require 'spec_helper'
describe Api::V1::TaskListsController do
render_views
describe "#show" do
context "as a user" do
before(:each) do
@user = FactoryGirl.create(:user)
sign_in @user
end
context "task list exists" do
let!(:task_list){ FactoryGirl.create(:task_list) }
it "returns 200" do
get :show, id: task_list.id, format: :json
expect(@response.code.to_i).to eql 200
end
it "returns valid task list json" do
get :show, id: task_list.id, format: :json
json = JSON.parse(@response.body)
expect(json.to_s).to include(task_list.name)
end
it "returns task list with tasks json" do
get :show, id: task_list.id, format: :json
json = JSON.parse(@response.body)
expect(task_list.tasks.count).to be > 0
task_list.tasks.each do |task|
expect(json.to_s).to include(task.task)
end
end
end
context "task list does not exist" do
it "returns 404" do
get :show, id: 999, format: :json
expect(@response.code.to_i).to eql 404
end
it "returns an error message" do
get :show, id: 999, format: :json
json = JSON.parse(@response.body)
expect(json["errors"].first).
to eql Api::V1::TaskListsController::NOT_FOUND
end
end
end
end
end
+100
View File
@@ -0,0 +1,100 @@
require 'spec_helper'
describe Api::V1::TasksController do
render_views
describe "#update" do
context "as a user with a bike" do
before(:each) do
@user = FactoryGirl.create(:user_with_bike)
sign_in @user
end
context "with no tasks in json data" do
it "returns 400" do
put :update
expect(@response.code.to_i).to eql 422
end
it "returns an error message" do
put :update
json = JSON.parse(@response.body)
expect(json["errors"].first).to eql Api::V1::TasksController::EXPECTED_TASKS
end
end
context "with non existing task in data" do
before(:each) do
@submit_json = { tasks: [
{ id: 9999, done: false },
]}
end
it "returns 404" do
put :update, @submit_json
expect(@response.code.to_i).to eql 404
end
it "returns an error message" do
put :update, @submit_json
json = JSON.parse(@response.body)
expect(json["errors"].to_s).to include(Api::V1::TasksController::NOT_FOUND)
end
end
context "with valid tasks json data" do
before(:each) do
@task = FactoryGirl.create(:task, done: false, task_list_id: @user.bike.task_list.id)
task_data = { tasks: [
{ id: @task.id, done: true},
]}
#this is necessary because render_views does not work with sign_in devise helper
@submit_json = api_submit_json(@user, task_data)
#not sure why format: :json not working
request.accept = 'application/json'
end
it "returns 200" do
put :update, @submit_json
expect(@response.code.to_i).to eql 200
end
it "returns the updated task json" do
put :update, @submit_json
json = JSON.parse(@response.body)
expect(json).to have_key("tasks")
expect(json.to_s).to include(Task.find_by_id(@task.id).done.to_s)
end
it "updates the task" do
expect{put :update, @submit_json}.
to change{ Task.find_by_id(@task.id).done }.
from(false).to(true)
end
end
end
context "as a user without a bike" do
before(:each) do
@user = FactoryGirl.create(:user, bike_id: nil)
sign_in @user
task = FactoryGirl.create(:task)
@submit_json = { tasks: [
{ id: task.id, done: false },
]}
end
it "should return 403" do
put :update, @submit_json
expect(@response.code.to_i).to eql 403
end
it "returns an error message" do
put :update, @submit_json
json = JSON.parse(@response.body)
expect(json["errors"].to_s).to include(Api::V1::TasksController::CANNOT_MANAGE)
end
end
end
end
@@ -0,0 +1,117 @@
require 'spec_helper'
describe Api::V1::TimeEntriesController do
render_views
describe "#create" do
context "as a user" do
let!(:user){ FactoryGirl.create(:user) }
before(:each) do
sign_in user
end
context "with no time entry in json data" do
it "returns error status" do
post :create
json = JSON.parse(@response.body)
expect(@response.code.to_i).to eql 422
end
it "returns an error message" do
post :create
json = JSON.parse(@response.body)
expect(json["errors"].first).to eql Api::V1::TimeEntriesController::EXPECTED_TIME_ENTRY
end
end
context "with valid time entry in json data" do
before(:each) do
time_data = { time_entries: [{
start_date: Time.zone.now,
end_date: Time.zone.now + 60,
log_action_id: 1,
bike_id: -1,
description: "My description"}]
}
#this is necessary because render_views does not work with sign_in devise helper
@submit_json = api_submit_json(user, time_data)
#not sure why format: :json not working
request.accept = 'application/json'
end
it "returns 200" do
post :create, @submit_json
expect(@response.code.to_i).to eql 200
end
it "returns the created time entry json" do
post :create, @submit_json
json = JSON.parse(@response.body)
expect(json).to have_key("time_entries")
expect(json.to_s).to include(@submit_json[:time_entries].first[:description])
end
end
context "with invalid time entry in json data" do
before(:each) do
@submit_json = { time_entries: [{
description: "My description",
}]}
end
it "returns 422" do
post :create, @submit_json
expect(@response.code.to_i).to eql 422
end
it "returns the fields with errors" do
post :create, @submit_json
json = JSON.parse(@response.body)
expect(json).to have_key("errors")
expect(json.to_s).to include("can't be blank")
end
end
end
end
describe "#delete" do
context "as a user" do
let!(:user){ FactoryGirl.create(:user) }
before(:each) do
sign_in user
end
context "entry does not exist" do
it "returns 404" do
delete :delete, id: 9000
expect(@response.code.to_i).to eql 404
end
it "returns not found" do
delete :delete, id: 9000
json = JSON.parse(@response.body)
expect(json).to have_key("errors")
expect(json.to_s).to include("not found")
end
end
context "entry exists" do
let!(:entry){ FactoryGirl.create(:time_entry) }
it "deletes the time entry" do
expect{delete :delete, id: entry.id}.to change{TimeEntry.count}
end
it "returns 204" do
delete :delete, id: entry.id
expect(@response.code.to_i).to eql 204
end
end
end
end
end
@@ -0,0 +1,72 @@
require 'spec_helper'
describe Api::V1::UsersController do
describe "#password_reset" do
context "as a user" do
before(:each) do
@user = FactoryGirl.create(:user)
sign_in @user
end
it "returns 403" do
post :password_reset
expect(@response.code.to_i).to eql 403
end
it "returns an error message" do
post :password_reset
json = JSON.parse(@response.body)
expect(json["errors"].first).to eql Api::V1::UsersController::CANNOT_MANAGE
end
end
context "as an admin" do
before(:each) do
@user = FactoryGirl.create(:admin)
sign_in @user
end
it "forbids a user to reset their own password" do
post :password_reset, user_id: @user.id
expect(@response.code.to_i).to eql 403
json = JSON.parse(@response.body)
expect(json["errors"].first).to eql Api::V1::UsersController::NOT_ALLOWED
end
context "with no user in json data" do
it "returns 404" do
post :password_reset
expect(@response.code.to_i).to eql 404
end
it "returns an error message" do
post :password_reset
json = JSON.parse(@response.body)
expect(json["errors"].first).to eql Api::V1::UsersController::NOT_FOUND
end
end
context "another user exists" do
before(:each) do
@user2 = FactoryGirl.create(:user)
end
it "returns 200" do
post :password_reset, user_id: @user2.id
expect(@response.code.to_i).to eql 200
end
it "returns that users new password" do
post :password_reset, user_id: @user2.id
json = JSON.parse(@response.body)
expect(json["password"].length).to eql Api::V1::UsersController::PASS_LENGTH
end
end
end
end
end
+5
View File
@@ -0,0 +1,5 @@
FactoryGirl.define do
factory :bike_brand do
brand {Faker::Commerce.product_name}
end
end
+5
View File
@@ -0,0 +1,5 @@
FactoryGirl.define do
factory :bike_condition do
condition "POOR"
end
end
+5
View File
@@ -0,0 +1,5 @@
FactoryGirl.define do
factory :bike_purpose do
purpose "SHOP"
end
end
+5
View File
@@ -0,0 +1,5 @@
FactoryGirl.define do
factory :bike_style do
style {Faker::Commerce.product_name}
end
end
+12
View File
@@ -0,0 +1,12 @@
FactoryGirl.define do
factory :bike_wheel_size do
twmm 40
rdmm 635
twin "1 1/2 [1 3/8]"
rdin 28
twfr "38B [35B]"
rdfr 700
description "a big wheel"
tire_common_score 1
end
end
+17
View File
@@ -2,6 +2,23 @@
FactoryGirl.define do
factory :bike do
sequence(:shop_id) {|n| n}
sequence :serial_number do |n|
"#{Faker::Code.isbn}-#{n}"
end
bike_brand { FactoryGirl.create(:bike_brand) }
model { Faker::Commerce.product_name }
color "FFFFFF"
bike_style { FactoryGirl.create(:bike_style) }
seat_tube_height 42
top_tube_length 42
bike_wheel_size { FactoryGirl.create(:bike_wheel_size) }
value 200
bike_condition { FactoryGirl.create(:bike_condition) }
bike_purpose { FactoryGirl.create(:bike_purpose) }
end
factory :seed_bike, class: Bike do
sequence(:shop_id) {|n| n}
sequence :serial_number do |n|
"#{Faker::Code.isbn}-#{n}"
+7
View File
@@ -0,0 +1,7 @@
FactoryGirl.define do
factory :task_list do
item_id { FactoryGirl.create(:bike).id }
item_type "Bike"
name { Faker::Lorem.words.join(" ")}
end
end
+7
View File
@@ -0,0 +1,7 @@
FactoryGirl.define do
factory :task do
task_list { FactoryGirl.create(:task_list) }
done false
task { Faker::Lorem.words(7).join(" ")}
end
end
+10
View File
@@ -0,0 +1,10 @@
FactoryGirl.define do
factory :time_entry do
loggable_type User.to_s
logger_type User.to_s
start_date Time.now
end_date Time.now
log_action_id 1
log_action_type ActsAsLoggable::UserAction.to_s
end
end
+4
View File
@@ -36,5 +36,9 @@ FactoryGirl.define do
end
end
factory :user_with_bike do
bike { FactoryGirl.create(:bike) }
end
end
end
+40
View File
@@ -0,0 +1,40 @@
require "spec_helper"
feature "Bikes" do
before(:each) do
# create brand and wheel size
@bike_brand = BikeBrand.create( brand: "Huffy")
@bike_wheel_size = BikeWheelSize.create( description: "MyWheelSize")
@user = FactoryGirl.create(:bike_admin)
visit new_user_session_path
fill_in "user_username", with: @user.username
fill_in "user_password", with: @user.password
click_button "Sign in"
end
scenario "User creates a new bike", js: true do
visit new_bike_path
fill_in "shop_id", with: 1
fill_in "model", with: "Huffle Puffle"
fill_in "serial_number", with: "XKCD"
find('label.btn.btn-default', text: 'RD').click
find('label.btn.btn-default', text: 'Poor').click
fill_in "seat_tube_height", with: 52
select @bike_brand.brand, from: "bike_brand_id"
select @bike_wheel_size.description, from: "bike_wheel_size_id"
click_button "Add Bike"
expect(page).to have_text(@bike_brand.brand)
end
scenario "User submits a bike with errors", js: true do
visit new_bike_path
click_button "Add Bike"
expect(page).to have_text(:all, "is not a number")
expect(page).to have_text(:all, "is not a valid brand")
expect(page).to have_text(:all, "is too short")
expect(page).to have_text(:all, "is not a valid style")
expect(page).to have_text(:all, "is not a valid wheel size")
expect(page).to have_text(:all, "is not a valid condition")
end
end
@@ -1,10 +1,11 @@
require 'spec_helper'
describe "New User Registrations" do
it 'should have a link to sign up on the homepage' do
visit root_path
page.should have_link 'Register'
click_link 'Register'
page.should have_link 'Sign up'
click_link 'Sign up'
current_path.should == new_user_registration_path
end
@@ -13,30 +14,29 @@ describe "New User Registrations" do
visit new_user_registration_path
end
it 'should have the additional user fields on the registration page' do
page.should have_field 'First name'
page.should have_field 'Last name'
page.should have_field 'Nickname'
page.should have_field 'Email'
page.should have_field 'Password'
page.should have_field 'Password confirmation'
page.should have_field 'user_first_name'
page.should have_field 'user_last_name'
page.should have_field 'user_email'
page.should have_field 'user_password'
page.should have_field 'user_password_confirmation'
page.should have_button 'Sign up'
end
context 'required non-devise fields' do
before do
fill_in 'Email', :with => 'FF@example.com'
fill_in 'Password', :with => 'password'
fill_in 'Password confirmation', :with => 'password'
fill_in 'user_email', :with => 'FF@example.com'
fill_in 'user_password', :with => 'password'
fill_in 'user_password_confirmation', :with => 'password'
end
it 'should require first name' do
fill_in 'Last name', :with => 'Footer'
fill_in 'user_last_name', :with => 'Footer'
click_button 'Sign up'
page.should have_content "First name can't be blank"
end
it 'should require last name' do
fill_in 'First name', :with => 'Frank'
fill_in 'user_first_name', :with => 'Frank'
click_button 'Sign up'
page.should have_content "Last name can't be blank"
end
+34
View File
@@ -0,0 +1,34 @@
require 'spec_helper'
describe "Index Page" do
before(:each) do
@user = FactoryGirl.create(:bike_admin)
visit root_path
fill_in "user_username", with: @user.username
fill_in "user_password", with: @user.password
end
it 'should have a link to check in' do
page.should have_button 'CHECK IN'
end
it 'should have a link to check out' do
page.should have_button 'CHECK OUT'
end
it 'clicking check in should check in a user', js: true do
pending
expect{click_button 'CHECK IN'}.
to change{@user.checked_in?}.
from(false).to(true)
end
it 'clicking check out should check out a user', js: true do
pending
@user.checkin
expect{click_button 'CHECK OUT'}.
to change{@user.checked_in?}.
from(true).to(false)
end
end
+22
View File
@@ -0,0 +1,22 @@
require "spec_helper"
feature "TimeEntries" do
let!(:user){ FactoryGirl.create(:user) }
let!(:entry){ FactoryGirl.create(:time_entry, loggable_id: user.id) }
before(:each) do
visit new_user_session_path
fill_in "user_username", with: user.username
fill_in "user_password", with: user.password
click_button "Sign in"
end
scenario "User deletes a time entry", js: true do
visit time_entries_path
save_screenshot("/tmp/testingpoop.png")
find('button.work_entry-delete-btn').trigger('click')
click_button "Delete"
expect(page).to have_text("Your Timesheet")
expect(TimeEntry.count).to eql 0
end
end
+48
View File
@@ -0,0 +1,48 @@
require "spec_helper"
feature "TimeEntries" do
let!(:user){ FactoryGirl.create(:user) }
before(:each) do
visit new_user_session_path
fill_in "user_username", with: user.username
fill_in "user_password", with: user.password
click_button "Sign in"
end
scenario "User creates a new time entry", js: true do
visit new_time_entry_path
fill_in "date_id", with: "04/27/2014"
fill_in "start_time_id", with: "3:00 PM"
fill_in "end_time_id", with: "3:15 PM"
find('label.btn.btn-default', text: 'Personal').trigger('click')
fill_in "description_id", with: "My Description"
click_button "Add Time Entry"
expect(page).to have_text("My Description")
expect(TimeEntry.count).to be > 0
end
context "A bike exists" do
let!(:bike){ FactoryGirl.create(:bike) }
scenario "User creates a new bike work time entry", js: true do
visit new_time_entry_path
fill_in "date_id", with: "04/27/2014"
fill_in "start_time_id", with: "3:00 PM"
fill_in "end_time_id", with: "3:15 PM"
find('label.btn.btn-default', text: 'Personal').trigger('click')
select bike.to_s, from: "bike_id"
fill_in "description_id", with: "My Description"
click_button "Add Time Entry"
expect(page).to have_text("Task List")
expect(TimeEntry.count).to be > 0
end
end
scenario "User submits a time entry with errors", js: true do
visit new_time_entry_path
click_button "Add Time Entry"
expect(page).to have_text(:all, "can't be blank")
end
end
+5 -3
View File
@@ -1,6 +1,7 @@
require 'rubygems'
require 'spork'
require 'rails'
require 'capybara/poltergeist'
#uncomment the following line to use spork with the debugger
#require 'spork/ext/ruby-debug'
@@ -19,12 +20,13 @@ Spork.prefork do
require 'rspec/rails'
require 'rspec/autorun'
require 'capybara/rspec'
require 'turnip/capybara'
# Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories.
Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
Capybara.javascript_driver = :poltergeist
RSpec.configure do |config|
# ## Mock Framework
#
@@ -66,19 +68,19 @@ Spork.prefork do
config.infer_base_class_for_anonymous_controllers = false
config.include FactoryGirlStepHelpers
config.include FactoryGirl::Syntax::Methods
config.include ApiTestHelpers
config.include Devise::TestHelpers, type: :controller
#allows :focus tag to only run specific tests
config.treat_symbols_as_metadata_keys_with_true_values = true
config.filter_run :focus => true
config.run_all_when_everything_filtered = true
end
end
Spork.each_run do
# This code will be run each time you run your specs.
#Reload Factories
#FYI make sure that Factories use strings instead of class constants
FactoryGirl.reload
+5
View File
@@ -0,0 +1,5 @@
module ApiTestHelpers
def api_submit_json(user, json_hash)
json_hash.merge({username: user.username, password: user.password})
end
end