mirror of
https://github.com/fspc/BikeShed-1.git
synced 2025-04-04 05:33:22 -04:00
Compare commits
No commits in common. "master" and "0.0.2-rc2" have entirely different histories.
4
.gitignore
vendored
4
.gitignore
vendored
@ -6,7 +6,7 @@ tmp/
|
||||
.powrc
|
||||
.rbenv-version
|
||||
.DS_Store
|
||||
public/extjs
|
||||
public/assets
|
||||
public/system
|
||||
public/images/icons
|
||||
config/database.yml
|
||||
.idea/
|
15
.powrc
15
.powrc
@ -1,15 +0,0 @@
|
||||
if [ -f "$rvm_path/scripts/rvm" ]; then
|
||||
source "$rvm_path/scripts/rvm"
|
||||
|
||||
if [ -f ".rvmrc" ]; then
|
||||
source ".rvmrc"
|
||||
fi
|
||||
|
||||
if [ -f ".ruby-version" ]; then
|
||||
rvm use `cat .ruby-version`
|
||||
fi
|
||||
|
||||
if [ -f ".ruby-gemset" ]; then
|
||||
rvm gemset use --create `cat .ruby-gemset`
|
||||
fi
|
||||
fi
|
@ -1 +1 @@
|
||||
ruby-2.1.1
|
||||
ruby-1.9.3-p374
|
||||
|
31
Dockerfile
31
Dockerfile
@ -1,31 +0,0 @@
|
||||
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 env NOKOGIRI_USE_SYSTEM_LIBRARIES=true bundle install
|
||||
|
||||
#COPY install_extjs.rb /usr/src/app/
|
||||
#RUN /usr/src/app/install_extjs.rb
|
||||
|
||||
COPY extjs-4.1.1.zip /usr/lib/
|
||||
|
||||
RUN unzip -d /usr/lib /usr/lib/extjs-4.1.1.zip; \
|
||||
mv /usr/lib/ext-4.1.1a /usr/lib/extjs
|
||||
|
||||
COPY install_famfamfam.rb /usr/src/app/
|
||||
RUN /usr/src/app/install_famfamfam.rb
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["rails", "server", "-b", "0.0.0.0"]
|
24
Gemfile
24
Gemfile
@ -1,7 +1,6 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'rails', '3.2.13'
|
||||
gem 'rake', '< 11.0'
|
||||
|
||||
gem 'netzke-cancan'
|
||||
gem 'netzke-core', '~>0.8.0'
|
||||
@ -13,39 +12,40 @@ gem 'bootstrap-will_paginate', '~> 0.0.6'
|
||||
gem 'cancan'
|
||||
gem 'decent_exposure', '~> 1.0.1'
|
||||
gem 'devise', '~> 2.0.4'
|
||||
gem 'haml-rails'
|
||||
gem 'haml-rails', '~> 0.3.4'
|
||||
gem 'jquery-rails', '~> 2.0'
|
||||
gem 'pg', '~> 0.17.1'
|
||||
gem 'will_paginate', '~> 3.0.3'
|
||||
gem 'jbuilder', '~> 2.0.3'
|
||||
gem 'paperclip', '~> 4.3'
|
||||
|
||||
# Assets
|
||||
gem 'sass-rails', '~> 3.0'
|
||||
gem 'coffee-rails', '~> 3.2.1'
|
||||
gem 'bootstrap-sass', '~> 3.1.1'
|
||||
# Gems used only for assets and not required
|
||||
# in production environments by default.
|
||||
group :assets do
|
||||
gem 'coffee-rails', '~> 3.2.1'
|
||||
gem "twitter-bootstrap-rails", "~> 2.0.3"
|
||||
|
||||
gem 'momentjs-rails', '>= 2.9.0'
|
||||
gem 'bootstrap3-datetimepicker-rails', '~> 4.17.43'
|
||||
# See https://github.com/sstephenson/execjs#readme for more supported runtimes
|
||||
# gem 'therubyracer'
|
||||
|
||||
gem 'uglifier', '>= 1.0.3'
|
||||
gem 'uglifier', '>= 1.0.3'
|
||||
end
|
||||
|
||||
group :development, :test do
|
||||
gem 'rspec-rails', '~> 2.14.0'
|
||||
gem 'factory_girl_rails', '~> 1.2'
|
||||
gem 'pry', '~> 0.9.8'
|
||||
gem 'faker', '~> 1.2.0'
|
||||
gem 'colorize'
|
||||
end
|
||||
|
||||
group :test do
|
||||
gem 'shoulda-matchers', '~> 1.0.0'
|
||||
gem 'capybara', '~> 2.2.1'
|
||||
gem 'poltergeist', '~> 1.10.0'
|
||||
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', '~> 1.5.1'
|
||||
gem 'guard-rspec', '~> 4.2.6'
|
||||
end
|
||||
|
159
Gemfile.lock
159
Gemfile.lock
@ -36,17 +36,11 @@ GEM
|
||||
activesupport (3.2.13)
|
||||
i18n (= 0.6.1)
|
||||
multi_json (~> 1.0)
|
||||
addressable (2.4.0)
|
||||
addressable (2.3.5)
|
||||
arel (3.0.3)
|
||||
bcrypt (3.1.10)
|
||||
bcrypt-ruby (3.1.5)
|
||||
bcrypt (>= 3.1.3)
|
||||
bootstrap-sass (3.1.1.1)
|
||||
sass (~> 3.2)
|
||||
bcrypt-ruby (3.1.2)
|
||||
bootstrap-will_paginate (0.0.10)
|
||||
will_paginate
|
||||
bootstrap3-datetimepicker-rails (4.17.43)
|
||||
momentjs-rails (>= 2.8.1)
|
||||
builder (3.0.4)
|
||||
cancan (1.6.10)
|
||||
capybara (2.2.1)
|
||||
@ -55,21 +49,20 @@ GEM
|
||||
rack (>= 1.0.0)
|
||||
rack-test (>= 0.5.4)
|
||||
xpath (~> 2.0)
|
||||
childprocess (0.5.9)
|
||||
celluloid (0.15.2)
|
||||
timers (~> 1.1.0)
|
||||
childprocess (0.4.0)
|
||||
ffi (~> 1.0, >= 1.0.11)
|
||||
climate_control (0.1.0)
|
||||
cliver (0.3.2)
|
||||
cocaine (0.5.8)
|
||||
climate_control (>= 0.0.3, < 1.0)
|
||||
coderay (1.1.1)
|
||||
coderay (1.1.0)
|
||||
coffee-rails (3.2.2)
|
||||
coffee-script (>= 2.2.0)
|
||||
railties (~> 3.2.0)
|
||||
coffee-script (2.4.1)
|
||||
coffee-script (2.2.0)
|
||||
coffee-script-source
|
||||
execjs
|
||||
coffee-script-source (1.10.0)
|
||||
colorize (0.8.1)
|
||||
coffee-script-source (1.7.0)
|
||||
commonjs (0.2.7)
|
||||
database_cleaner (1.2.0)
|
||||
decent_exposure (1.0.2)
|
||||
devise (2.0.6)
|
||||
@ -79,7 +72,7 @@ GEM
|
||||
warden (~> 1.1.1)
|
||||
diff-lcs (1.2.5)
|
||||
erubis (2.7.0)
|
||||
execjs (2.6.0)
|
||||
execjs (2.0.2)
|
||||
factory_girl (2.6.4)
|
||||
activesupport (>= 2.3.9)
|
||||
factory_girl_rails (1.7.0)
|
||||
@ -87,58 +80,57 @@ GEM
|
||||
railties (>= 3.0.0)
|
||||
faker (1.2.0)
|
||||
i18n (~> 0.5)
|
||||
ffi (1.9.10)
|
||||
formatador (0.2.5)
|
||||
guard (2.13.0)
|
||||
ffi (1.9.3)
|
||||
formatador (0.2.4)
|
||||
guard (2.4.0)
|
||||
formatador (>= 0.2.4)
|
||||
listen (>= 2.7, <= 4.0)
|
||||
listen (~> 2.1)
|
||||
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-rspec (4.2.6)
|
||||
guard (~> 2.1)
|
||||
rspec (>= 2.14, < 4.0)
|
||||
guard-spork (1.5.1)
|
||||
childprocess (>= 0.2.3)
|
||||
guard (>= 1.1)
|
||||
spork (>= 0.8.4)
|
||||
haml (4.0.7)
|
||||
tilt
|
||||
haml-rails (0.4)
|
||||
haml (3.1.8)
|
||||
haml-rails (0.3.5)
|
||||
actionpack (>= 3.1, < 4.1)
|
||||
activesupport (>= 3.1, < 4.1)
|
||||
haml (>= 3.1, < 4.1)
|
||||
haml (~> 3.1)
|
||||
railties (>= 3.1, < 4.1)
|
||||
hike (1.2.3)
|
||||
i18n (0.6.1)
|
||||
jbuilder (2.0.8)
|
||||
activesupport (>= 3.0.0, < 5)
|
||||
multi_json (~> 1.2)
|
||||
jbuilder (2.0.3)
|
||||
activesupport (>= 3.0.0)
|
||||
multi_json (>= 1.2.0)
|
||||
journey (1.0.4)
|
||||
jquery-rails (2.3.0)
|
||||
railties (>= 3.0, < 5.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (1.8.3)
|
||||
launchy (2.4.3)
|
||||
json (1.8.1)
|
||||
launchy (2.4.2)
|
||||
addressable (~> 2.3)
|
||||
listen (3.0.6)
|
||||
less (2.2.2)
|
||||
commonjs (~> 0.2.6)
|
||||
less-rails (2.2.6)
|
||||
actionpack (>= 3.1)
|
||||
less (~> 2.2.0)
|
||||
libv8 (3.3.10.4)
|
||||
listen (2.4.1)
|
||||
celluloid (>= 0.15.2)
|
||||
rb-fsevent (>= 0.9.3)
|
||||
rb-inotify (>= 0.9.7)
|
||||
lumberjack (1.0.10)
|
||||
rb-inotify (>= 0.9)
|
||||
lumberjack (1.0.4)
|
||||
mail (2.5.4)
|
||||
mime-types (~> 1.16)
|
||||
treetop (~> 1.4.8)
|
||||
method_source (0.8.2)
|
||||
mime-types (1.25.1)
|
||||
mimemagic (0.3.0)
|
||||
mini_portile2 (2.0.0)
|
||||
momentjs-rails (2.15.1)
|
||||
railties (>= 3.1)
|
||||
multi_json (1.12.1)
|
||||
nenv (0.3.0)
|
||||
mini_portile (0.5.2)
|
||||
multi_json (1.8.4)
|
||||
netzke-basepack (0.8.4)
|
||||
netzke-core (~> 0.8.2)
|
||||
netzke-cancan (0.8.2)
|
||||
@ -147,34 +139,26 @@ GEM
|
||||
netzke-core (0.8.4)
|
||||
execjs
|
||||
uglifier
|
||||
nokogiri (1.6.7.2)
|
||||
mini_portile2 (~> 2.0.0.rc2)
|
||||
notiffany (0.0.8)
|
||||
nenv (~> 0.1)
|
||||
shellany (~> 0.0)
|
||||
nokogiri (1.6.1)
|
||||
mini_portile (~> 0.5.0)
|
||||
orm_adapter (0.0.7)
|
||||
paperclip (4.3.7)
|
||||
activemodel (>= 3.2.0)
|
||||
activesupport (>= 3.2.0)
|
||||
cocaine (~> 0.5.5)
|
||||
mime-types
|
||||
mimemagic (= 0.3.0)
|
||||
pg (0.17.1)
|
||||
poltergeist (1.10.0)
|
||||
poltergeist (1.5.0)
|
||||
capybara (~> 2.1)
|
||||
cliver (~> 0.3.1)
|
||||
multi_json (~> 1.0)
|
||||
websocket-driver (>= 0.2.0)
|
||||
polyglot (0.3.5)
|
||||
polyglot (0.3.3)
|
||||
pry (0.9.12.6)
|
||||
coderay (~> 1.0)
|
||||
method_source (~> 0.8)
|
||||
slop (~> 3.4)
|
||||
rack (1.4.7)
|
||||
rack-cache (1.6.1)
|
||||
rack (1.4.5)
|
||||
rack-cache (1.2)
|
||||
rack (>= 0.4)
|
||||
rack-ssl (1.3.4)
|
||||
rack-ssl (1.3.3)
|
||||
rack
|
||||
rack-test (0.6.3)
|
||||
rack-test (0.6.2)
|
||||
rack (>= 1.0)
|
||||
rails (3.2.13)
|
||||
actionmailer (= 3.2.13)
|
||||
@ -191,9 +175,9 @@ GEM
|
||||
rake (>= 0.8.7)
|
||||
rdoc (~> 3.4)
|
||||
thor (>= 0.14.6, < 2.0)
|
||||
rake (10.5.0)
|
||||
rb-fsevent (0.9.7)
|
||||
rb-inotify (0.9.7)
|
||||
rake (10.1.1)
|
||||
rb-fsevent (0.9.4)
|
||||
rb-inotify (0.9.3)
|
||||
ffi (>= 0.5.0)
|
||||
rdoc (3.12.2)
|
||||
json (~> 1.4)
|
||||
@ -201,11 +185,11 @@ GEM
|
||||
rspec-core (~> 2.14.0)
|
||||
rspec-expectations (~> 2.14.0)
|
||||
rspec-mocks (~> 2.14.0)
|
||||
rspec-core (2.14.8)
|
||||
rspec-core (2.14.7)
|
||||
rspec-expectations (2.14.5)
|
||||
diff-lcs (>= 1.1.3, < 2.0)
|
||||
rspec-mocks (2.14.6)
|
||||
rspec-rails (2.14.2)
|
||||
rspec-mocks (2.14.5)
|
||||
rspec-rails (2.14.1)
|
||||
actionpack (>= 3.0)
|
||||
activemodel (>= 3.0)
|
||||
activesupport (>= 3.0)
|
||||
@ -213,35 +197,35 @@ GEM
|
||||
rspec-core (~> 2.14.0)
|
||||
rspec-expectations (~> 2.14.0)
|
||||
rspec-mocks (~> 2.14.0)
|
||||
sass (3.4.22)
|
||||
sass-rails (3.2.6)
|
||||
railties (~> 3.2.0)
|
||||
sass (>= 3.1.10)
|
||||
tilt (~> 1.3)
|
||||
shellany (0.0.1)
|
||||
shoulda-matchers (1.0.0)
|
||||
slop (3.6.0)
|
||||
slop (3.4.7)
|
||||
spork (0.9.2)
|
||||
sprockets (2.2.3)
|
||||
sprockets (2.2.2)
|
||||
hike (~> 1.2)
|
||||
multi_json (~> 1.0)
|
||||
rack (~> 1.0)
|
||||
tilt (~> 1.1, != 1.3.0)
|
||||
thor (0.19.4)
|
||||
therubyracer (0.10.2)
|
||||
libv8 (~> 3.3.10)
|
||||
thor (0.18.1)
|
||||
tilt (1.4.1)
|
||||
timers (1.1.0)
|
||||
treetop (1.4.15)
|
||||
polyglot
|
||||
polyglot (>= 0.3.1)
|
||||
tzinfo (0.3.46)
|
||||
uglifier (2.7.2)
|
||||
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.4.0)
|
||||
execjs (>= 0.3.0)
|
||||
json (>= 1.8.0)
|
||||
warden (1.1.1)
|
||||
rack (>= 1.0)
|
||||
websocket-driver (0.6.5)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.2)
|
||||
will_paginate (3.0.7)
|
||||
websocket-driver (0.3.2)
|
||||
will_paginate (3.0.5)
|
||||
xpath (2.0.0)
|
||||
nokogiri (~> 1.3)
|
||||
|
||||
@ -250,13 +234,10 @@ PLATFORMS
|
||||
|
||||
DEPENDENCIES
|
||||
acts_as_loggable!
|
||||
bootstrap-sass (~> 3.1.1)
|
||||
bootstrap-will_paginate (~> 0.0.6)
|
||||
bootstrap3-datetimepicker-rails (~> 4.17.43)
|
||||
cancan
|
||||
capybara (~> 2.2.1)
|
||||
coffee-rails (~> 3.2.1)
|
||||
colorize
|
||||
database_cleaner (~> 1.2.0)
|
||||
decent_exposure (~> 1.0.1)
|
||||
devise (~> 2.0.4)
|
||||
@ -264,23 +245,21 @@ DEPENDENCIES
|
||||
faker (~> 1.2.0)
|
||||
guard-rspec (~> 4.2.6)
|
||||
guard-spork (~> 1.5.1)
|
||||
haml-rails
|
||||
haml-rails (~> 0.3.4)
|
||||
jbuilder (~> 2.0.3)
|
||||
jquery-rails (~> 2.0)
|
||||
launchy (~> 2.4.2)
|
||||
momentjs-rails (>= 2.9.0)
|
||||
netzke-basepack (~> 0.8.0)
|
||||
netzke-cancan
|
||||
netzke-core (~> 0.8.0)
|
||||
paperclip (~> 4.3)
|
||||
pg (~> 0.17.1)
|
||||
poltergeist (~> 1.10.0)
|
||||
poltergeist (~> 1.5.0)
|
||||
pry (~> 0.9.8)
|
||||
rails (= 3.2.13)
|
||||
rake (< 11.0)
|
||||
rb-fsevent
|
||||
rspec-rails (~> 2.14.0)
|
||||
sass-rails (~> 3.0)
|
||||
shoulda-matchers (~> 1.0.0)
|
||||
spork (~> 0.9.2)
|
||||
twitter-bootstrap-rails (~> 2.0.3)
|
||||
uglifier (>= 1.0.3)
|
||||
will_paginate (~> 3.0.3)
|
||||
|
72
README.md
72
README.md
@ -1,48 +1,60 @@
|
||||
# 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 a live demo here: <http://bikeshed.wvcompletestreets.org/> Thanks @fspc for setting that up!
|
||||
See an overview video of what it looks like and how it works here: https://www.youtube.com/watch?v=0-JjM6d9nK4.
|
||||
|
||||
See an overview video of what the desktop view looks like and how it works here: https://www.youtube.com/watch?v=0-JjM6d9nK4.
|
||||
## Instructions/Guides
|
||||
|
||||
## Overview/Instructions/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.
|
||||
|
||||
See [guides](doc/guides.md)
|
||||
### Adding a Bike
|
||||
How to add a bike to the database: 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. You can see those changes on this [branch]( https://github.com/spacemunkay/BikeShed/tree/mobile-ui)
|
||||
|
||||
## 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.
|
||||
|
||||
# Developer Setup
|
||||
|
||||
## Running with Docker (recommended)
|
||||
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`
|
||||
|
||||
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:setup`
|
||||
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.
|
||||
|
||||
### Alternative Dockerfile
|
||||
# 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`
|
||||
|
||||
See <https://github.com/fspc/bikeshed> for an alternative docker setup.
|
||||
# Testing
|
||||
|
||||
### 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`.
|
||||
1. Install phantomjs `brew install phantomjs`
|
||||
1. Run tests with `rspec`
|
||||
|
||||
You'll likely want to add the following aliases:
|
||||
```
|
||||
alias dm='docker-machine'
|
||||
alias dc='docker-compose'
|
||||
alias dcrw='docker-compose run web'
|
||||
```
|
||||
# Optional
|
||||
Add 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`.
|
||||
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`
|
||||
|
||||
# License
|
||||
Velocipede is released under the MIT license (http://opensource.org/licenses/MIT)
|
||||
|
@ -12,9 +12,9 @@
|
||||
//
|
||||
//= require jquery
|
||||
//= require jquery_ujs
|
||||
//= require bootstrap
|
||||
//= require twitter/bootstrap/bootstrap-button
|
||||
//= require twitter/bootstrap/bootstrap-modal
|
||||
//= require utils
|
||||
//= require moment
|
||||
//= require bootstrap-datetimepicker
|
||||
//= require jquery.form
|
||||
|
||||
//= require bootstrap-datepicker
|
||||
//= require bootstrap-timepicker
|
||||
//= require jquery-date-format
|
||||
|
@ -1,11 +1,31 @@
|
||||
$('.btn').button();
|
||||
|
||||
$("#add_bike_form").ajaxForm({
|
||||
$("#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){
|
||||
success: function(data, status, xhr){
|
||||
window.location = data.bikes[0].id + "?add_bike=1";
|
||||
},
|
||||
error: function(data){
|
||||
error: function(data, status ){
|
||||
displayFormErrors(data.responseJSON);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
});
|
||||
|
474
app/assets/javascripts/bootstrap-datepicker.js
vendored
Executable file
474
app/assets/javascripts/bootstrap-datepicker.js
vendored
Executable 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">‹</th>'+
|
||||
'<th colspan="5" class="switch"></th>'+
|
||||
'<th class="next">›</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 );
|
1097
app/assets/javascripts/bootstrap-timepicker.js
vendored
Normal file
1097
app/assets/javascripts/bootstrap-timepicker.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@ -2,79 +2,50 @@ $(document).ready(function(){
|
||||
var MIN_LEN = 3;
|
||||
var MAX_SUBMITS = 3;
|
||||
var submit_count = 0;
|
||||
$("input[name=commit]").click( function(e){
|
||||
console.log("clicked");
|
||||
submit_count += 1;
|
||||
//IDs of contact info
|
||||
var contact_info_ids = [
|
||||
"user_email",
|
||||
"user_user_profiles_attributes_0_addrStreet1",
|
||||
"user_user_profiles_attributes_0_addrCity",
|
||||
"user_user_profiles_attributes_0_addrState",
|
||||
"user_user_profiles_attributes_0_addrZip",
|
||||
"user_user_profiles_attributes_0_phone"
|
||||
];
|
||||
var contact_vals = "";
|
||||
var index = 0;
|
||||
//see if any contact info exists
|
||||
for( var index in contact_info_ids){
|
||||
contact_vals += $("#"+contact_info_ids[index]).val();
|
||||
}
|
||||
if( contact_vals.length >= MIN_LEN || submit_count > MAX_SUBMITS){
|
||||
|
||||
var checkContacts = function() {
|
||||
submit_count += 1;
|
||||
//IDs of contact info
|
||||
var contact_info_ids = [
|
||||
"user_email",
|
||||
"user_user_profiles_attributes_0_addrStreet1",
|
||||
"user_user_profiles_attributes_0_addrCity",
|
||||
"user_user_profiles_attributes_0_addrState",
|
||||
"user_user_profiles_attributes_0_addrZip",
|
||||
"user_user_profiles_attributes_0_phone"
|
||||
];
|
||||
|
||||
var contact_vals = "";
|
||||
//see if any contact info exists
|
||||
for( var index in contact_info_ids){
|
||||
contact_vals += $("#"+contact_info_ids[index]).val();
|
||||
if( submit_count > MAX_SUBMITS ){
|
||||
alert("Fine.");
|
||||
}
|
||||
if( contact_vals.length >= MIN_LEN || submit_count > MAX_SUBMITS){
|
||||
return true;
|
||||
|
||||
if( submit_count == MAX_SUBMITS + 1 ){
|
||||
alert("Fine.");
|
||||
}
|
||||
return true;
|
||||
}else{
|
||||
|
||||
}else{
|
||||
|
||||
switch(submit_count){
|
||||
case 1:
|
||||
alert("It appears you have not entered any contact information. " +
|
||||
"Please do.");
|
||||
break;
|
||||
case 2:
|
||||
alert("It is highly recommended that you enter at least one form of" +
|
||||
" contact information. It is in your best interest.");
|
||||
break;
|
||||
case 3:
|
||||
alert("If something happens to your bicycle, we will not be able to" +
|
||||
" notify you. Please enter at least one form of contact.");
|
||||
break;
|
||||
default:
|
||||
alert("Please enter at least one form of contact.");
|
||||
}
|
||||
return false;
|
||||
switch(submit_count){
|
||||
case 1:
|
||||
alert("It appears you have not entered any contact information. " +
|
||||
"Please do.");
|
||||
break;
|
||||
case 2:
|
||||
alert("It is highly recommended that you enter at least one form of" +
|
||||
" contact information. It is in your best interest.");
|
||||
break;
|
||||
case 3:
|
||||
alert("If something happens to your bicycle, we will not be able to" +
|
||||
" notify you. Please enter at least one form of contact.");
|
||||
break;
|
||||
default:
|
||||
alert("Please enter at least one form of contact.");
|
||||
}
|
||||
};
|
||||
|
||||
var checkValid = function() {
|
||||
var errors = {};
|
||||
var hasErrors = false;
|
||||
["username", "first_name", "last_name"].forEach(function(requiredField) {
|
||||
if(!$("#user_" + requiredField).val().trim()) {
|
||||
errors["user_" + requiredField] = ["can't be blank"];
|
||||
hasErrors = true;
|
||||
}
|
||||
});
|
||||
|
||||
if($("#user_password").val().length < 6) {
|
||||
errors["user_password"] = ["is too short (minimum is 6 characters)"];
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
if($("#user_password").val() != $("#user_password_confirmation").val()) {
|
||||
errors["user_password_confirmation"] = ["confirmation doesn't match password"];
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
displayFormErrors({errors: errors}, "#new_user");
|
||||
|
||||
return !hasErrors;
|
||||
};
|
||||
|
||||
$("input[name=commit]").click(function(e){
|
||||
return checkContacts() && checkValid();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
445
app/assets/javascripts/jquery-date-format.js
vendored
Normal file
445
app/assets/javascripts/jquery-date-format.js
vendored
Normal 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 && response.statusText === "Unauthorized") { window.location = '/users/sign_in'; }
|
||||
if (response.status === 401) { window.location = '/users/sign_in'; }
|
||||
}, this);
|
||||
|
||||
//Override default netzke time entry field
|
||||
|
@ -4,7 +4,7 @@ $(".task_list_task").click(function(){
|
||||
|
||||
$("#update_tasks_submit").click(function(){
|
||||
|
||||
var tasks = [];
|
||||
tasks = [];
|
||||
$(".task_list_task").each(function(){
|
||||
tasks.push({
|
||||
id: parseInt($(this).data("id")),
|
||||
@ -12,7 +12,7 @@ $("#update_tasks_submit").click(function(){
|
||||
});
|
||||
});
|
||||
|
||||
var json_data = {tasks: tasks};
|
||||
json_data = { tasks: tasks };
|
||||
|
||||
$.ajax({
|
||||
url: $("#update_tasks_submit").data("url"),
|
||||
|
@ -1,83 +1,81 @@
|
||||
$(document).ready(function () {
|
||||
$(document).ready(function(){
|
||||
|
||||
var $date_input = $("#date_id");
|
||||
$date_input.datetimepicker({format: $date_input.data("format")});
|
||||
var currentdate = new Date();
|
||||
$("#date_id").datepicker().on('changeDate', function(ev){
|
||||
$("#date_id").datepicker('hide');
|
||||
});
|
||||
$("#date_id").datepicker('setValue', currentdate);
|
||||
|
||||
var $start_time_input = $("#start_time_id");
|
||||
var $end_time_input = $("#end_time_id");
|
||||
[$start_time_input, $end_time_input].forEach(function($x) {
|
||||
$x.datetimepicker({format: $x.data("format"), stepping: 15});
|
||||
})
|
||||
$("#start_time_id").timepicker();
|
||||
$("#end_time_id").timepicker();
|
||||
|
||||
$("#add_time_entry_submit").click(function () {
|
||||
var date = $date_input.val();
|
||||
var start_date = new Date(date + " " + $start_time_input.val());
|
||||
var end_date = new Date(date + " " + $end_time_input.val());
|
||||
$("#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());
|
||||
|
||||
var forward = $("#add_time_entry_submit").data("forward");
|
||||
forward = $("#add_time_entry_submit").data("forward");
|
||||
|
||||
// If a bike is selected, forward to the bike
|
||||
// checklist.
|
||||
var bike_id = parseInt($("#bike_id").val());
|
||||
if (bike_id > 0) {
|
||||
forward = "/task_lists/" + bike_id + "/edit";
|
||||
}
|
||||
// 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.
|
||||
var json_data = {
|
||||
time_entries: [{
|
||||
start_date: moment(start_date).format("DD-MM-YYYY h:mm A"),
|
||||
end_date: moment(end_date).format("DD-MM-YYYY h: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);
|
||||
}
|
||||
});
|
||||
//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 () {
|
||||
var row = $(this).closest("tr");
|
||||
var entry_id = row.data("id");
|
||||
var start_date = row.data("start_date");
|
||||
var duration = row.data("duration");
|
||||
var description = row.data("description");
|
||||
$("#work_entry_start_date").text(start_date);
|
||||
$("#work_entry_duration").text(duration);
|
||||
$("#work_entry_description").text(description);
|
||||
$("#confirmation_delete").data("entry_id", entry_id);
|
||||
});
|
||||
});
|
||||
|
||||
$("#confirmation_delete").click(function () {
|
||||
var entry_id = $(this).data("entry_id");
|
||||
var url = $("#confirmation_delete").data("url-template").replace(/__ID__/, entry_id);
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: "delete",
|
||||
contentType: 'application/json',
|
||||
success: function (data, status, xhr) {
|
||||
location.reload();
|
||||
},
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,10 +1,8 @@
|
||||
function displayFormErrors(data, form){
|
||||
if(form){
|
||||
$(form).find(".form-group.has-error").removeClass("has-error").find(".help-block").html("");
|
||||
}
|
||||
if(data.errors != undefined ){
|
||||
$.each(data.errors, function(field, errorMsg) {
|
||||
$("#"+field).closest(".form-group").addClass("has-error").find(".help-block").html(errorMsg.join(", "));
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
15
app/assets/stylesheets/application.css
Normal file
15
app/assets/stylesheets/application.css
Normal file
@ -0,0 +1,15 @@
|
||||
/*
|
||||
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
||||
* listed below.
|
||||
*
|
||||
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
||||
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
|
||||
*
|
||||
* You're free to add application-wide styles to this file and they'll appear at the top of the
|
||||
* compiled file, but it's generally better to create a new file per style scope.
|
||||
*
|
||||
*= require_self
|
||||
*= require datepicker
|
||||
*= require bootstrap-timepicker
|
||||
*= require bootstrap_and_overrides
|
||||
*/
|
@ -1,3 +0,0 @@
|
||||
@import "bootstrap";
|
||||
@import "bootstrap-datetimepicker";
|
||||
@import "frontend";
|
146
app/assets/stylesheets/bootstrap-timepicker.css
vendored
Normal file
146
app/assets/stylesheets/bootstrap-timepicker.css
vendored
Normal 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%;
|
||||
}
|
||||
}
|
47
app/assets/stylesheets/bootstrap_and_overrides.css.less
vendored
Normal file
47
app/assets/stylesheets/bootstrap_and_overrides.css.less
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
@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
|
||||
// you may use and inherit here
|
||||
//
|
||||
// If you'd like to override bootstrap's own variables, you can do so here as well
|
||||
// See http://twitter.github.com/bootstrap/less.html for their names and documentation
|
||||
//
|
||||
// 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
app/assets/stylesheets/datepicker.css
Executable file
182
app/assets/stylesheets/datepicker.css
Executable 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;
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
body {
|
||||
padding-top: $grid-gutter-width / 2;
|
||||
}
|
||||
|
||||
.x-boundlist-item {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
fieldset, .fieldset {
|
||||
margin-top: $line-height-computed;
|
||||
margin-bottom: $line-height-computed;
|
||||
}
|
||||
|
||||
.timepickers {
|
||||
.form-group {
|
||||
vertical-align: top;
|
||||
&.dash {
|
||||
line-height: 2.4em;
|
||||
}
|
||||
}
|
||||
|
||||
.bootstrap-datetimepicker-widget.dropdown-menu {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.bootstrap-datetimepicker-widget .timepicker-hour,
|
||||
.bootstrap-datetimepicker-widget .timepicker-minute,
|
||||
.bootstrap-datetimepicker-widget .timepicker-second {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.bootstrap-datetimepicker-widget table td {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
width: 40px;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.bootstrap-datetimepicker-widget table td.separator {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.bootstrap-datetimepicker-widget table td span {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bootstrap-datetimepicker-widget a[data-action] {
|
||||
padding: 0;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
.bike-color {
|
||||
display: inline-block;
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
.navbar-default {
|
||||
.navbar-text {
|
||||
width: 100%;
|
||||
font-size: 150%;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
class BikesController < AuthenticatedController
|
||||
|
||||
def new
|
||||
@bike = Bike.new bike_purpose_id: 1
|
||||
@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] }
|
||||
@ -10,7 +9,7 @@ class BikesController < AuthenticatedController
|
||||
|
||||
def show
|
||||
@bike = Bike.find_by_id(params[:id])
|
||||
@task_list = @bike.task_list
|
||||
@task_list_id = @bike.task_list.id
|
||||
@show_add_bike = true if params[:add_bike]
|
||||
end
|
||||
|
||||
|
34
app/controllers/users_controller.rb
Normal file
34
app/controllers/users_controller.rb
Normal file
@ -0,0 +1,34 @@
|
||||
class UsersController < AuthenticatedController
|
||||
expose(:user)
|
||||
expose(:users) { User.order('id').paginate(:page => params[:page]) }
|
||||
|
||||
def index
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def new
|
||||
end
|
||||
|
||||
def create
|
||||
if user.save
|
||||
redirect_to user
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if user.save
|
||||
redirect_to user
|
||||
else
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
user.destroy
|
||||
redirect_to bikes_url
|
||||
end
|
||||
end
|
@ -7,28 +7,4 @@ module ApplicationHelper
|
||||
end
|
||||
link_to(name, '#', id: "add_#{association.to_s.singularize}" , class: "add_fields", data: {id: id, fields: fields.gsub("\n", "")})
|
||||
end
|
||||
|
||||
def top_menu(right = nil)
|
||||
content_tag :nav, class: 'navbar navbar-default' do
|
||||
content_tag :div, class: 'container-fluid' do
|
||||
content_tag :div, class: 'row' do
|
||||
[
|
||||
content_tag(:div, link_to_dashboard, class: 'col-xs-3'),
|
||||
content_tag(:div, header_logo, class: 'col-xs-6'),
|
||||
content_tag(:div, class: 'col-xs-3') { content_tag :div, right, class: 'pull-right' },
|
||||
].join.html_safe
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def link_to_dashboard
|
||||
link_to root_path, class: 'navbar-brand', title: 'Back to dashboard' do
|
||||
content_tag :i, '', class: "glyphicon glyphicon-home"
|
||||
end
|
||||
end
|
||||
|
||||
def header_logo
|
||||
content_tag :div, 'Velocipede', class: 'navbar-text text-center'
|
||||
end
|
||||
end
|
||||
|
@ -1,18 +1,21 @@
|
||||
module DeviseHelper
|
||||
# A simple way to show error messages for the current devise resource. If you need
|
||||
# to customize this method, you can either overwrite it in your application helpers or
|
||||
# copy the views to your application.
|
||||
#
|
||||
# This method is intended to stay simple and it is unlikely that we are going to change
|
||||
# it to add more behavior or options.
|
||||
def devise_error_messages!
|
||||
return '' if resource.errors.empty?
|
||||
return "" if resource.errors.empty?
|
||||
|
||||
messages = resource.errors.full_messages.map { |msg| content_tag(:li, msg) }
|
||||
messages = resource.errors.full_messages.map { |msg| content_tag(:p, msg, :class => "alert") }.join
|
||||
sentence = I18n.t("errors.messages.not_saved",
|
||||
:count => resource.errors.count,
|
||||
:resource => resource.class.model_name.human.downcase)
|
||||
|
||||
html = <<-HTML
|
||||
<div class="alert alert-danger">#{sentence}
|
||||
<ul>
|
||||
#{messages.join}
|
||||
</ul>
|
||||
</div>
|
||||
<p>#{sentence}</p>
|
||||
#{messages}
|
||||
HTML
|
||||
|
||||
html.html_safe
|
||||
|
@ -23,7 +23,7 @@ class Ability
|
||||
|
||||
def user
|
||||
can :read, :all
|
||||
can :manage, @current_user.bike unless @current_user.bike.nil?
|
||||
can :manage, Bike, :id => @current_user.bike_id 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
|
||||
|
@ -1,21 +1,18 @@
|
||||
class Bike < ActiveRecord::Base
|
||||
acts_as_loggable
|
||||
attr_accessible :shop_id, :serial_number, :bike_brand_id, :bike_model_id, :model, :color, :bike_style_id,
|
||||
:seat_tube_height, :top_tube_length, :bike_wheel_size_id, :value, :bike_condition_id, :bike_purpose_id, :photo
|
||||
attr_accessible :shop_id, :serial_number, :bike_brand_id, :model, :color, :bike_style_id, :seat_tube_height,
|
||||
:top_tube_length, :bike_wheel_size_id, :value, :bike_condition_id, :bike_purpose_id
|
||||
|
||||
has_many :transactions
|
||||
|
||||
has_one :owner, :class_name => 'User'
|
||||
has_one :task_list, :as => :item, :dependent => :destroy
|
||||
belongs_to :bike_brand
|
||||
belongs_to :bike_model
|
||||
belongs_to :bike_style
|
||||
belongs_to :bike_condition
|
||||
belongs_to :bike_purpose
|
||||
belongs_to :bike_wheel_size
|
||||
|
||||
has_attached_file :photo, :styles => {:thumb => '100x100>'}
|
||||
|
||||
validates :shop_id, :presence => true, :uniqueness => true, :numericality => { :only_integer => true }
|
||||
validates :serial_number, :length => { :minimum => 3 }
|
||||
validates :model, :length => { :maximum => 50 }
|
||||
@ -27,9 +24,6 @@ class Bike < ActiveRecord::Base
|
||||
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" }
|
||||
|
||||
validates_attachment :photo, :content_type => {:content_type => %w{ image/jpeg image/gif image/png }},
|
||||
:file_name => {:matches => [/png\Z/, /jpe?g\Z/, /gif\Z/]}
|
||||
|
||||
self.per_page = 15
|
||||
|
||||
after_create :create_task_list
|
||||
|
@ -1,160 +0,0 @@
|
||||
require 'csv'
|
||||
|
||||
# Imports data from CSV file into the bikes database.
|
||||
class BikeCsvImporter
|
||||
|
||||
include BikeCsvImporter::Cache
|
||||
include BikeCsvImporter::Cleaner
|
||||
include BikeCsvImporter::BikeAttrs
|
||||
include BikeCsvImporter::Logs
|
||||
|
||||
attr_reader :file
|
||||
|
||||
# Default constructor
|
||||
#
|
||||
# @param [String] file Path to the CSV file
|
||||
def initialize(file)
|
||||
@file = file
|
||||
end
|
||||
|
||||
# Runs the import. Will print out progress to stdout
|
||||
#
|
||||
# @param [Boolean] dry_run If true, does not save data, only shows the progress of validation
|
||||
def run(dry_run)
|
||||
imported_count, skipped_count = 0, 0
|
||||
|
||||
puts "Performing a #{dry_run ? 'DRY RUN' : 'LIVE RUN'} of import"
|
||||
|
||||
fetch do |bike_hash|
|
||||
bike = new_bike bike_hash
|
||||
check_method = dry_run ? :valid? : :save
|
||||
|
||||
if bike.try check_method
|
||||
puts "Imported #{bike.shop_id}: #{bike}".green
|
||||
|
||||
logs = new_logs_entries bike, bike_hash
|
||||
logs.each do |log|
|
||||
if log.send check_method
|
||||
puts "\tLog entry created: #{log.inspect}".green
|
||||
else
|
||||
puts "\tLog entry creation failed: #{log.errors.full_messages.join '; '}".red
|
||||
end
|
||||
end
|
||||
|
||||
imported_count += 1
|
||||
else
|
||||
puts "Skipped #{bike.try(:shop_id) || bike_hash.values.first}: #{bike.try(:errors).try(:full_messages).try :join, '; '}".red
|
||||
skipped_count += 1
|
||||
end
|
||||
end
|
||||
|
||||
puts "#{imported_count} bikes imported, #{skipped_count} bikes skipped, total of #{imported_count + skipped_count} rows in the CSV"
|
||||
end
|
||||
|
||||
# Analyzes and prints out the input CSV file values
|
||||
#
|
||||
# @param [Array<Strong>] fields If passed, analyze only the given fields (names are down cased)
|
||||
def analyze(fields = [])
|
||||
puts "Analyzing CSV values frequency for #{fields.any? ? fields.join(', ') + ' field' : 'all fields'}"
|
||||
|
||||
fields = fields.map &:downcase
|
||||
grouped = {}
|
||||
fetch do |bike_hash|
|
||||
bike_hash.each do |key, value|
|
||||
next if fields.any? && !fields.include?(key)
|
||||
grouped[key] ||= {}
|
||||
grouped[key][value] ||= 0
|
||||
grouped[key][value] += 1
|
||||
end
|
||||
end
|
||||
|
||||
grouped.each do |field, values|
|
||||
puts "#{field}:"
|
||||
values.each do |value, count|
|
||||
puts "\t#{value.inspect}: #{count}"
|
||||
end
|
||||
puts "\tTotal of #{values.count} distinct values"
|
||||
end
|
||||
end
|
||||
|
||||
# Imports new brands from CSV file (field 'make'). Will print out progress to stdout
|
||||
#
|
||||
# @param [Boolean] dry_run If true, does not save data, only shows the progress of validation
|
||||
def brands(dry_run)
|
||||
created_count, skipped_count = 0, 0
|
||||
|
||||
puts "Performing a #{dry_run ? 'DRY RUN' : 'LIVE RUN'} of brands import"
|
||||
|
||||
fetch do |bike_hash|
|
||||
make = clean_value bike_hash['make']
|
||||
brand = bike_attr_bike_brand make, true
|
||||
check_method = dry_run ? :valid? : :save
|
||||
|
||||
if brand.try :persisted?
|
||||
puts "Skipped already existing brand #{brand.brand}"
|
||||
skipped_count +=1
|
||||
elsif brand.try check_method
|
||||
puts "Created brand #{brand.brand}".green
|
||||
created_count += 1
|
||||
else
|
||||
puts "Skipped #{brand.try(:brand) || make}: #{brand.try(:errors).try(:full_messages).try(:join, '; ') || 'object not created'}".red
|
||||
skipped_count += 1
|
||||
end
|
||||
end
|
||||
|
||||
puts "#{created_count} brand created, #{skipped_count} brand skipped, total of #{created_count + skipped_count} rows in the CSV"
|
||||
end
|
||||
|
||||
|
||||
|
||||
private
|
||||
|
||||
# Parses the CSV header & rows, yielding a block for each row (except the header)
|
||||
# Header is down cased!
|
||||
#
|
||||
# @param [Proc] &block The block to yield to
|
||||
def fetch
|
||||
CSV.foreach(file).each_with_index do |row, i|
|
||||
if i.zero?
|
||||
parse_header row
|
||||
else
|
||||
yield parse_bike(row)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Parses & stores the input header, down casing by the way
|
||||
#
|
||||
# @param [Array<String>] row
|
||||
def parse_header(row)
|
||||
@header = row.map(&:downcase)
|
||||
end
|
||||
|
||||
# Parses the input row into a hash with keys from the header, @see #parse_header
|
||||
#
|
||||
# @param [Array<String>] row
|
||||
#
|
||||
# @return [Hash]
|
||||
def parse_bike(row)
|
||||
@header.zip(row).to_h
|
||||
end
|
||||
|
||||
# Constructs a new Bike instance from the given hash from a CSV row
|
||||
#
|
||||
# @param [Hash] bike_hash
|
||||
#
|
||||
# @return [Bike]
|
||||
def new_bike(bike_hash)
|
||||
Bike.new bike_attrs(bike_hash)
|
||||
end
|
||||
|
||||
# Constructs new Bike Log Entries instances from the given hash from a CSV row
|
||||
#
|
||||
# @param [Bike] bike The Bike instance to construct log entries for
|
||||
# @param [Hash] bike_hash The input hash from a CSV row
|
||||
#
|
||||
# @return [Array<ActsAsLoggable::Log>]
|
||||
def new_logs_entries(bike, bike_hash)
|
||||
%i{ acquired comment gone }.map { |x| send :"log_entry_#{x}", bike, bike_hash }.compact
|
||||
end
|
||||
end
|
@ -1,93 +0,0 @@
|
||||
# Helper module to create various Bike instanct fields from a CSV row hash
|
||||
class BikeCsvImporter
|
||||
module BikeAttrs
|
||||
def bike_attr_fields
|
||||
{
|
||||
shop_id: 'velocipede number',
|
||||
bike_purpose_id: 'program',
|
||||
#gone: 'gone',
|
||||
value: 'price',
|
||||
bike_brand_id: 'make',
|
||||
bike_model_id: 'model',
|
||||
model: 'model',
|
||||
bike_style_id: nil,
|
||||
bike_condition_id: nil,
|
||||
seat_tube_height: nil,
|
||||
bike_wheel_size_id: nil,
|
||||
serial_number: nil,
|
||||
}
|
||||
end
|
||||
|
||||
def bike_attrs(bike_hash)
|
||||
bike_attr_fields.each_with_object({}) do |(model_field, csv_field), memo|
|
||||
memo[model_field] = send :"bike_attr_#{model_field}", clean_value(bike_hash[csv_field])
|
||||
end
|
||||
end
|
||||
|
||||
def bike_attr_shop_id(value)
|
||||
value.to_i
|
||||
end
|
||||
|
||||
def bike_attr_bike_purpose_id(value)
|
||||
map = {
|
||||
'SALE' => /shop|as(-|\s+)is|safety\s*check/,
|
||||
'BUILDBIKE' => /build|bikes.*world/,
|
||||
'STORAGE' => nil,
|
||||
'PARTS' => /part|frame/,
|
||||
'SCRAP' => /scrap|strip/,
|
||||
}
|
||||
|
||||
default = 'UNDETERMINED'
|
||||
test_value = value.try :downcase
|
||||
value = map.find { |_, regexp| regexp.try :match, test_value }.try :first
|
||||
|
||||
cached_bike_purpose(value || default).id
|
||||
end
|
||||
|
||||
def bike_attr_gone(value)
|
||||
%w{ yes yeah y }.include? value.try :downcase
|
||||
end
|
||||
|
||||
def bike_attr_value(value)
|
||||
value.try(:gsub, /[$]/, '').try :to_i
|
||||
end
|
||||
|
||||
def bike_attr_bike_brand(value, new_if_empty = false)
|
||||
value = 'Unknown' if !value || value =~ /\Aunknown/i
|
||||
cached_bike_brand value, new_if_empty
|
||||
end
|
||||
|
||||
def bike_attr_bike_brand_id(value)
|
||||
bike_attr_bike_brand(value, false).try :id
|
||||
end
|
||||
|
||||
def bike_attr_bike_model_id(value)
|
||||
return unless value
|
||||
cached_bike_model(value).try :id
|
||||
end
|
||||
|
||||
def bike_attr_model(value)
|
||||
value if value && value !~ /unknown/i
|
||||
end
|
||||
|
||||
def bike_attr_bike_style_id(_)
|
||||
@bike_style_other_cache ||= BikeStyle.find_by_style('OTHER').id
|
||||
end
|
||||
|
||||
def bike_attr_bike_condition_id(_)
|
||||
@bike_condition_undertermined_cache ||= BikeCondition.find_by_condition('UNDETERMINED').id
|
||||
end
|
||||
|
||||
def bike_attr_seat_tube_height(_)
|
||||
0
|
||||
end
|
||||
|
||||
def bike_attr_bike_wheel_size_id(_)
|
||||
@bike_condition_wheel_size_undertermined_cache ||= BikeWheelSize.find_by_description('UNDETERMINED').id
|
||||
end
|
||||
|
||||
def bike_attr_serial_number(_)
|
||||
'UNDETERMINED'
|
||||
end
|
||||
end
|
||||
end
|
@ -1,35 +0,0 @@
|
||||
# Helper module to create various cached instances for bike CSV imports
|
||||
class BikeCsvImporter
|
||||
module Cache
|
||||
def cached_bike_purpose(purpose)
|
||||
@bike_purpose_cache ||= {}
|
||||
@bike_purpose_cache[purpose] ||= BikePurpose.find_by_purpose purpose
|
||||
end
|
||||
|
||||
def cached_bike_brand(brand, new_if_empty = false)
|
||||
@bike_brand_cache ||= {}
|
||||
if @bike_brand_cache.has_key? brand
|
||||
@bike_brand_cache[brand]
|
||||
else
|
||||
bike_brand = BikeBrand.where('lower(brand) = ?', brand.downcase).first
|
||||
bike_brand ||= BikeBrand.new(brand: brand) if new_if_empty
|
||||
|
||||
@bike_brand_cache[brand] = bike_brand
|
||||
end
|
||||
end
|
||||
|
||||
def cached_bike_model(model)
|
||||
@bike_model_cache ||= {}
|
||||
if @bike_model_cache.has_key? model
|
||||
@bike_model_cache[model]
|
||||
else
|
||||
@bike_model_cache[model] = BikeModel.where('lower(model) = ?', model.downcase).first
|
||||
end
|
||||
end
|
||||
|
||||
def cached_log_bike_action(action)
|
||||
@log_bike_action_id_cache ||= {}
|
||||
@log_bike_action_id_cache[action] ||= ActsAsLoggable::BikeAction.find_by_action(action)
|
||||
end
|
||||
end
|
||||
end
|
@ -1,16 +0,0 @@
|
||||
# Helper module to clean the incoming data from CSV fields
|
||||
class BikeCsvImporter
|
||||
module Cleaner
|
||||
def clean_value(value)
|
||||
value_or_nil strip_value(value)
|
||||
end
|
||||
|
||||
def strip_value(value)
|
||||
value.try(:strip).try(:gsub, /\n|\r/, '')
|
||||
end
|
||||
|
||||
def value_or_nil(value)
|
||||
return value unless ['?', 'n/a', 'missing', 'unknown', ''].include? value.try(:downcase)
|
||||
end
|
||||
end
|
||||
end
|
@ -1,42 +0,0 @@
|
||||
# Helper module to create ActsAsLoggable log entries for a Bike instance from a CSV row hash
|
||||
class BikeCsvImporter
|
||||
module Logs
|
||||
def log_entry_gone(bike, bike_hash)
|
||||
if clean_value(bike_hash['gone']).to_s =~ /y/i
|
||||
log_entry bike, log_entry_date(clean_value(bike_hash['date out'])), 'COMPLETED', 'Gone'
|
||||
end
|
||||
end
|
||||
|
||||
def log_entry_acquired(bike, bike_hash)
|
||||
if clean_value(bike_hash['date in'])
|
||||
log_entry bike, log_entry_date(clean_value(bike_hash['date in'])), 'ACQUIRED'
|
||||
end
|
||||
end
|
||||
|
||||
def log_entry_comment(bike, bike_hash)
|
||||
if clean_value(bike_hash['comment']).present?
|
||||
log_entry bike, nil, 'NOTE', clean_value(bike_hash['comment'])
|
||||
end
|
||||
end
|
||||
|
||||
def log_entry_date(value)
|
||||
return unless value
|
||||
Date.strptime value, '%m/%d/%y' rescue nil
|
||||
end
|
||||
|
||||
def log_entry(bike, date, type, description = nil)
|
||||
date ||= DateTime.now
|
||||
bike_action = cached_log_bike_action(type)
|
||||
|
||||
ActsAsLoggable::Log.new(
|
||||
loggable_type: bike.class.to_s,
|
||||
loggable_id: bike.id || bike.shop_id.to_i, # for dry run
|
||||
log_action_type: bike_action.class.to_s,
|
||||
log_action_id: bike_action.id,
|
||||
start_date: date,
|
||||
end_date: date,
|
||||
description: description,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
@ -1,6 +1,4 @@
|
||||
class User < ActiveRecord::Base
|
||||
MAX_AVATAR_SIZE_KB = 1024
|
||||
|
||||
acts_as_loggable
|
||||
# Include default devise modules. Others available are:
|
||||
# :token_authenticatable, :encryptable, :confirmable, :lockable, :timeoutable and :omniauthable
|
||||
@ -10,7 +8,7 @@ class User < ActiveRecord::Base
|
||||
# Setup accessible (or protected) attributes for your model
|
||||
attr_accessible :email, :password, :password_confirmation, :remember_me,
|
||||
:first_name, :last_name, :bike_id,
|
||||
:user_profiles_attributes, :username, :avatar
|
||||
:user_profiles_attributes, :username
|
||||
|
||||
has_many :transactions, as: :customer
|
||||
has_many :transaction_logs, through: :transactions, source: :logs
|
||||
@ -22,19 +20,11 @@ class User < ActiveRecord::Base
|
||||
|
||||
belongs_to :bike
|
||||
|
||||
has_attached_file :avatar, :styles => {:thumb => '100x100>'}
|
||||
|
||||
default_scope order('username ASC')
|
||||
|
||||
validates :username, :presence => true, uniqueness: true
|
||||
validates :first_name, :presence => true
|
||||
validates :last_name, :presence => true
|
||||
|
||||
validates_attachment :avatar, :content_type => {:content_type => %w{ image/jpeg image/gif image/png }},
|
||||
:file_name => {:matches => [/png\Z/, /jpe?g\Z/, /gif\Z/]},
|
||||
:size => {:in => 0..MAX_AVATAR_SIZE_KB.kilobytes}
|
||||
|
||||
|
||||
def to_s
|
||||
"#{first_name} #{last_name}"
|
||||
end
|
||||
@ -54,11 +44,6 @@ class User < ActiveRecord::Base
|
||||
roles.include?(role)
|
||||
end
|
||||
|
||||
# try keeping the email field in DB clear and consistent, without empty strings (NULLs instead)
|
||||
def email=(other)
|
||||
super(other.blank? ? nil : other)
|
||||
end
|
||||
|
||||
### TODO methods below probably belong somewhere else
|
||||
|
||||
def completed_build_bikes
|
||||
|
@ -1,80 +1,62 @@
|
||||
= top_menu
|
||||
%a{ class: "btn btn-default btn-lg", href: root_path}
|
||||
%span{ class:"icon-home"}
|
||||
%h2 Add Bike
|
||||
|
||||
%h1 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}"}
|
||||
|
||||
.row
|
||||
.col-xs-12.col-sm-6.col-lg-4
|
||||
= form_for @bike, as: 'bikes', index: '', url: api_create_bike_path, method: :post, enctype: 'multipart/form-data', html: {id: 'add_bike_form'} do |f|
|
||||
= f.hidden_field 'bike_purpose_id'
|
||||
%fieldset
|
||||
.form-group
|
||||
= f.number_field 'shop_id', placeholder: 'Shop ID', min: 0, class: 'form-control', id: 'shop_id'
|
||||
.help-block
|
||||
|
||||
.form-group
|
||||
= f.select 'bike_brand_id', options_for_select(@brands), {}, class: 'form-control', id: 'bike_brand_id'
|
||||
.help-block
|
||||
|
||||
.form-group
|
||||
= f.text_field 'model', placeholder: 'Model', class: 'form-control', id: 'model'
|
||||
.help-block
|
||||
|
||||
.form-group
|
||||
= f.text_field 'serial_number', placeholder: 'Serial Number', class: 'form-control', id: 'serial_number'
|
||||
.help-block
|
||||
|
||||
.form-group
|
||||
.btn-group(data-toggle="buttons")
|
||||
%label.btn.btn-default
|
||||
= f.radio_button 'bike_style_id', 3
|
||||
RD
|
||||
%label.btn.btn-default
|
||||
= f.radio_button 'bike_style_id', 1
|
||||
MTN
|
||||
%label.btn.btn-default
|
||||
= f.radio_button 'bike_style_id', 2
|
||||
HYB
|
||||
%label.btn.btn-default
|
||||
= f.radio_button 'bike_style_id', 4
|
||||
OTHER
|
||||
= hidden_field_tag nil, nil, id: 'bike_style_id'
|
||||
.help-block
|
||||
|
||||
.form-group
|
||||
= f.select 'bike_wheel_size_id', options_for_select(@wheel_sizes), {}, class: 'form-control', id: 'bike_wheel_size_id'
|
||||
.help-block
|
||||
|
||||
.form-group
|
||||
.btn-group(data-toggle="buttons")
|
||||
%label.btn.btn-default
|
||||
= f.radio_button 'bike_condition_id', 2
|
||||
Poor
|
||||
%label.btn.btn-default
|
||||
= f.radio_button 'bike_condition_id', 3
|
||||
Fair
|
||||
%label.btn.btn-default
|
||||
= f.radio_button 'bike_condition_id', 4
|
||||
Good
|
||||
%label.btn.btn-default
|
||||
= f.radio_button 'bike_condition_id', 5
|
||||
Excellent
|
||||
= hidden_field_tag nil, nil, id: 'bike_condition_id'
|
||||
.help-block
|
||||
|
||||
.form-group
|
||||
.input-group
|
||||
= f.number_field 'seat_tube_height', placeholder: 'Seat Tube', min: 0, max: 100, class: 'form-control', id: 'seat_tube_height'
|
||||
.input-group-addon cm
|
||||
.help-block
|
||||
|
||||
.form-group
|
||||
%label Bike photo (optional)
|
||||
= f.file_field 'photo', id: 'photo'
|
||||
.help-block
|
||||
|
||||
-# Commenting this out until description is added to Bike
|
||||
/.form-group
|
||||
%input{id: "bike_description", placeholder: "Short description", type: "text", class: "input-lg" }
|
||||
|
||||
.form-group
|
||||
= button_tag 'Add Bike', class: 'btn btn-primary'
|
@ -1,57 +1,27 @@
|
||||
- btn = if @show_add_bike
|
||||
- capture do
|
||||
= link_to 'Add Another Bike?', new_bike_path, class: 'btn btn-default navbar-btn hidden-xs'
|
||||
= link_to new_bike_path, class: 'navbar-brand visible-xs', title: 'Add Another Bike?' do
|
||||
%i.glyphicon.glyphicon-plus
|
||||
- if @show_add_bike
|
||||
%p
|
||||
%a{class: "btn btn-lg btn-block btn-info", href: new_bike_path} Add Another Bike?
|
||||
|
||||
= top_menu btn
|
||||
%a{ class: "btn btn-default btn-lg", href: root_path}
|
||||
%span{ class:"icon-home"}
|
||||
%h2 #{@bike.shop_id}: #{@bike.bike_brand}
|
||||
%h2 #{@bike.model}
|
||||
|
||||
%h1 #{@bike.shop_id}: #{@bike.bike_brand}
|
||||
%h4= @bike.model
|
||||
|
||||
.form-horizontal
|
||||
- if @bike.photo?
|
||||
.form-group
|
||||
%label.col-sm-2.control-label Photo
|
||||
.col-sm-10
|
||||
= link_to @bike.photo.url, target: '_blank' do
|
||||
%img{src: @bike.photo.url(:thumb), class: 'img-thumbnail'}
|
||||
|
||||
.form-group
|
||||
%label.col-sm-2.control-label Type
|
||||
.col-sm-10.form-control-static= @bike.bike_style
|
||||
|
||||
.form-group
|
||||
%label.col-sm-2.control-label Wheel Size
|
||||
.col-sm-10.form-control-static= @bike.bike_wheel_size.display_string
|
||||
|
||||
.form-group
|
||||
%label.col-sm-2.control-label Condition
|
||||
.col-sm-10.form-control-static= @bike.bike_condition
|
||||
|
||||
.form-group
|
||||
%label.col-sm-2.control-label Seat Tube (cm)
|
||||
.col-sm-10.form-control-static= @bike.seat_tube_height
|
||||
|
||||
.form-group
|
||||
%label.col-sm-2.control-label Purpose
|
||||
.col-sm-10.form-control-static= @bike.bike_purpose
|
||||
|
||||
- unless @bike.value.nil?
|
||||
.form-group
|
||||
%label.col-sm-2.control-label Value
|
||||
.col-sm-10.form-control-static= @bike.value
|
||||
|
||||
- unless @bike.color.nil?
|
||||
.form-group
|
||||
%label.col-sm-2.control-label Color
|
||||
.col-sm-10.form-control-static
|
||||
.bike-color(style="background-color: ##{@bike.color}")= @bike.color
|
||||
|
||||
- if @task_list
|
||||
- tasks = @task_list.tasks.to_a
|
||||
.form-group
|
||||
%label.col-sm-2.control-label Task list
|
||||
.col-sm-10.form-control-static
|
||||
= link_to edit_task_list_path(@task_list.id) do
|
||||
#{tasks.select(&:done).count}/#{tasks.count}
|
||||
%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
|
||||
|
32
app/views/devise/_links.erb
Normal file
32
app/views/devise/_links.erb
Normal file
@ -0,0 +1,32 @@
|
||||
<%- if controller_name != 'sessions' %>
|
||||
<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' %>
|
||||
<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' %>
|
||||
<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' %>
|
||||
<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' %>
|
||||
<% end -%>
|
||||
|
||||
<%- if devise_mapping.omniauthable? %>
|
||||
<%- resource_class.omniauth_providers.each do |provider| %>
|
||||
<%= link_to "Sign in with #{provider.to_s.titleize}", omniauth_authorize_path(resource_name, provider) %><br />
|
||||
<% end -%>
|
||||
<% end -%>
|
@ -1,20 +0,0 @@
|
||||
- links = []
|
||||
|
||||
- if controller_name != 'sessions'
|
||||
- links << ['Sign in', new_session_path(resource_name)]
|
||||
|
||||
- if devise_mapping.registerable? && controller_name != 'registrations'
|
||||
- links << ['Sign up', new_registration_path(resource_name)]
|
||||
|
||||
- if devise_mapping.recoverable? && controller_name != 'passwords'
|
||||
- links << ['Forgot your password?', new_password_path(resource_name)]
|
||||
|
||||
- if devise_mapping.confirmable? && controller_name != 'confirmations'
|
||||
- links << ['Didn\'t receive confirmation instructions?', new_confirmation_path(resource_name)]
|
||||
|
||||
- if devise_mapping.omniauthable?
|
||||
- resource_class.omniauth_providers.each do |provider|
|
||||
- links << ["Sign in with #{provider.to_s.titleize}", omniauth_authorize_path(resource_name, provider)]
|
||||
|
||||
- if links.any?
|
||||
%p= links.map { |title, url| link_to title, url }.join(' · ').html_safe
|
12
app/views/devise/confirmations/new.html.erb
Normal file
12
app/views/devise/confirmations/new.html.erb
Normal file
@ -0,0 +1,12 @@
|
||||
<h2>Resend confirmation instructions</h2>
|
||||
|
||||
<%= form_for(resource, :as => resource_name, :url => confirmation_path(resource_name), :html => { :method => :post }) do |f| %>
|
||||
<%= devise_error_messages! %>
|
||||
|
||||
<div><%= f.label :email %><br />
|
||||
<%= f.email_field :email %></div>
|
||||
|
||||
<div><%= f.submit "Resend confirmation instructions" %></div>
|
||||
<% end %>
|
||||
|
||||
<%= render "links" %>
|
16
app/views/devise/passwords/edit.html.erb
Normal file
16
app/views/devise/passwords/edit.html.erb
Normal file
@ -0,0 +1,16 @@
|
||||
<h2>Change your password</h2>
|
||||
|
||||
<%= form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :put }) do |f| %>
|
||||
<%= devise_error_messages! %>
|
||||
<%= f.hidden_field :reset_password_token %>
|
||||
|
||||
<div><%= f.label :password, "New password" %><br />
|
||||
<%= f.password_field :password %></div>
|
||||
|
||||
<div><%= f.label :password_confirmation, "Confirm new password" %><br />
|
||||
<%= f.password_field :password_confirmation %></div>
|
||||
|
||||
<div><%= f.submit "Change my password" %></div>
|
||||
<% end %>
|
||||
|
||||
<%= render "links" %>
|
@ -1,19 +0,0 @@
|
||||
%h1 Change your password
|
||||
|
||||
.row
|
||||
.col-xs-12.col-sm-6.col-lg-4
|
||||
= form_for resource, as: resource_name, url: password_path(resource_name), html: {method: :put} do |f|
|
||||
= devise_error_messages!
|
||||
= f.hidden_field :reset_password_token
|
||||
|
||||
%fieldset
|
||||
.form-group
|
||||
= f.password_field :password, placeholder: 'New password', class: 'form-control'
|
||||
|
||||
.form-group
|
||||
= f.password_field :password_confirmation, placeholder: 'Confirm new password', class: 'form-control'
|
||||
|
||||
.form-group
|
||||
= f.submit 'Change my password', class: 'btn btn-primary'
|
||||
|
||||
= render 'links'
|
16
app/views/devise/passwords/new.html.erb
Normal file
16
app/views/devise/passwords/new.html.erb
Normal file
@ -0,0 +1,16 @@
|
||||
<%= 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 class="form-group">
|
||||
<%= f.email_field :email, placeholder: "Email", class: "form-control input-lg" %>
|
||||
</div>
|
||||
|
||||
<div><%= f.submit "Reset Password", class:"btn btn-lg btn-primary"%></div>
|
||||
<% end %>
|
||||
|
||||
<%= render "links" %>
|
@ -1,15 +0,0 @@
|
||||
%h1 Forgot your password?
|
||||
|
||||
.row
|
||||
.col-xs-12.col-sm-6.col-lg-4
|
||||
= form_for resource, as: resource_name, url: password_path(resource_name), html: {method: :post} do |f|
|
||||
= devise_error_messages!
|
||||
|
||||
%fieldset
|
||||
.form-group
|
||||
= f.email_field :email, placeholder: 'Email', class: 'form-control'
|
||||
|
||||
.form-group
|
||||
= f.submit 'Reset Password', class: 'btn btn-primary'
|
||||
|
||||
= render 'links'
|
@ -1,17 +1,14 @@
|
||||
.form-group
|
||||
= f.text_field :addrStreet1, placeholder: 'Street Address Line 1', class: 'form-control'
|
||||
|
||||
.form-group
|
||||
= f.text_field :addrStreet2, placeholder: 'Street Address Line 2',class: 'form-control'
|
||||
|
||||
.form-group
|
||||
= f.text_field :addrCity, placeholder: 'City', class: 'form-control'
|
||||
|
||||
.form-group
|
||||
= f.text_field :addrState, placeholder: 'State Abbreviation', class: 'form-control'
|
||||
|
||||
.form-group
|
||||
= f.text_field :addrZip, placeholder: 'Zip Code', class: 'form-control'
|
||||
|
||||
.form-group
|
||||
= f.text_field :phone, placeholder: 'Phone', class: 'form-control'
|
||||
%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"
|
||||
|
27
app/views/devise/registrations/edit.html.erb
Normal file
27
app/views/devise/registrations/edit.html.erb
Normal file
@ -0,0 +1,27 @@
|
||||
<%= stylesheet_link_tag "bootstrap_and_overrides", :media => "all" %>
|
||||
|
||||
<h2>Edit <%= resource_name.to_s.humanize %></h2>
|
||||
|
||||
<%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :method => :put }) do |f| %>
|
||||
<%= devise_error_messages! %>
|
||||
|
||||
<div><%= f.label :email %><br />
|
||||
<%= f.email_field :email %></div>
|
||||
|
||||
<div><%= f.label :password %> <i>(leave blank if you don't want to change it)</i><br />
|
||||
<%= f.password_field :password, :autocomplete => "off" %></div>
|
||||
|
||||
<div><%= f.label :password_confirmation %><br />
|
||||
<%= f.password_field :password_confirmation %></div>
|
||||
|
||||
<div><%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i><br />
|
||||
<%= f.password_field :current_password %></div>
|
||||
|
||||
<div><%= f.submit "Update" %></div>
|
||||
<% end %>
|
||||
|
||||
<h3>Cancel my account</h3>
|
||||
|
||||
<p>Unhappy? <%= link_to "Cancel my account", registration_path(resource_name), :confirm => "Are you sure?", :method => :delete %>.</p>
|
||||
|
||||
<%= link_to "Back", :back %>
|
@ -1,47 +1,23 @@
|
||||
%h1 Sign up
|
||||
|
||||
.row
|
||||
.col-xs-12.col-sm-6.col-lg-4
|
||||
= form_for resource, as: resource_name, url: registration_path(resource_name) do |f|
|
||||
= devise_error_messages!
|
||||
|
||||
%fieldset
|
||||
.form-group
|
||||
= f.text_field :username, placeholder: 'Username', class: 'form-control', required: true
|
||||
.help-block
|
||||
|
||||
.form-group
|
||||
= f.text_field :first_name, placeholder: 'First Name', class: 'form-control', required: true
|
||||
.help-block
|
||||
|
||||
.form-group
|
||||
= f.text_field :last_name, placeholder: 'Last Name', class: 'form-control', required: true
|
||||
.help-block
|
||||
|
||||
.form-group
|
||||
= f.email_field :email, placeholder: 'E-mail', class: 'form-control'
|
||||
|
||||
%fieldset
|
||||
- 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
|
||||
|
||||
%fieldset
|
||||
.form-group
|
||||
%label You may upload your photo to be used as an avatar
|
||||
= f.file_field :avatar
|
||||
.help-block #{User::MAX_AVATAR_SIZE_KB} Kb max
|
||||
|
||||
%fieldset
|
||||
.form-group
|
||||
= f.password_field :password, placeholder: 'Password', class: 'form-control', required: true
|
||||
.help-block
|
||||
|
||||
.form-group
|
||||
= f.password_field :password_confirmation, placeholder: 'Password Confirmation', class: 'form-control', required: true
|
||||
.help-block
|
||||
|
||||
.form-group
|
||||
= f.submit 'Sign up', class: 'btn btn-primary'
|
||||
|
||||
= render 'links'
|
||||
= stylesheet_link_tag "bootstrap_and_overrides", :media => "all"
|
||||
= form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f|
|
||||
= devise_error_messages!
|
||||
.controls
|
||||
%h2 Sign up
|
||||
.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
|
||||
.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"
|
||||
|
48
app/views/devise/sessions/new.html.erb
Normal file
48
app/views/devise/sessions/new.html.erb
Normal file
@ -0,0 +1,48 @@
|
||||
<%= stylesheet_link_tag "bootstrap_and_overrides", :media => "all" %>
|
||||
|
||||
<h2>Velocipede</h2>
|
||||
<a href="http://madewithloveinbaltimore.org">Made with ♥ in Baltimore</a>
|
||||
<br>
|
||||
<br>
|
||||
<%= form_for(resource, :as => resource_name, :url => session_path(resource_name)) do |f| %>
|
||||
<div class="form-group">
|
||||
<%= f.text_field :username, placeholder: "Username", class: "form-control input-lg"%>
|
||||
</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? -%>
|
||||
<label>
|
||||
<%= f.check_box :remember_me %> Remember Me
|
||||
</label>
|
||||
<% end -%>
|
||||
</div>
|
||||
|
||||
<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%>
|
||||
<%= f.hidden_field :password, :value => 'password' %>
|
||||
<%= f.submit "Sign in as #{user.username}", class:"btn btn-info" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
@ -1,41 +0,0 @@
|
||||
%h1 Velocipede
|
||||
%p= link_to 'Made with ♥ in Baltimore'.html_safe, 'http://madewithloveinbaltimore.org'
|
||||
|
||||
.row
|
||||
.col-xs-12.col-sm-6.col-lg-4
|
||||
= form_for resource, as: resource_name, url: session_path(resource_name) do |f|
|
||||
|
||||
%fieldset
|
||||
.form-group
|
||||
= f.text_field :username, placeholder: 'Username', class: 'form-control'
|
||||
|
||||
.form-group
|
||||
= f.password_field :password, placeholder: 'Password', class: 'form-control'
|
||||
|
||||
- if devise_mapping.rememberable?
|
||||
.form-group
|
||||
.checkbox
|
||||
= f.label :remember_me do
|
||||
= f.check_box :remember_me
|
||||
Remember Me
|
||||
|
||||
.form-group
|
||||
.pull-left
|
||||
= f.submit 'Sign in', class: 'btn btn-primary'
|
||||
|
||||
.pull-right
|
||||
%span.btn-group
|
||||
= f.button 'Check in', id: 'checkin', name: 'checkin', type: 'button', class: 'btn btn-success'
|
||||
= f.button 'Check out', id: 'checkout', name: 'checkout', type: 'button', class: 'btn btn-danger'
|
||||
|
||||
.clearfix
|
||||
|
||||
= render 'links'
|
||||
|
||||
- if Rails.env.development?
|
||||
%hr
|
||||
- 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
|
||||
= f.hidden_field :password, value: 'password'
|
||||
%p= f.submit "Sign in as #{user.username}", class: 'btn btn-sm btn-info'
|
12
app/views/devise/unlocks/new.html.erb
Normal file
12
app/views/devise/unlocks/new.html.erb
Normal file
@ -0,0 +1,12 @@
|
||||
<h2>Resend unlock instructions</h2>
|
||||
|
||||
<%= form_for(resource, :as => resource_name, :url => unlock_path(resource_name), :html => { :method => :post }) do |f| %>
|
||||
<%= devise_error_messages! %>
|
||||
|
||||
<div><%= f.label :email %><br />
|
||||
<%= f.email_field :email %></div>
|
||||
|
||||
<div><%= f.submit "Resend unlock instructions" %></div>
|
||||
<% end %>
|
||||
|
||||
<%= render "links" %>
|
@ -1,27 +1,35 @@
|
||||
!!! 5
|
||||
%html(lang="en")
|
||||
%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'
|
||||
%meta{:charset => "utf-8"}/
|
||||
%meta{ name: "viewport", content: "width=device-width", "initial-scale" => "1.0"}
|
||||
%title= content_for?(:title) ? yield(:title) : "Velocipede"
|
||||
= csrf_meta_tags
|
||||
= stylesheet_link_tag 'application', media: 'all'
|
||||
= 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'
|
||||
= javascript_include_tag "http://html5shim.googlecode.com/svn/trunk/html5.js"
|
||||
:css
|
||||
body {
|
||||
padding-top: 5px;
|
||||
}
|
||||
.x-boundlist-item {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
%body
|
||||
.container
|
||||
|
||||
.content
|
||||
- if flash[:notice]
|
||||
%div.alert.alert-info= flash[:notice]
|
||||
%p{:class => 'notice'}= flash[:notice]
|
||||
- if flash[:alert]
|
||||
%div.alert.alert-danger= flash[:alert]
|
||||
|
||||
= yield
|
||||
%p{:class => 'alert'}= flash[:alert]
|
||||
.row
|
||||
.span12
|
||||
= yield
|
||||
|
||||
%footer
|
||||
%hr
|
||||
%p.text-muted © BikeShed #{Time.now.year}
|
||||
%p © BikeShed #{Time.now.year}
|
||||
|
||||
= javascript_include_tag 'application'
|
||||
= javascript_include_tag "application"
|
||||
= javascript_include_tag params[:controller]
|
||||
|
@ -1,31 +1,21 @@
|
||||
%h1 Dashboard
|
||||
= stylesheet_link_tag "bootstrap_and_overrides", :media => "all"
|
||||
%h2 Dashboard
|
||||
|
||||
.row.fieldset
|
||||
.col-xs-12.col-sm-3.col-lg-2
|
||||
%p= link_to 'Add Time Entry', new_time_entry_path, class: 'btn btn-default btn-block'
|
||||
.col-xs-12.col-sm-3.col-lg-2
|
||||
%p= link_to 'View Timesheet', time_entries_path, class: 'btn btn-default btn-block'
|
||||
%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
|
||||
%p
|
||||
%a{class: "btn btn-lg btn-block btn-primary", href: new_bike_path} Add Bike
|
||||
|
||||
- can_manage_bike = can? :manage, Bike
|
||||
- has_bike = !@bike.nil?
|
||||
- if can_manage_bike || has_bike
|
||||
.row.fieldset
|
||||
- if can_manage_bike
|
||||
.col-xs-12.col-sm-3.col-lg-2
|
||||
%p
|
||||
= link_to 'Add Bike', new_bike_path, class: 'btn btn-default btn-block'
|
||||
- if !@bike.nil?
|
||||
%p
|
||||
%a{class: "btn btn-lg btn-block btn-primary", href: bike_path(@bike)} View Your Bike
|
||||
|
||||
- if has_bike
|
||||
.col-xs-12.col-sm-3.col-lg-2
|
||||
%p
|
||||
= link_to 'View Your Bike', bike_path(@bike), class: 'btn btn-default btn-block'
|
||||
|
||||
.row.fieldset
|
||||
.col-xs-12.col-sm-3.col-lg-2
|
||||
%p
|
||||
= link_to 'Desktop View', admin_index_path, class: 'btn btn-default btn-block'
|
||||
|
||||
.row.fieldset
|
||||
.col-xs-12.col-sm-3.col-lg-2
|
||||
%p
|
||||
= button_tag 'Logout', id: 'index_logout', class: 'btn btn-danger btn-block', data: {url: destroy_user_session_path}
|
||||
%p
|
||||
%p
|
||||
%a{class: "btn btn-lg btn-block btn-primary hidden-xs", href: admin_index_path} Admin View
|
||||
%p
|
||||
%input{id: "index_logout", value: "Logout", type: "button", class: "btn btn-lg btn-block btn-danger", "data-url" => destroy_user_session_path }
|
||||
|
@ -1,24 +1,16 @@
|
||||
= top_menu
|
||||
|
||||
%h1 Task List
|
||||
|
||||
%a{ class: "btn btn-default btn-lg", href: root_path}
|
||||
%span{ class:"icon-home"}
|
||||
%h2 Task List
|
||||
%h3
|
||||
= link_to bike_path(@task_list.item) do
|
||||
#{@task_list.item.shop_id}: #{@task_list.item.bike_brand}
|
||||
%a{ href: bike_path(@task_list.item)}#{@task_list.item.shop_id} #{@task_list.item.bike_brand}
|
||||
%h4 #{@task_list.item.model}
|
||||
|
||||
%h4= @task_list.item.model
|
||||
|
||||
- if @task_list.item.photo?
|
||||
%p
|
||||
%img{src: @task_list.item.photo.url(:thumb), class: 'img-thumbnail'}
|
||||
|
||||
%fieldset
|
||||
.form-group
|
||||
%p
|
||||
.control-group
|
||||
- @task_list.tasks.each do |task|
|
||||
.checkbox
|
||||
%label
|
||||
= check_box_tag nil, nil, task.done, class: 'task_list_task', type: 'checkbox', data: {id: task.id}
|
||||
= task.task
|
||||
|
||||
.form-group
|
||||
= button_tag 'Save Changes', id: 'update_tasks_submit', class: 'btn btn-primary disabled', data: {url: api_update_task_path}
|
||||
.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}"}
|
||||
|
@ -1,47 +1,44 @@
|
||||
= top_menu
|
||||
%a{ class: "btn btn-default btn-lg", href: root_path}
|
||||
%span{ class:"icon-home"}
|
||||
%h2 Your Timesheet
|
||||
|
||||
%h1 Your Timesheet
|
||||
|
||||
.row
|
||||
.col-xs-12.col-sm-6.col-lg-3
|
||||
%p
|
||||
%span.badge= @hours_worked
|
||||
Total Hours Worked
|
||||
|
||||
.col-xs-12.col-sm-6.col-lg-3
|
||||
%p
|
||||
%span.badge= @credits_available
|
||||
Total Credits Available
|
||||
%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|
|
||||
-# data: {start_date: ..} gets transformed to data-start-date instead of data-start_date, we don't want this
|
||||
%tr{data: {id: entry.id, description: entry.description, 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= link_to truncate(entry.description).presence || '<em>No title</em>'.html_safe, edit_task_list_path(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
|
||||
= link_to '#modal', class: 'text-danger work_entry-delete-btn', role: 'button', data: {toggle: 'modal', target: '#confirmation'} do
|
||||
%i.glyphicon.glyphicon-remove
|
||||
%button{ class: "btn icon-remove btn-danger work_entry-delete-btn", role: "button", "data-toggle" => "modal", "data-target" => "#confirmation" }
|
||||
|
||||
= link_to 'Add Time Entry', new_time_entry_path, class: 'btn btn-primary'
|
||||
%a{class: "btn btn-lg btn-block btn-primary", href: new_time_entry_path } Add Time Entry
|
||||
|
||||
.modal#confirmation(role="dialog" aria-labelledby="confirmation_title" aria-hidden="true" tabindex="-1")
|
||||
.modal-dialog
|
||||
.modal-content
|
||||
.modal-header
|
||||
%button.close(data-dismiss="modal" aria-hidden="true") ×
|
||||
%h4#confirmation_title Are you sure you want to delete?
|
||||
.modal-body
|
||||
#work_entry_start_date Start Date
|
||||
#work_entry_duration Duration
|
||||
#work_entry_description Description
|
||||
.modal-footer
|
||||
%button.btn.btn-default(data-dismiss="modal" aria-hidden="true") Cancel
|
||||
%button.btn.btn-danger#confirmation_delete{data: {url_template: api_delete_time_entry_path('__ID__')}} Delete
|
||||
.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
|
||||
|
||||
|
||||
|
||||
|
@ -1,56 +1,42 @@
|
||||
= top_menu
|
||||
%a{ class: "btn btn-default btn-lg", href: root_path}
|
||||
%span{ class:"icon-home"}
|
||||
%h2 Add Time Entry
|
||||
|
||||
%h1 Add Time Entry
|
||||
- time = Time.now
|
||||
- date_format, time_format = '%m/%d/%Y', '%l:%M %p'
|
||||
- mjs_date_format, mjs_time_format = 'MM/DD/YYYY', 'h:mm A'
|
||||
|
||||
.row
|
||||
.col-xs-12.col-sm-6.col-lg-4
|
||||
%fieldset
|
||||
.form-group.form-inline
|
||||
= text_field_tag nil, time.strftime(date_format), id: 'date_id', placeholder: 'Date', class: 'form-control',
|
||||
size: 12, data: {format: mjs_date_format}
|
||||
.help-block
|
||||
|
||||
.form-group.timepickers.form-inline
|
||||
.form-group
|
||||
%label.visible-xs Start time
|
||||
= text_field_tag nil, time.strftime(time_format), id: 'start_time_id', placeholder: 'Start time',
|
||||
class: 'form-control', size: 8, data: {format: mjs_time_format}
|
||||
.hidden#start_date
|
||||
.help-block
|
||||
|
||||
.form-group.dash.hidden-xs
|
||||
—
|
||||
|
||||
.form-group
|
||||
%label.visible-xs End time
|
||||
= text_field_tag nil, time.strftime(time_format), id: 'end_time_id', placeholder: 'End time',
|
||||
class: 'form-control', size: 8, data: {format: mjs_time_format}
|
||||
.hidden#end_date
|
||||
.help-block
|
||||
|
||||
.form-group
|
||||
.btn-group(data-toggle="buttons")
|
||||
%label.btn.btn-default
|
||||
= radio_button_tag 'action_id', 1, false, autocomplete: 'off'
|
||||
Volunteer
|
||||
%label.btn.btn-default
|
||||
= radio_button_tag 'action_id', 2, false, autocomplete: 'off'
|
||||
Personal
|
||||
%label.btn.btn-default
|
||||
= radio_button_tag 'action_id', 3, false, autocomplete: 'off'
|
||||
Staff
|
||||
.hidden#log_action_id
|
||||
.help-block
|
||||
|
||||
.form-group
|
||||
= select_tag :bike_id, options_for_select(@bikes), class: 'form-control'
|
||||
|
||||
.form-group
|
||||
= text_area_tag nil, nil, id: 'description_id', placeholder: 'Work description', class: 'form-control', rows: 5
|
||||
|
||||
.control-group
|
||||
.controls
|
||||
= button_tag 'Add Time Entry', id: 'add_time_entry_submit', class: 'btn btn-primary', data: {url: api_create_time_entry_path, forward: '.'}
|
||||
%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" => "#{"."}"}
|
||||
|
68
app/views/users/_form.html.haml
Normal file
68
app/views/users/_form.html.haml
Normal file
@ -0,0 +1,68 @@
|
||||
= form_for @user, :html => { :class => 'form-horizontal' } do |f|
|
||||
.control-group
|
||||
= f.label :email, :class => 'control-label'
|
||||
.controls
|
||||
= f.text_field :email, :class => 'text_field'
|
||||
.control-group
|
||||
= f.label :encrypted_password, :class => 'control-label'
|
||||
.controls
|
||||
= f.text_field :encrypted_password, :class => 'text_field'
|
||||
.control-group
|
||||
= f.label :reset_password_token, :class => 'control-label'
|
||||
.controls
|
||||
= f.text_field :reset_password_token, :class => 'text_field'
|
||||
.control-group
|
||||
= f.label :reset_password_sent_at, :class => 'control-label'
|
||||
.controls
|
||||
= f.datetime_select :reset_password_sent_at, :class => 'datetime_select'
|
||||
.control-group
|
||||
= f.label :remember_created_at, :class => 'control-label'
|
||||
.controls
|
||||
= f.datetime_select :remember_created_at, :class => 'datetime_select'
|
||||
.control-group
|
||||
= f.label :sign_in_count, :class => 'control-label'
|
||||
.controls
|
||||
= f.number_field :sign_in_count, :class => 'number_field'
|
||||
.control-group
|
||||
= f.label :current_sign_in_at, :class => 'control-label'
|
||||
.controls
|
||||
= f.datetime_select :current_sign_in_at, :class => 'datetime_select'
|
||||
.control-group
|
||||
= f.label :last_sign_in_at, :class => 'control-label'
|
||||
.controls
|
||||
= f.datetime_select :last_sign_in_at, :class => 'datetime_select'
|
||||
.control-group
|
||||
= f.label :current_sign_in_ip, :class => 'control-label'
|
||||
.controls
|
||||
= f.text_field :current_sign_in_ip, :class => 'text_field'
|
||||
.control-group
|
||||
= f.label :last_sign_in_ip, :class => 'control-label'
|
||||
.controls
|
||||
= f.text_field :last_sign_in_ip, :class => 'text_field'
|
||||
.control-group
|
||||
= f.label :failed_attempts, :class => 'control-label'
|
||||
.controls
|
||||
= f.number_field :failed_attempts, :class => 'number_field'
|
||||
.control-group
|
||||
= f.label :unlock_token, :class => 'control-label'
|
||||
.controls
|
||||
= f.text_field :unlock_token, :class => 'text_field'
|
||||
.control-group
|
||||
= f.label :locked_at, :class => 'control-label'
|
||||
.controls
|
||||
= f.datetime_select :locked_at, :class => 'datetime_select'
|
||||
.control-group
|
||||
= f.label :first_name, :class => 'control-label'
|
||||
.controls
|
||||
= f.text_field :first_name, :class => 'text_field'
|
||||
.control-group
|
||||
= f.label :last_name, :class => 'control-label'
|
||||
.controls
|
||||
= f.text_field :last_name, :class => 'text_field'
|
||||
.control-group
|
||||
= f.label :nickname, :class => 'control-label'
|
||||
.controls
|
||||
= f.text_field :nickname, :class => 'text_field'
|
||||
.form-actions
|
||||
= f.submit nil, :class => 'btn btn-primary'
|
||||
= link_to t('.cancel', :default => t("helpers.links.cancel")), users_path, :class => 'btn'
|
4
app/views/users/edit.html.haml
Normal file
4
app/views/users/edit.html.haml
Normal file
@ -0,0 +1,4 @@
|
||||
- model_class = user.class
|
||||
.page-header
|
||||
%h1=t '.title', :default => t('helpers.titles.edit', :model => model_class.model_name.human, :default => "Edit #{model_class.model_name.human}")
|
||||
= render :partial => "form"
|
28
app/views/users/index.html.haml
Normal file
28
app/views/users/index.html.haml
Normal file
@ -0,0 +1,28 @@
|
||||
- model_class = User.new.class
|
||||
.page-header
|
||||
%h1=t '.title', :default => model_class.model_name.human.pluralize
|
||||
%table.table.table-striped
|
||||
%thead
|
||||
%tr
|
||||
%th= model_class.human_attribute_name(:id)
|
||||
%th= model_class.human_attribute_name(:email)
|
||||
%th= model_class.human_attribute_name(:first_name)
|
||||
%th= model_class.human_attribute_name(:last_name)
|
||||
%th= model_class.human_attribute_name(:nickname)
|
||||
%th= model_class.human_attribute_name(:created_at)
|
||||
%th=t '.actions', :default => t("helpers.actions")
|
||||
%tbody
|
||||
- users.each do |user|
|
||||
%tr
|
||||
%td= link_to user.id, user_path(user)
|
||||
%td= link_to user.email, user_path(user)
|
||||
%td= user.first_name
|
||||
%td= user.last_name
|
||||
%td= user.nickname
|
||||
%td=l user.created_at
|
||||
%td
|
||||
= link_to t('.edit', :default => t("helpers.links.edit")), edit_user_path(user), :class => 'btn btn-mini'
|
||||
= link_to t('.destroy', :default => t("helpers.links.destroy")), user_path(user), :method => :delete, :confirm => t('.confirm', :default => t("helpers.links.confirm", :default => 'Are you sure?')), :class => 'btn btn-mini btn-danger'
|
||||
|
||||
= will_paginate users
|
||||
= link_to t('.new', :default => t("helpers.links.new")), new_user_path, :class => 'btn btn-primary'
|
4
app/views/users/new.html.haml
Normal file
4
app/views/users/new.html.haml
Normal file
@ -0,0 +1,4 @@
|
||||
- model_class = user.class
|
||||
.page-header
|
||||
%h1=t '.title', :default => t('helpers.titles.new', :model => model_class.model_name.human, :default => "New #{model_class.model_name.human}")
|
||||
= render :partial => "form"
|
73
app/views/users/show.html.haml
Normal file
73
app/views/users/show.html.haml
Normal file
@ -0,0 +1,73 @@
|
||||
- model_class = user.class
|
||||
.page-header
|
||||
%h1=t '.title', :default => model_class.model_name.human
|
||||
|
||||
%p
|
||||
%strong= model_class.human_attribute_name(:email) + ':'
|
||||
%br
|
||||
= user.email
|
||||
%p
|
||||
%strong= model_class.human_attribute_name(:encrypted_password) + ':'
|
||||
%br
|
||||
= user.encrypted_password
|
||||
%p
|
||||
%strong= model_class.human_attribute_name(:reset_password_token) + ':'
|
||||
%br
|
||||
= user.reset_password_token
|
||||
%p
|
||||
%strong= model_class.human_attribute_name(:reset_password_sent_at) + ':'
|
||||
%br
|
||||
= user.reset_password_sent_at
|
||||
%p
|
||||
%strong= model_class.human_attribute_name(:remember_created_at) + ':'
|
||||
%br
|
||||
= user.remember_created_at
|
||||
%p
|
||||
%strong= model_class.human_attribute_name(:sign_in_count) + ':'
|
||||
%br
|
||||
= user.sign_in_count
|
||||
%p
|
||||
%strong= model_class.human_attribute_name(:current_sign_in_at) + ':'
|
||||
%br
|
||||
= user.current_sign_in_at
|
||||
%p
|
||||
%strong= model_class.human_attribute_name(:last_sign_in_at) + ':'
|
||||
%br
|
||||
= user.last_sign_in_at
|
||||
%p
|
||||
%strong= model_class.human_attribute_name(:current_sign_in_ip) + ':'
|
||||
%br
|
||||
= user.current_sign_in_ip
|
||||
%p
|
||||
%strong= model_class.human_attribute_name(:last_sign_in_ip) + ':'
|
||||
%br
|
||||
= user.last_sign_in_ip
|
||||
%p
|
||||
%strong= model_class.human_attribute_name(:failed_attempts) + ':'
|
||||
%br
|
||||
= user.failed_attempts
|
||||
%p
|
||||
%strong= model_class.human_attribute_name(:unlock_token) + ':'
|
||||
%br
|
||||
= user.unlock_token
|
||||
%p
|
||||
%strong= model_class.human_attribute_name(:locked_at) + ':'
|
||||
%br
|
||||
= user.locked_at
|
||||
%p
|
||||
%strong= model_class.human_attribute_name(:first_name) + ':'
|
||||
%br
|
||||
= user.first_name
|
||||
%p
|
||||
%strong= model_class.human_attribute_name(:last_name) + ':'
|
||||
%br
|
||||
= user.last_name
|
||||
%p
|
||||
%strong= model_class.human_attribute_name(:nickname) + ':'
|
||||
%br
|
||||
= user.nickname
|
||||
|
||||
.form-actions
|
||||
= link_to t('.back', :default => t("helpers.links.back")), users_path, :class => 'btn'
|
||||
= link_to t('.edit', :default => t("helpers.links.edit")), edit_user_path(user), :class => 'btn'
|
||||
= link_to t('.destroy', :default => t("helpers.links.destroy")), user_path(user), :method => "delete", :confirm => t('.confirm', :default => t("helpers.links.confirm", :default => 'Are you sure?')), :class => 'btn btn-danger'
|
@ -62,8 +62,5 @@ module Velocipede
|
||||
|
||||
# Version of your assets, change this if you want to expire all your assets
|
||||
config.assets.version = '1.0'
|
||||
|
||||
# Enabled for bootstrap-sass gem
|
||||
config.assets.initialize_on_precompile
|
||||
end
|
||||
end
|
||||
|
@ -1,19 +1,23 @@
|
||||
development: &default
|
||||
development:
|
||||
adapter: postgresql
|
||||
encoding: unicode
|
||||
pool: 5
|
||||
database: <%= ENV['RDS_DB_NAME'] || 'postgres' %>
|
||||
username: <%= ENV['RDS_USERNAME'] || 'postgres' %>
|
||||
password: <%= ENV['RDS_PASSWORD'] %>
|
||||
host: <%= ENV['RDS_HOSTNAME'] || 'db' %>
|
||||
database: velocipede
|
||||
username: velocipede
|
||||
password:
|
||||
host: 127.0.0.1
|
||||
|
||||
# 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:
|
||||
<<: *default
|
||||
adapter: postgresql
|
||||
database: velocipede_test
|
||||
username: velocipede
|
||||
password:
|
||||
host: 127.0.0.1
|
||||
|
||||
production:
|
||||
<<: *default
|
||||
database: velocipede_production
|
||||
adapter: postgresql
|
||||
database: velocipede
|
||||
username: velocipede
|
||||
password:
|
||||
host: 127.0.0.1
|
||||
|
@ -10,6 +10,10 @@ class CreateBikeWheelSizes < ActiveRecord::Migration
|
||||
t.string :description
|
||||
t.string :tire_common_score
|
||||
end
|
||||
|
||||
#create the default undetermined wheel size record
|
||||
BikeWheelSize.create( twmm: 0, rdmm: 0, rdin: 0, twin: 0, rdfr: 0, twfr: 0, description: "UNDETERMINED", tire_common_score: 0)
|
||||
ActiveRecord::Base.connection.execute(IO.read(File.join(Rails.root, "db", "seed", "sql", "common_wheel_sizes.sql")))
|
||||
end
|
||||
|
||||
def down
|
||||
|
@ -1,11 +0,0 @@
|
||||
class AddAttachmentAvatarToUsers < ActiveRecord::Migration
|
||||
def self.up
|
||||
change_table :users do |t|
|
||||
t.attachment :avatar
|
||||
end
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_attachment :users, :avatar
|
||||
end
|
||||
end
|
@ -1,11 +0,0 @@
|
||||
class AddAttachmentPhotoToBikes < ActiveRecord::Migration
|
||||
def self.up
|
||||
change_table :bikes do |t|
|
||||
t.attachment :photo
|
||||
end
|
||||
end
|
||||
|
||||
def self.down
|
||||
remove_attachment :bikes, :photo
|
||||
end
|
||||
end
|
@ -1,15 +0,0 @@
|
||||
class UserEmailCanBeNull < ActiveRecord::Migration
|
||||
def up
|
||||
change_table :users do |t|
|
||||
t.change :email, :string, default: nil, null: true
|
||||
end
|
||||
|
||||
User.where(email: '').update_all(email: nil)
|
||||
end
|
||||
|
||||
def down
|
||||
change_table :users do |t|
|
||||
t.change :email, :string, default: '', null: false
|
||||
end
|
||||
end
|
||||
end
|
12
db/schema.rb
12
db/schema.rb
@ -11,7 +11,7 @@
|
||||
#
|
||||
# It's strongly recommended to check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(:version => 20170131164224) do
|
||||
ActiveRecord::Schema.define(:version => 20131019170248) do
|
||||
|
||||
create_table "bike_actions", :force => true do |t|
|
||||
t.string "action", :limit => 128, :null => false
|
||||
@ -73,10 +73,6 @@ ActiveRecord::Schema.define(:version => 20170131164224) do
|
||||
t.string "model"
|
||||
t.integer "shop_id"
|
||||
t.integer "bike_wheel_size_id"
|
||||
t.string "photo_file_name"
|
||||
t.string "photo_content_type"
|
||||
t.integer "photo_file_size"
|
||||
t.datetime "photo_updated_at"
|
||||
end
|
||||
|
||||
create_table "credit_conversions", :force => true do |t|
|
||||
@ -183,7 +179,7 @@ ActiveRecord::Schema.define(:version => 20170131164224) do
|
||||
end
|
||||
|
||||
create_table "users", :force => true do |t|
|
||||
t.string "email"
|
||||
t.string "email", :default => "", :null => false
|
||||
t.string "encrypted_password", :default => "", :null => false
|
||||
t.string "reset_password_token"
|
||||
t.datetime "reset_password_sent_at"
|
||||
@ -202,10 +198,6 @@ ActiveRecord::Schema.define(:version => 20170131164224) do
|
||||
t.string "first_name", :default => "", :null => false
|
||||
t.string "last_name", :default => "", :null => false
|
||||
t.string "username"
|
||||
t.string "avatar_file_name"
|
||||
t.string "avatar_content_type"
|
||||
t.integer "avatar_file_size"
|
||||
t.datetime "avatar_updated_at"
|
||||
end
|
||||
|
||||
add_index "users", ["bike_id"], :name => "index_users_on_bike_id", :unique => true
|
||||
|
@ -1,6 +1,3 @@
|
||||
PRAGMA foreign_keys=OFF;
|
||||
BEGIN TRANSACTION;
|
||||
INSERT INTO bike_wheel_sizes (TWMM, RDMM,RDIN,TWIN,RDFR,TWFR,DESCRIPTION,tire_common_score) values (0,0,0,0,0,0,'UNDETERMINED',0);
|
||||
INSERT INTO bike_wheel_sizes (TWMM, RDMM,RDIN,TWIN,RDFR,TWFR,DESCRIPTION,tire_common_score) values (54,110,'8 1/2','2','','','',1);
|
||||
INSERT INTO bike_wheel_sizes (TWMM, RDMM,RDIN,TWIN,RDFR,TWFR,DESCRIPTION,tire_common_score) values (62,203,'12','1/2 x 2 1/4','320 ','57','',1);
|
||||
INSERT INTO bike_wheel_sizes (TWMM, RDMM,RDIN,TWIN,RDFR,TWFR,DESCRIPTION,tire_common_score) values (47,305,'16','1,75 [x 2]','','','',1);
|
||||
@ -38,4 +35,3 @@ INSERT INTO bike_wheel_sizes (TWMM, RDMM,RDIN,TWIN,RDFR,TWFR,DESCRIPTION,tire_co
|
||||
INSERT INTO bike_wheel_sizes (TWMM, RDMM,RDIN,TWIN,RDFR,TWFR,DESCRIPTION,tire_common_score) values (28,630,'27','1 1/8 [1 1/4 fifty]','','','',1);
|
||||
INSERT INTO bike_wheel_sizes (TWMM, RDMM,RDIN,TWIN,RDFR,TWFR,DESCRIPTION,tire_common_score) values (32,630,'27','1 1/4','','','',1);
|
||||
INSERT INTO bike_wheel_sizes (TWMM, RDMM,RDIN,TWIN,RDFR,TWFR,DESCRIPTION,tire_common_score) values (40,635,'28','1 1/2 [1 3/8]','700 ','38B [35B]','',1);
|
||||
COMMIT;
|
||||
|
@ -18,12 +18,10 @@ if BikeBrand.all.empty? and BikeModel.all.empty?
|
||||
# so that the PG table ID sequence is incremented
|
||||
#
|
||||
# Note the drop(1) which assumes we have a junk PRAGMA line at the top
|
||||
['common_wheel_sizes.sql', 'bike_brands_and_models.sql'].each do |sql|
|
||||
load_statements = File.readlines(File.join(Rails.root, 'db', 'seed', 'sql', sql)).drop(1).map do |statement|
|
||||
statement.sub(/VALUES\(\d+,/, 'VALUES(DEFAULT,')
|
||||
end
|
||||
ActiveRecord::Base.connection.execute(load_statements.join)
|
||||
load_statements = File.readlines(File.join(Rails.root, 'db', 'seed', 'sql', 'bike_brands_and_models.sql')).drop(1).map do |statement|
|
||||
statement.sub(/VALUES\(\d+,/, 'VALUES(DEFAULT,')
|
||||
end
|
||||
ActiveRecord::Base.connection.execute(load_statements.join)
|
||||
end
|
||||
|
||||
if Rails.env.development?
|
||||
|
1
doc/README_FOR_APP
Normal file
1
doc/README_FOR_APP
Normal file
@ -0,0 +1 @@
|
||||
# No API yet
|
@ -1,20 +0,0 @@
|
||||
# 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.
|
||||
|
||||
|
@ -1,34 +0,0 @@
|
||||
# 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:setup`
|
||||
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`
|
@ -1,21 +0,0 @@
|
||||
# BikeShed
|
||||
#
|
||||
# cp config/database.yml.example config/database.yml
|
||||
#
|
||||
# docker-compose -f docker-compose.demo.yml build
|
||||
# docker-compose -f docker-compose.demo.yml up -d db
|
||||
# docker-compose -f docker-compose.demo.yml run bikeshed rake db:setup
|
||||
# docker-compose -f docker-compose.demo.yml up -d bikeshed
|
||||
|
||||
db:
|
||||
image: postgres
|
||||
ports:
|
||||
- "5432"
|
||||
web:
|
||||
volumes:
|
||||
- .:/usr/src/app
|
||||
build: .
|
||||
ports:
|
||||
- "8080:3000"
|
||||
links:
|
||||
- db
|
BIN
extjs-4.1.1.zip
BIN
extjs-4.1.1.zip
Binary file not shown.
@ -1,20 +0,0 @@
|
||||
#!/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?
|
@ -1,19 +0,0 @@
|
||||
#!/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,36 +0,0 @@
|
||||
namespace :import do
|
||||
namespace :bikes do
|
||||
# Imports bikes info from CSV file
|
||||
#
|
||||
# rake import:bikes:csv[import.csv,dry] # dry run
|
||||
# rake import:bikes:csv[import.csv] # live import
|
||||
task :csv, [:file, :dry_run] => :environment do |t, args|
|
||||
file, dry_run = args.values_at :file, :dry_run
|
||||
next puts "Usage: rake #{t.name}[$csv_file_path[,$dry_run=dry]]" unless file
|
||||
next puts "File #{file} does not exist or is unreachable" unless File.readable? file
|
||||
BikeCsvImporter.new(file).run dry_run == 'dry'
|
||||
end
|
||||
|
||||
# Analyze a single field from CSV file
|
||||
#
|
||||
# rake import:bikes:analyze_csv[import.csv] # dumps all fields data
|
||||
# rake import:bikes:analyze_csv[import.csv,"date in"] # shows only single field
|
||||
task :analyze_csv, [:file, :field] => :environment do |t, args|
|
||||
file, field = args.values_at :file, :field
|
||||
next puts "Usage: rake #{t.name}[$csv_file_path[,\"$field_name\"]]" unless file
|
||||
next puts "File #{file} does not exist or is unreachable" unless File.readable? file
|
||||
BikeCsvImporter.new(file).analyze field ? [field] : []
|
||||
end
|
||||
|
||||
# Imports new brands from CSV file
|
||||
#
|
||||
# rake import:bikes:brands_csv[import.csv,dry] # dry run
|
||||
# rake import:bikes:brands_csv[import.csv] # live import
|
||||
task :brands_csv, [:file, :dry_run] => :environment do |t, args|
|
||||
file, dry_run = args.values_at :file, :dry_run
|
||||
next puts "Usage: rake #{t.name}[$csv_file_path[,$dry_run=dry]]" unless file
|
||||
next puts "File #{file} does not exist or is unreachable" unless File.readable? file
|
||||
BikeCsvImporter.new(file).brands dry_run == 'dry'
|
||||
end
|
||||
end
|
||||
end
|
@ -1 +0,0 @@
|
||||
/usr/lib/extjs
|
@ -1 +0,0 @@
|
||||
/usr/lib/famfamfam/icons/
|
@ -18,7 +18,7 @@ FactoryGirl.define do
|
||||
bike_purpose { FactoryGirl.create(:bike_purpose) }
|
||||
end
|
||||
|
||||
factory :seed_bike, class: Bike do
|
||||
factory :seed_bike do
|
||||
sequence(:shop_id) {|n| n}
|
||||
sequence :serial_number do |n|
|
||||
"#{Faker::Code.isbn}-#{n}"
|
||||
|
@ -13,9 +13,7 @@ describe "New User Registrations" do
|
||||
before do
|
||||
visit new_user_registration_path
|
||||
end
|
||||
|
||||
it 'should have the additional user fields on the registration page' do
|
||||
page.should have_field 'user_username'
|
||||
page.should have_field 'user_first_name'
|
||||
page.should have_field 'user_last_name'
|
||||
page.should have_field 'user_email'
|
||||
@ -31,13 +29,6 @@ describe "New User Registrations" do
|
||||
fill_in 'user_password_confirmation', :with => 'password'
|
||||
end
|
||||
|
||||
it 'should require username' do
|
||||
fill_in 'user_first_name', :with => 'Frank'
|
||||
fill_in 'user_last_name', :with => 'Footer'
|
||||
click_button 'Sign up'
|
||||
page.should have_content "Username can't be blank"
|
||||
end
|
||||
|
||||
it 'should require first name' do
|
||||
fill_in 'user_last_name', :with => 'Footer'
|
||||
click_button 'Sign up'
|
||||
@ -50,21 +41,5 @@ describe "New User Registrations" do
|
||||
page.should have_content "Last name can't be blank"
|
||||
end
|
||||
end
|
||||
|
||||
it 'should allow registering many users with empty emails' do
|
||||
expect do
|
||||
FactoryGirl.create :user, email: ''
|
||||
FactoryGirl.create :user, email: nil
|
||||
|
||||
fill_in 'user_username', :with => 'test3'
|
||||
fill_in 'user_first_name', :with => 'Frank3'
|
||||
fill_in 'user_last_name', :with => 'Footer3'
|
||||
fill_in 'user_password', :with => 'password3'
|
||||
fill_in 'user_password_confirmation', :with => 'password3'
|
||||
click_button 'Sign up'
|
||||
end.to change { User.count }.by(3)
|
||||
|
||||
expect(User.where(email: nil).count).to eq(3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -10,16 +10,16 @@ describe "Index Page" do
|
||||
end
|
||||
|
||||
it 'should have a link to check in' do
|
||||
page.should have_button 'Check in'
|
||||
page.should have_button 'CHECK IN'
|
||||
end
|
||||
|
||||
it 'should have a link to check out' do
|
||||
page.should have_button 'Check out'
|
||||
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'}.
|
||||
expect{click_button 'CHECK IN'}.
|
||||
to change{@user.checked_in?}.
|
||||
from(false).to(true)
|
||||
end
|
||||
@ -27,7 +27,7 @@ describe "Index Page" do
|
||||
it 'clicking check out should check out a user', js: true do
|
||||
pending
|
||||
@user.checkin
|
||||
expect{click_button 'Check out'}.
|
||||
expect{click_button 'CHECK OUT'}.
|
||||
to change{@user.checked_in?}.
|
||||
from(true).to(false)
|
||||
end
|
||||
|
@ -14,7 +14,7 @@ feature "TimeEntries" do
|
||||
scenario "User deletes a time entry", js: true do
|
||||
visit time_entries_path
|
||||
save_screenshot("/tmp/testingpoop.png")
|
||||
find('.work_entry-delete-btn').trigger('click')
|
||||
find('button.work_entry-delete-btn').trigger('click')
|
||||
click_button "Delete"
|
||||
expect(page).to have_text("Your Timesheet")
|
||||
expect(TimeEntry.count).to eql 0
|
||||
|
1277
vendor/assets/javascripts/jquery.form.js
vendored
1277
vendor/assets/javascripts/jquery.form.js
vendored
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user