├── .gitignore
├── .rspec
├── .travis.yml
├── Dockerfile
├── Gemfile
├── Gemfile.lock
├── LICENSE
├── Procfile
├── README.md
├── Rakefile
├── app
├── assets
│ ├── images
│ │ ├── .keep
│ │ ├── intro-bug.jpg
│ │ ├── intro-mantis.jpg
│ │ └── intro-town.jpg
│ ├── javascripts
│ │ ├── application.js
│ │ ├── controllers
│ │ │ ├── admin
│ │ │ │ ├── projects.js.coffee
│ │ │ │ └── users.js.coffee
│ │ │ ├── home.js.coffee
│ │ │ ├── members.js.coffee
│ │ │ ├── project_dashboard.js.coffee
│ │ │ ├── projects.js.coffee
│ │ │ ├── stats.js.coffee
│ │ │ ├── tasks.js.coffee
│ │ │ └── timesheets.js.coffee
│ │ ├── devise.js
│ │ ├── dispatcher.js.coffee
│ │ ├── keybindings.js.coffee
│ │ ├── models
│ │ │ ├── projects.js.coffee
│ │ │ ├── tasks.js.coffee
│ │ │ └── users.js.coffee
│ │ ├── shared
│ │ │ ├── api.js.coffee
│ │ │ ├── assignee_editor.js.coffee
│ │ │ ├── common.js.coffee
│ │ │ ├── duration.js.coffee
│ │ │ ├── duration_editor.js.coffee
│ │ │ └── projects_helper.js.coffee
│ │ └── views
│ │ │ ├── profiles.js.coffee
│ │ │ ├── projects.js.coffee
│ │ │ ├── tasks.js.coffee
│ │ │ └── users.js.coffee
│ └── stylesheets
│ │ ├── application.scss
│ │ ├── common.scss
│ │ ├── devise.scss
│ │ ├── home.css.scss
│ │ ├── morris.css
│ │ └── users.css.scss
├── controllers
│ ├── admin
│ │ ├── admin_controller.rb
│ │ ├── projects_controller.rb
│ │ └── users_controller.rb
│ ├── application_controller.rb
│ ├── concerns
│ │ └── .keep
│ ├── errors_controller.rb
│ ├── home_controller.rb
│ ├── profiles
│ │ ├── avatars_controller.rb
│ │ └── passwords_controller.rb
│ ├── profiles_controller.rb
│ ├── projects
│ │ ├── items_controller.rb
│ │ ├── members_controller.rb
│ │ ├── stats_controller.rb
│ │ ├── tasks_controller.rb
│ │ └── timesheets_controller.rb
│ ├── projects_controller.rb
│ ├── users
│ │ └── omniauth_callbacks_controller.rb
│ └── users_controller.rb
├── helpers
│ ├── admin
│ │ ├── projects_helper.rb
│ │ └── users_helper.rb
│ ├── application_helper.rb
│ ├── devise_helper.rb
│ ├── errors_helper.rb
│ ├── home_helper.rb
│ ├── profiles
│ │ ├── avatars_helper.rb
│ │ └── passwords_helper.rb
│ ├── profiles_helper.rb
│ ├── projects
│ │ ├── members_helper.rb
│ │ ├── stats_helper.rb
│ │ ├── tasks_helper.rb
│ │ └── timesheets_helper.rb
│ └── projects_helper.rb
├── mailers
│ └── .keep
├── models
│ ├── ability.rb
│ ├── concerns
│ │ └── internal_id.rb
│ ├── identity.rb
│ ├── project.rb
│ ├── project_member.rb
│ ├── project_opening.rb
│ ├── project_snapshot.rb
│ ├── task.rb
│ ├── timesheet_task.rb
│ ├── user.rb
│ ├── work_log.rb
│ └── work_week.rb
└── views
│ ├── admin
│ ├── projects
│ │ └── index.html.slim
│ └── users
│ │ ├── _form.html.slim
│ │ ├── _user_card.html.slim
│ │ ├── edit.html.slim
│ │ ├── index.html.slim
│ │ └── new.html.slim
│ ├── devise
│ ├── confirmations
│ │ └── new.html.erb
│ ├── mailer
│ │ ├── confirmation_instructions.html.erb
│ │ ├── reset_password_instructions.html.erb
│ │ └── unlock_instructions.html.erb
│ ├── passwords
│ │ ├── edit.html.slim
│ │ └── new.html.slim
│ ├── registrations
│ │ ├── edit.html.erb
│ │ └── new.html.slim
│ ├── sessions
│ │ └── new.html.slim
│ ├── shared
│ │ ├── _links.html.slim
│ │ └── _social.html.slim
│ └── unlocks
│ │ └── new.html.erb
│ ├── errors
│ ├── internal_error.html.slim
│ └── not_found.html.slim
│ ├── home
│ ├── _work_week_table.html.slim
│ ├── index.html.slim
│ └── work_for_period.js.erb
│ ├── layouts
│ ├── _flash.html.slim
│ ├── _footer_panel.html.slim
│ ├── _head.html.slim
│ ├── _header_panel.html.slim
│ ├── _sidebar.html.slim
│ ├── application.html.slim
│ ├── devise.html.slim
│ └── project.html.slim
│ ├── profiles
│ ├── edit.html.slim
│ ├── passwords
│ │ └── edit.html.slim
│ └── show.html.slim
│ ├── projects
│ ├── _form.html.slim
│ ├── _progress.html.slim
│ ├── _project_card.html.slim
│ ├── _projects_grid.html.slim
│ ├── edit.html.slim
│ ├── export.xlsx.axlsx
│ ├── index.html.slim
│ ├── members
│ │ ├── index.html.slim
│ │ └── new.html.slim
│ ├── new.html.slim
│ ├── show.html.slim
│ ├── show_export.html.slim
│ ├── stats
│ │ ├── _total.html.slim
│ │ ├── index.html.slim
│ │ └── show.html.slim
│ ├── tasks
│ │ ├── _task_card.html.slim
│ │ ├── _task_dlg.html.slim
│ │ └── index.html.slim
│ └── timesheets
│ │ └── edit.html.slim
│ ├── shared
│ ├── _callout_danger.html.slim
│ ├── _callout_info.html.slim
│ ├── _loading_spinner.html.slim
│ ├── _project_card.html.slim
│ ├── _projects_box.html.slim
│ └── _projects_summary.html.slim
│ └── users
│ └── avatar.text.slim
├── bin
├── bundle
├── rails
└── rake
├── config.ru
├── config
├── application.rb
├── boot.rb
├── database.yml
├── environment.rb
├── environments
│ ├── development.rb
│ ├── production.rb
│ └── test.rb
├── initializers
│ ├── backtrace_silencers.rb
│ ├── devise.rb
│ ├── filter_parameter_logging.rb
│ ├── inflections.rb
│ ├── mime_types.rb
│ ├── omniauth.rb
│ ├── secret_token.rb
│ ├── session_store.rb
│ ├── state_machine_hack.rb
│ └── wrap_parameters.rb
├── locales
│ ├── devise.en.yml
│ └── en.yml
├── puma.rb
├── routes.rb
└── schedule.rb
├── db
├── migrate
│ ├── 20140426173644_devise_create_users.rb
│ ├── 20140428100928_add_name_and_bio_to_user.rb
│ ├── 20140428102407_add_avatar_to_user.rb
│ ├── 20140428131010_add_admin_to_user.rb
│ ├── 20140504095518_create_projects.rb
│ ├── 20140504095531_create_project_members.rb
│ ├── 20140506134955_add_authentication_token_to_users.rb
│ ├── 20140507095311_create_tasks.rb
│ ├── 20140507105253_create_work_logs.rb
│ ├── 20140617153407_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb
│ ├── 20140617153408_add_missing_unique_indices.acts_as_taggable_on_engine.rb
│ ├── 20140617153409_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb
│ ├── 20140804195208_add_state_to_project.rb
│ ├── 20140804213245_create_versions.rb
│ ├── 20140810120406_create_project_snapshots.rb
│ ├── 20140821204235_create_project_openings.rb
│ ├── 20140824133531_change_times_to_seconds.rb
│ ├── 20140923195808_create_identities.rb
│ ├── 20141031210039_add_internal_ids_to_tasks.rb
│ └── 20141031223050_change_times_to_minutes.rb
├── schema.rb
└── seeds.rb
├── lib
├── api
│ ├── api.rb
│ ├── api_helpers.rb
│ ├── entities.rb
│ ├── project_members.rb
│ ├── project_snapshots.rb
│ ├── projects.rb
│ ├── tasks.rb
│ ├── timesheets.rb
│ ├── users.rb
│ └── work_logs.rb
├── assets
│ └── .keep
├── chronic_duration.rb
└── tasks
│ ├── .keep
│ └── migrate_iids.rake
├── log
└── .keep
├── public
├── 422.html
├── attachments
│ ├── medium
│ │ └── missing.png
│ └── original
│ │ └── missing.png
├── favicon.ico
├── fonts
│ └── bootstrap
│ │ ├── glyphicons-halflings-regular.eot
│ │ ├── glyphicons-halflings-regular.svg
│ │ ├── glyphicons-halflings-regular.ttf
│ │ └── glyphicons-halflings-regular.woff
└── robots.txt
├── spec
├── factories.rb
├── models
│ ├── project_member_spec.rb
│ ├── project_opening_spec.rb
│ ├── project_spec.rb
│ └── user_spec.rb
├── rails_helper.rb
└── spec_helper.rb
└── vendor
└── assets
├── javascripts
├── .keep
├── admin_lte.js
├── bootstrap-datepicker.js
├── handsontable.full.js
├── holder.js
├── jquery.easing.min.js
├── jquery.flip.js
├── jquery.flot.categories.min.js
├── jquery.flot.min.js
├── jquery.flot.resize.min.js
├── jquery.flot.stack.min.js
├── jquery.flot.tooltip.min.js
├── jquery.knob.js
├── morris.min.js
├── raphael.min.js
└── select2-editor.js
└── stylesheets
├── .keep
├── admin_lte.css
├── datepicker.css
└── handsontable.full.css
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files.
2 | #
3 | # If you find yourself ignoring temporary files generated by your text editor
4 | # or operating system, you probably want to add a global ignore instead:
5 | # git config --global core.excludesfile '~/.gitignore_global'
6 |
7 | .DS_Store
8 | .c9
9 | .secret
10 | .bundle
11 |
12 | config/application.yml
13 |
14 | # Ignore the default SQLite database.
15 | /db/*.sqlite3
16 | /db/*.sqlite3-journal
17 |
18 | # Ignore all logfiles and tempfiles.
19 | /log/*.log
20 | /tmp
21 |
22 | # Ignore uploaded files
23 | /public/system
24 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --require spec_helper
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | rvm:
3 | - 2.1.0
4 |
5 | before_script:
6 | - bundle exec rake db:migrate
7 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ubuntu:16.04
2 |
3 | ENV DEBIAN_FRONTEND noninteractive
4 | ENV INITRD No
5 |
6 | ### Build packages
7 |
8 | RUN set -ex; \
9 | apt-get update; \
10 | apt-get install -y \
11 | git-core \
12 | curl \
13 | zlib1g-dev \
14 | build-essential \
15 | libssl-dev \
16 | libreadline-dev \
17 | libyaml-dev \
18 | libsqlite3-dev \
19 | sqlite3 \
20 | libxml2-dev \
21 | libxslt1-dev \
22 | libcurl4-openssl-dev \
23 | python-software-properties \
24 | libffi-dev
25 |
26 | ### Install rbenv
27 | RUN git clone https://github.com/sstephenson/rbenv.git /usr/local/rbenv
28 | RUN echo '# rbenv setup' > /etc/profile.d/rbenv.sh
29 | RUN echo 'export RBENV_ROOT=/usr/local/rbenv' >> /etc/profile.d/rbenv.sh
30 | RUN echo 'export PATH="$RBENV_ROOT/bin:$PATH"' >> /etc/profile.d/rbenv.sh
31 | RUN echo 'eval "$(rbenv init -)"' >> /etc/profile.d/rbenv.sh
32 | RUN chmod +x /etc/profile.d/rbenv.sh
33 |
34 | # install ruby-build
35 | RUN mkdir /usr/local/rbenv/plugins
36 | RUN git clone https://github.com/sstephenson/ruby-build.git /usr/local/rbenv/plugins/ruby-build
37 |
38 | ENV RBENV_ROOT /usr/local/rbenv
39 | ENV PATH $RBENV_ROOT/bin:$RBENV_ROOT/shims:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
40 |
41 | RUN rbenv install -v 2.4.1
42 | RUN rbenv global 2.4.1
43 |
44 | RUN ruby -v
45 | RUN echo "gem: --no-document" > ~/.gemrc
46 | RUN gem install bundler
47 |
48 | ### Install node
49 |
50 | # gpg keys listed at https://github.com/nodejs/node
51 | RUN set -ex \
52 | && for key in \
53 | 94AE36675C464D64BAFA68DD7434390BDBE9B9C5 \
54 | FD3A5288F042B6850C66B31F09FE44734EB7990E \
55 | 71DCFD284A79C3B38668286BC97EC7A07EDE3FC1 \
56 | DD8F2338BAE7501E3DD5AC78C273792F7D83545D \
57 | C4F0DFFF4E8C1A8236409D08E73BC641CC11F4C8 \
58 | B9AE9905FFD7803F25714661B63B535A4C206CA9 \
59 | 56730D5401028683275BD23C23EFEFE93C4CFFFE \
60 | 77984A986EBC2AA786BC0F66B01FBB92821C587A \
61 | ; do \
62 | gpg --keyserver pgp.mit.edu --recv-keys "$key" || \
63 | gpg --keyserver keyserver.pgp.com --recv-keys "$key" || \
64 | gpg --keyserver ha.pool.sks-keyservers.net --recv-keys "$key" ; \
65 | done
66 |
67 | ENV NPM_CONFIG_LOGLEVEL info
68 | ENV NODE_VERSION 8.9.0
69 |
70 | RUN buildDeps='xz-utils curl ca-certificates' \
71 | && set -x \
72 | && apt-get update && apt-get install -y $buildDeps --no-install-recommends \
73 | && rm -rf /var/lib/apt/lists/* \
74 | && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
75 | && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
76 | && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
77 | && grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \
78 | && tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \
79 | && rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \
80 | && apt-get purge -y --auto-remove $buildDeps
81 |
82 |
83 | RUN apt-get update -qq && apt-get upgrade -y && apt-get install -y \
84 | ca-certificates \
85 | tzdata \
86 | libsqlite3-dev \
87 | libmysqlclient-dev \
88 | mysql-client \
89 | libfontconfig1 \
90 | libfontconfig1-dev \
91 | libicu-dev \
92 | libfreetype6 \
93 | libfreetype6-dev \
94 | libssl-dev \
95 | libxft-dev \
96 | libpng-dev \
97 | libjpeg-dev
98 |
99 | # for paperclip image manipulation
100 | RUN apt-get install -y file imagemagick
101 |
102 | # for nokogiri
103 | RUN apt-get install -y libxml2-dev libxslt1-dev
104 |
105 | ENV APP_HOME /myapp
106 | RUN mkdir $APP_HOME
107 | WORKDIR $APP_HOME
108 |
109 | # Gems
110 | COPY Gemfile Gemfile
111 | COPY Gemfile.lock Gemfile.lock
112 | RUN gem install bundler
113 | RUN gem install foreman
114 | RUN bundle install --without test
115 | RUN rbenv rehash
116 |
117 | # All app
118 | COPY . $APP_HOME
119 |
120 | # Install cron and start
121 | RUN apt-get install -y cron
122 | RUN touch /var/log/cron.log
123 |
124 | # Update the crontab
125 | RUN whenever -w
126 |
127 | EXPOSE 3000
128 | CMD ["foreman", "start"]
129 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
4 | gem 'rails', '4.2.10'
5 |
6 | # Use sqlite3 as the database for Active Record
7 | gem 'sqlite3'
8 | gem 'mysql2'
9 |
10 | # settings
11 | gem 'figaro'
12 |
13 | # Use SCSS for stylesheets
14 | gem 'sass-rails', '~> 4.0.2'
15 | gem 'uglifier', '>= 1.3.0'
16 | gem 'coffee-rails', '~> 4.0.0'
17 | gem 'select2-rails'
18 | gem 'bootstrap-sass'
19 | gem 'font-awesome-rails'
20 |
21 | # Use jquery as the JavaScript library
22 | gem 'jquery-rails'
23 | gem 'momentjs-rails'
24 | gem 'bootstrap-daterangepicker-rails'
25 |
26 | # Auth
27 | gem 'devise'
28 | gem 'six'
29 | gem 'omniauth'
30 | gem 'omniauth-facebook'
31 | gem 'omniauth-google-oauth2'
32 | gem 'omniauth-github'
33 |
34 | # Pagination
35 | gem 'kaminari'
36 |
37 | # Template engine
38 | gem 'slim'
39 |
40 | # breadcrumbs on pages
41 | gem 'breadcrumbs_on_rails'
42 |
43 | # tags on tasks
44 | gem 'acts-as-taggable-on'
45 |
46 | # Files attachments
47 | gem 'paperclip'
48 | gem 'paperclip-meta'
49 |
50 | # Versioning
51 | gem 'paper_trail', '~> 3.0.5'
52 |
53 | # State machine
54 | gem 'state_machine'
55 |
56 | # Date manipulation and number
57 | gem 'chronic'
58 | gem 'numerizer'
59 |
60 | # Cron jobs
61 | gem 'whenever', :require => false
62 |
63 | # xslsx exports
64 | gem 'axlsx_rails'
65 |
66 | # Keyboard shortcuts
67 | gem 'mousetrap-rails'
68 |
69 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
70 | gem 'grape', '~> 0.10.1'
71 | gem 'grape-entity', '~> 0.4.4'
72 | gem 'gon'
73 |
74 | gem 'tzinfo-data', platforms: [:mingw, :mswin]
75 |
76 |
77 | group :test, :development do
78 | gem 'rspec-rails'
79 | gem 'capybara'
80 | gem 'faker'
81 | gem 'minitest', '~> 5.3.0'
82 | gem 'rack-mini-profiler'
83 | gem 'bullet'
84 | gem 'quiet_assets'
85 | end
86 |
87 | group :test do
88 | gem 'simplecov', require: false
89 | gem 'shoulda-matchers', require: false
90 | end
91 |
92 | # Use puma as the app server
93 | gem 'foreman'
94 | gem 'puma'
95 |
96 | gem 'annotate', '~> 2.6.5', group: :development
97 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 "Frederic Rocher"
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: rails s -b 0.0.0.0
2 | cron: cron && tail -f /var/log/cron.log
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## SoFreakingBoring
2 |
3 | ### What is SoFreakingBoring ?
4 |
5 | SoFreakingBoring is a free and simple project management and time tracking application. It allows you to easily create your own projects and tasks, invite people and start working together.
6 |
7 | 
8 |
9 | ### Requirements
10 |
11 | * Ruby 2.0+
12 | * MySQL for production
13 |
14 | ### Installation
15 |
16 | Before starting SoFreakingBoring you need to follow these steps:
17 |
18 | * migrate database with 'rake db:migrate'
19 | * start in development mode with 'rails s'
20 |
21 | The Whenever gem is used for cron jobs. To make it work:
22 | * Enter 'whenever' to see what would be added to your cron tab
23 | * Enter 'whenever -w' to add jobs to your crontab.
24 |
25 |
26 | Note: SoFreakingBoring works on Linux and Mac OS X. It has not been tested on Windows and the whenever should not work. If you work on Windows you should consider [Vagrant](https://www.vagrantup.com/).
27 |
28 | ## Configuration
29 |
30 | This project uses the following environment variables.
31 |
32 | ### Mandatory configuration
33 |
34 | | Name | Default Value | Description |
35 | | ----- | ------------- | ------------ |
36 | | DEVISE_SECRET_KEY | none | You must generate a Devise key
37 | | RAILS_ENV | development | Don't forget to switch it to production |
38 | | PORT | 3000 | Puma server port |
39 | | SECRET_TOKEN | none | Rails needs a secret token |
40 |
41 |
42 | ### MySQL Configuration
43 |
44 | | Name | Default Value | Description |
45 | | --------|:---------:| -----|
46 | | DB_HOST | localhost | Database host server |
47 | | DB_NAME | sofreaking | Database name |
48 | | DB_PASSWORD | | User password |
49 | | DB_PORT | 3306 | Database port |
50 | | DB_USERNAME | root | User name used to log in |
51 |
52 | ### External services and mail configuration
53 |
54 | | Name | Default Value | Description |
55 | | --------|:---------:| -----|
56 | | GOOGLE_ANALYTICS_KEY | none | if you want to track usage statistics |
57 | | GRAVATAR_HTTPS | false | Must be true if https is used |
58 |
59 | ### OAuth Configuration
60 |
61 | | Name | Default Value | Description |
62 | | --------|:---------:| -----|
63 | | FACEBOOK_KEY | none | facebook key for omniauth |
64 | | FACEBOOK_SECRET | none | facebook secret for omniauth |
65 | | GITHUB_KEY | none | github key for omniauth |
66 | | GITHUB_SECRET | none | github secret for omniauth |
67 | | GOOGLE_KEY | none | google key for omniauth |
68 | | GOOGLE_SECRET | none | google secret for omniauth |
69 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require File.expand_path('../config/application', __FILE__)
5 |
6 | Olb::Application.load_tasks
7 |
--------------------------------------------------------------------------------
/app/assets/images/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frocher/sofreakingboring/37ccfa946e3764652cc552f644e0d9affbcaa4a3/app/assets/images/.keep
--------------------------------------------------------------------------------
/app/assets/images/intro-bug.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frocher/sofreakingboring/37ccfa946e3764652cc552f644e0d9affbcaa4a3/app/assets/images/intro-bug.jpg
--------------------------------------------------------------------------------
/app/assets/images/intro-mantis.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frocher/sofreakingboring/37ccfa946e3764652cc552f644e0d9affbcaa4a3/app/assets/images/intro-mantis.jpg
--------------------------------------------------------------------------------
/app/assets/images/intro-town.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frocher/sofreakingboring/37ccfa946e3764652cc552f644e0d9affbcaa4a3/app/assets/images/intro-town.jpg
--------------------------------------------------------------------------------
/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js, which will include all the files
2 | // listed below.
3 | //
4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6 | //
7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8 | // compiled file.
9 | //
10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
11 | // about supported directives.
12 | //
13 | //= require jquery
14 | //= require jquery_ujs
15 | //= require jquery.easing.min
16 | //= require mousetrap
17 | //= require bootstrap
18 | //= require select2-full
19 | //= require admin_lte
20 | //= require jquery.flip
21 | //= require handsontable.full
22 | //= require jquery.knob
23 | //= require jquery.flot.min
24 | //= require jquery.flot.categories.min
25 | //= require jquery.flot.stack.min
26 | //= require jquery.flot.tooltip.min
27 | //= require jquery.flot.resize.min
28 | //= require bootstrap-datepicker
29 | //= require moment
30 | //= require daterangepicker
31 | //= require raphael.min
32 | //= require morris.min
33 | //= require holder
34 |
35 | //= require devise
36 | //= require keybindings
37 | //= require dispatcher
38 |
39 | //= require controllers/admin/projects
40 | //= require controllers/admin/users
41 | //= require controllers/home
42 | //= require controllers/members
43 | //= require controllers/project_dashboard
44 | //= require controllers/projects
45 | //= require controllers/stats
46 | //= require controllers/tasks
47 | //= require controllers/timesheets
48 |
49 | //= require models/projects
50 | //= require models/tasks
51 | //= require models/users
52 |
53 | //= require shared/api
54 | //= require shared/assignee_editor
55 | //= require shared/common
56 | //= require shared/duration
57 | //= require shared/duration_editor
58 | //= require shared/projects_helper
59 |
60 | //= require views/profiles
61 | //= require views/projects
62 | //= require views/tasks
63 | //= require views/users
64 |
--------------------------------------------------------------------------------
/app/assets/javascripts/controllers/admin/projects.js.coffee:
--------------------------------------------------------------------------------
1 |
2 | @AdminProjects=
3 |
4 | init: ->
5 | @model = new ProjectsModel(true)
6 | @cardsView = new ProjectsCardsView(@model)
7 | @model.loadProjects( ->
8 | $("#loadingSpinner").hide()
9 | AdminProjects.cardsView.initialize()
10 | )
11 |
--------------------------------------------------------------------------------
/app/assets/javascripts/controllers/admin/users.js.coffee:
--------------------------------------------------------------------------------
1 | @AdminUsers=
2 |
3 | init: ->
4 | @model = new UsersModel()
5 | @cardsView = new UsersCardsView(@model)
6 | @model.loadUsers( ->
7 | $("#loadingSpinner").hide()
8 | AdminUsers.cardsView.initialize()
9 | )
--------------------------------------------------------------------------------
/app/assets/javascripts/controllers/home.js.coffee:
--------------------------------------------------------------------------------
1 | @Home=
2 |
3 | init: ->
4 | Home.initPeriod()
5 | return
6 |
7 | initPeriod: ->
8 | $('#previousPeriod').click ->
9 | periodStart = moment(gon.period_start, "YYYYMMDD")
10 | periodStart.add('d', -7)
11 | Home.updatePeriod(periodStart)
12 | return
13 |
14 | $('#nextPeriod').click ->
15 | periodStart = moment(gon.period_start, "YYYYMMDD")
16 | periodStart.add('d', 7)
17 | Home.updatePeriod(periodStart)
18 | return
19 |
20 | $('#dpPeriod').datepicker({
21 | endDate: moment(gon.period_end, "YYYYMMDD").toDate()
22 | weekStart: 1
23 | autoclose: true
24 | daysOfWeekDisabled: "0,6"
25 | }).on( "changeDate", (e) ->
26 | periodStart = moment(e.date).startOf('isoWeek')
27 | Home.updatePeriod(periodStart)
28 | return
29 | )
30 |
31 | updatePeriod: (periodStart) ->
32 | periodEnd = moment(periodStart).add('d', 6)
33 | gon.period_start = periodStart.format("YYYYMMDD")
34 | gon.period_end = periodEnd.format("YYYYMMDD")
35 | $.ajax(
36 | url: "/work_for_period"
37 | data:
38 | period_start: gon.period_start
39 | period_end: gon.period_end
40 | )
41 | period = periodStart.format("MMM DD, YYYY") + " - " + periodEnd.format("MMM DD, YYYY")
42 | $('#periodInput').val(period)
43 | return
44 |
45 |
--------------------------------------------------------------------------------
/app/assets/javascripts/controllers/members.js.coffee:
--------------------------------------------------------------------------------
1 | @Members=
2 |
3 | init: ->
4 | Members.initSelectRole()
5 |
6 | userFormatResult: (user, container, query) ->
7 | "
8 | #{user.name}"
9 | userFormatSelection: (user, container) -> user.name
10 |
11 |
12 | initSelectRole: ->
13 | $('#user-search').select2
14 | placeholder: 'Type a username'
15 | minimumInputLength: 2
16 | multiple: true
17 | query: (query) ->
18 | Api.users query.term, 20, (users) ->
19 | data =
20 | results: users
21 | text: 'name'
22 | query.callback(data)
23 | initSelection: (element, callback) ->
24 | ids = $(element).val().split(',')
25 | data = []
26 | Api.user(id, (item) ->
27 | data.push(item)
28 | callback(data) if data.length == ids.length
29 | ) for id in ids
30 | formatResult: Members.userFormatResult
31 | formatSelection: Members.userFormatSelection
32 | dropdownCssClass: 'bigdrop'
33 | escapeMarkup: (m) -> m
34 |
35 | $('.role-select').on 'change', ->
36 | $(this.form).submit()
37 |
38 |
--------------------------------------------------------------------------------
/app/assets/javascripts/controllers/project_dashboard.js.coffee:
--------------------------------------------------------------------------------
1 | @ProjectDashboard=
2 | init: ->
3 | ProjectDashboard.initCharts()
4 | ProjectDashboard.initData()
5 | $(".knob").knob()
6 |
7 | initCharts: ->
8 | ProjectDashboard.burndown = new Morris.Line({
9 | element: 'burndown-chart',
10 | resize: true,
11 | data: [],
12 | xkey: 'created_at',
13 | ykeys: ['remaining_estimate'],
14 | labels: ['Value'],
15 | dateFormat: (d) ->
16 | moment(d).format('MMMM Do YYYY')
17 | lineColors: ['#efefef'],
18 | lineWidth: 2,
19 | pointSize: 3,
20 | hideHover: 'auto',
21 | gridTextColor: "#fff",
22 | gridStrokeWidth: 0.4,
23 | pointStrokeColors: ["#efefef"],
24 | gridLineColor: "#efefef",
25 | gridTextFamily: "Open Sans",
26 | gridTextSize: 10
27 | })
28 |
29 | ProjectDashboard.budget = new Morris.Line({
30 | element: 'budget-chart',
31 | resize: true,
32 | data: [],
33 | xkey: 'created_at',
34 | ykeys: ['original_estimate', 'total'],
35 | labels: ['Original estimate', 'Target'],
36 | dateFormat: (d) ->
37 | moment(d).format('MMMM Do YYYY')
38 | lineColors: ['#ffaaaa', '#efefef'],
39 | lineWidth: 2,
40 | pointSize: 3,
41 | hideHover: 'auto',
42 | gridTextColor: "#fff",
43 | gridStrokeWidth: 0.4,
44 | pointStrokeColors: ["#efefef"],
45 | gridLineColor: "#efefef",
46 | gridTextFamily: "Open Sans",
47 | gridTextSize: 10
48 | })
49 |
50 | $.plot("#members-chart", [members_done_data, members_tbd_data], {
51 | grid: {
52 | borderWidth: 1
53 | borderColor: "#f3f3f3"
54 | tickColor: "#f3f3f3"
55 | hoverable: true
56 | }
57 | tooltip: true
58 | tooltipOpts: {
59 | content: "%x.2 day(s)"
60 | }
61 | series: {
62 | stack: true
63 | bars: {
64 | show: true
65 | barWidth: 0.5
66 | fill: 0.9
67 | horizontal: true
68 | align: "center"
69 | }
70 | }
71 | yaxis: {
72 | mode: "categories"
73 | tickLength: 0
74 | }
75 | })
76 |
77 | $.plot("#tags-chart", [tags_done_data, tags_tbd_data], {
78 | grid: {
79 | borderWidth: 1
80 | borderColor: "#f3f3f3"
81 | tickColor: "#f3f3f3"
82 | hoverable: true
83 | }
84 | tooltip: true
85 | tooltipOpts: {
86 | content: "%x.2 day(s)"
87 | }
88 | series: {
89 | stack: true
90 | bars: {
91 | show: true
92 | barWidth: 0.5
93 | fill: 0.9
94 | horizontal: true
95 | align: "center"
96 | }
97 | }
98 | yaxis: {
99 | mode: "categories"
100 | tickLength: 0
101 | }
102 | })
103 |
104 |
105 | initData: ->
106 | Api.project_snapshots gon.project_id, (shots) ->
107 | toDay = 60 * 8
108 | for shot in shots
109 | shot.original_estimate /= toDay
110 | shot.work_logged /= toDay
111 | shot.remaining_estimate /= toDay
112 | shot.delta /= toDay
113 | shot.total = shot.work_logged + shot.remaining_estimate
114 |
115 | shot.original_estimate = Math.round(shot.original_estimate * 100) / 100
116 | shot.work_logged = Math.round(shot.work_logged * 100) / 100
117 | shot.remaining_estimate = Math.round(shot.remaining_estimate * 100) / 100
118 | shot.delta = Math.round(shot.delta * 100) / 100
119 | shot.total = Math.round(shot.total * 100) / 100
120 |
121 |
122 |
123 | ProjectDashboard.burndown.setData(shots)
124 | ProjectDashboard.budget.setData(shots)
125 |
126 |
--------------------------------------------------------------------------------
/app/assets/javascripts/controllers/projects.js.coffee:
--------------------------------------------------------------------------------
1 | @Projects=
2 |
3 | init: ->
4 | @model = new ProjectsModel(false)
5 | @cardsView = new ProjectsCardsView(@model)
6 | @model.loadProjects( ->
7 | $("#loadingSpinner").hide()
8 | Projects.cardsView.initialize()
9 | )
10 |
--------------------------------------------------------------------------------
/app/assets/javascripts/controllers/stats.js.coffee:
--------------------------------------------------------------------------------
1 | @Stats=
2 |
3 | init: ->
4 | $('#periodPicker').daterangepicker(
5 | {
6 | format: 'MMM DD, YYYY'
7 | }
8 | (start, end, label) ->
9 | $('#loadingSpinner').show()
10 | url = "/projects/#{gon.project_id}/stats/show"
11 | $.ajax(
12 | url: url
13 | data:
14 | start: start.format('YYYYMMDD')
15 | end: end.format('YYYYMMDD')
16 | ).done (html) ->
17 | $('#loadingSpinner').hide()
18 | $('#results').html(html)
19 | $('.avatar').tooltip({container: 'body'})
20 | )
21 |
--------------------------------------------------------------------------------
/app/assets/javascripts/devise.js:
--------------------------------------------------------------------------------
1 | // jQuery to collapse the navbar on scroll
2 | $(window).scroll(function() {
3 | if ($(".navbar").offset().top > 50) {
4 | $(".navbar-fixed-top").addClass("top-nav-collapse");
5 | } else {
6 | $(".navbar-fixed-top").removeClass("top-nav-collapse");
7 | }
8 | });
9 |
10 | // jQuery for page scrolling feature - requires jQuery Easing plugin
11 | $(function() {
12 | $('a.page-scroll').bind('click', function(event) {
13 | var $anchor = $(this);
14 | $('html, body').stop().animate({
15 | scrollTop: $($anchor.attr('href')).offset().top
16 | }, 1500, 'easeInOutExpo');
17 | event.preventDefault();
18 | });
19 | });
--------------------------------------------------------------------------------
/app/assets/javascripts/dispatcher.js.coffee:
--------------------------------------------------------------------------------
1 | $ ->
2 | new Dispatcher()
3 |
4 | class Dispatcher
5 |
6 | constructor: ->
7 | @initFlash()
8 | @initPageScripts()
9 |
10 | initPageScripts: ->
11 | page = $('body').attr('data-page')
12 |
13 | unless page
14 | return false
15 |
16 | switch page
17 | when 'admin:users:index'
18 | AdminUsers.init()
19 | when 'admin:projects:index'
20 | AdminProjects.init()
21 | when 'home:index'
22 | Home.init()
23 | when 'projects:index'
24 | Projects.init()
25 | when 'projects:show'
26 | ProjectDashboard.init()
27 | when 'projects:tasks:index'
28 | Tasks.init()
29 | when 'projects:members:index', 'projects:members:new'
30 | Members.init()
31 | when 'projects:stats:index'
32 | Stats.init()
33 | when 'projects:timesheets:edit'
34 | Timesheet.init()
35 |
36 | initFlash: ->
37 | flash = $(".flash-container")
38 | if flash.length > 0
39 | flash.click -> $(@).fadeOut()
40 | flash.show()
41 | setTimeout (-> flash.fadeOut()), 5000
--------------------------------------------------------------------------------
/app/assets/javascripts/keybindings.js.coffee:
--------------------------------------------------------------------------------
1 | $ ->
2 | handleKeyBindings()
3 |
4 | $(document).on 'page:change', ->
5 | handleKeyBindings()
6 |
7 | handleKeyBindings = ->
8 | # As turbolinks does not refresh the page, some old keybindings could be still present. Therefore a reset is required.
9 | Mousetrap.reset()
10 |
11 | # Hotkey binding to links with 'data-keybinding' attribute
12 | # Navigate link when hotkey pressed
13 | $('a[data-keybinding]').each (i, el) ->
14 | bindedKey = $(el).data('keybinding')
15 | bindedKey = bindedKey.toString() if typeof(bindedKey) == 'number'
16 | Mousetrap.bind bindedKey, (e) ->
17 | if typeof(Turbolinks) == 'undefined'
18 | # Emulate click if turbolinks defined
19 | el.click()
20 | else
21 | # Use turbolinks to go to URL
22 | Turbolinks.visit(el.href)
23 |
24 | # Hotkey binding to inputs with 'data-keybinding' attribute
25 | # Focus input when hotkey pressed
26 | $('input[data-keybinding]').each (i, el) ->
27 | Mousetrap.bind $(el).data('keybinding'), (e) ->
28 | el.focus()
29 | if e.preventDefault
30 | e.preventDefault()
31 | else
32 | e.returnValue = false
33 |
34 | # Toggle show/hide hotkey hints
35 | window.mouseTrapRails =
36 | showOnLoad: false # Show/hide hotkey hints by default (on page load). Mostly for debugging purposes.
37 | toggleKeys: 'alt+shift+h' # Keys combo to toggle hints visibility.
38 | keysShown: false # State of hotkey hints
39 | toggleHints: ->
40 | $('a[data-keybinding]').each (i, el) ->
41 | $el = $(el)
42 | if mouseTrapRails.keysShown
43 | $el.removeClass('mt-hotkey-el').find('.mt-hotkey-hint').remove()
44 | else
45 | mtKey = $el.data('keybinding')
46 | $hint = "#{mtKey} "
47 | $el.addClass('mt-hotkey-el') unless $el.css('position') is 'absolute'
48 | $el.append $hint
49 | @keysShown ^= true
50 |
51 | Mousetrap.bind mouseTrapRails.toggleKeys, -> mouseTrapRails.toggleHints()
52 |
53 | mouseTrapRails.toggleHints() if mouseTrapRails.showOnLoad
54 |
55 |
--------------------------------------------------------------------------------
/app/assets/javascripts/models/projects.js.coffee:
--------------------------------------------------------------------------------
1 | class @ProjectsModel
2 | constructor: (@isAdmin) ->
3 | @projects = []
4 | @subscribers = []
5 |
6 | subscribe: (callback) ->
7 | @subscribers.push callback
8 |
9 | unsubscribe: (callback) ->
10 | @subscribers = @subscribers.filter (item) -> item isnt callback
11 |
12 | notify: ->
13 | subscriber() for subscriber in @subscribers
14 |
15 | getProject: (id) ->
16 | found = @projects.where id:id
17 | if found.length > 0 then found[0] else null
18 |
19 | getProjects: (filter = '', sort = '', order = 'asc', displayClosed = false) ->
20 | projects = @filterProjects(@projects, filter, displayClosed)
21 | projects = @sortProjects(projects, sort, order) if sort.length > 0
22 | projects
23 |
24 | filterProjects: (projects, filter = '', displayClosed = false) ->
25 | data = projects
26 | if filter.length > 0
27 | filter = filter.toLowerCase()
28 | filtered = []
29 | for project in data
30 | addIt = project.name.toLowerCase().indexOf(filter) > -1
31 |
32 | if !addIt
33 | addIt = project.code.toLowerCase().indexOf(filter) > -1
34 | if !addIt
35 | addIt = project.description.toLowerCase().indexOf(filter) > -1
36 |
37 | filtered.push(project) if addIt
38 |
39 | data = filtered
40 |
41 | if !displayClosed
42 | filtered = []
43 | for project in data
44 | filtered.push(project) if project.state != 'closed'
45 | data = filtered
46 |
47 | data
48 |
49 | sortProjects: (projects, key, order) ->
50 | projects.sort (a,b) ->
51 | m = if order == 'asc' then 1 else -1
52 | return -1 * m if a[key] < b[key]
53 | return +1 * m if a[key] > b[key]
54 | return 0
55 |
56 | loadProjects: (callback) ->
57 | Api.projects @isAdmin, 10000, (projects) =>
58 | @projects = projects
59 | @notify()
60 | callback()
61 |
62 | loadMembers: (project, callback) ->
63 | Api.project_members project.id, (members) ->
64 | project.members = members
65 | callback()
66 |
67 |
--------------------------------------------------------------------------------
/app/assets/javascripts/models/users.js.coffee:
--------------------------------------------------------------------------------
1 | class @UsersModel
2 | constructor: ->
3 | @users = []
4 | @subscribers = []
5 |
6 | subscribe: (callback) ->
7 | @subscribers.push callback
8 |
9 | unsubscribe: (callback) ->
10 | @subscribers = @subscribers.filter (item) -> item isnt callback
11 |
12 | notify: ->
13 | subscriber() for subscriber in @subscribers
14 |
15 | getUsers: (filter = '', sort = '', order = 'asc') ->
16 | users = @filterUsers(@users, filter)
17 | users = @sortUsers(users, sort, order) if sort.length > 0
18 | users
19 |
20 | filterUsers: (users, filter = '') ->
21 | data = users
22 | if filter.length > 0
23 | filter = filter.toLowerCase()
24 | filtered = []
25 | for user in data
26 | addIt = user.email.toLowerCase().indexOf(filter) > -1
27 |
28 | if !addIt
29 | addIt = user.name.toLowerCase().indexOf(filter) > -1
30 |
31 | filtered.push(user) if addIt
32 |
33 | data = filtered
34 |
35 | data
36 |
37 | sortUsers: (users, key, order) ->
38 | users.sort (a,b) ->
39 | m = if order == 'asc' then 1 else -1
40 | return -1 * m if a[key] < b[key]
41 | return +1 * m if a[key] > b[key]
42 | return 0
43 |
44 | loadUsers: (callback) ->
45 | Api.users '', 10000, (users) =>
46 | @users = users
47 | @notify()
48 | callback()
49 |
--------------------------------------------------------------------------------
/app/assets/javascripts/shared/assignee_editor.js.coffee:
--------------------------------------------------------------------------------
1 |
2 | class AssigneeEditor extends Handsontable.editors.SelectEditor
3 |
4 |
5 | setValue: (member) ->
6 | newValue = "0"
7 | newValue = member.user_id if member != ""
8 | this.select.value = newValue
9 |
10 | prepare: ->
11 | Handsontable.editors.BaseEditor.prototype.prepare.apply(this, arguments);
12 |
13 | options = this.cellProperties.selectOptions;
14 | Handsontable.Dom.empty(this.select)
15 | for option in options
16 | optionElement = document.createElement('OPTION')
17 | optionElement.value = option.user_id
18 | Handsontable.Dom.fastInnerHTML(optionElement, option.username)
19 | this.select.appendChild(optionElement)
20 |
21 |
22 |
23 | Handsontable.editors.AssigneeEditor = AssigneeEditor
24 | Handsontable.editors.registerEditor('assignee', AssigneeEditor)
25 |
--------------------------------------------------------------------------------
/app/assets/javascripts/shared/common.js.coffee:
--------------------------------------------------------------------------------
1 | Array::where = (query) ->
2 | return [] if typeof query isnt "object"
3 | hit = Object.keys(query).length
4 | @filter (item) ->
5 | match = 0
6 | for key, val of query
7 | match += 1 if item[key] is val
8 | if match is hit then true else false
9 |
10 |
11 | $ ->
12 | $(".knob.duration").knob({
13 | })
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/assets/javascripts/shared/duration_editor.js.coffee:
--------------------------------------------------------------------------------
1 |
2 | class DurationEditor extends Handsontable.editors.TextEditor
3 |
4 | getValue: (duration) ->
5 | try
6 | return Duration.parse(this.TEXTAREA.value)
7 | catch error
8 | return "error"
9 |
10 | setValue: (duration) ->
11 | try
12 | this.TEXTAREA.value = Duration.stringify(duration, {format: 'micro'})
13 | catch error
14 | this.TEXTAREA.value = ""
15 |
16 |
17 | Handsontable.editors.DurationEditor = DurationEditor
18 | Handsontable.editors.registerEditor('duration', DurationEditor)
19 |
--------------------------------------------------------------------------------
/app/assets/javascripts/shared/projects_helper.js.coffee:
--------------------------------------------------------------------------------
1 | $ ->
2 | $('.js-choose-project-picture-button').bind "click", ->
3 | form = $(this).closest("form")
4 | form.find(".js-project-picture-input").click()
5 |
6 | $('.js-project-picture-input').bind "change", ->
7 | form = $(this).closest("form")
8 | filename = $(this).val().replace(/^.*[\\\/]/, '')
9 | form.find(".js-picture-filename").text(filename)
10 |
11 | @ProjectsHelper=
12 |
13 | getTagColor: (tag) ->
14 | tag = $.trim(tag)
15 |
16 | index = 0
17 | for i in [0..tag.length-1]
18 | index += tag.charCodeAt(i)
19 | index = index % 14
20 |
21 | switch index
22 | when 0 then labelClass = 'red'
23 | when 1 then labelClass = 'yellow'
24 | when 2 then labelClass = 'aqua'
25 | when 3 then labelClass = 'blue'
26 | when 4 then labelClass = 'light-blue'
27 | when 5 then labelClass = 'green'
28 | when 6 then labelClass = 'navy'
29 | when 7 then labelClass = 'teal'
30 | when 8 then labelClass = 'olive'
31 | when 9 then labelClass = 'lime'
32 | when 10 then labelClass = 'orange'
33 | when 11 then labelClass = 'fuchsia'
34 | when 12 then labelClass = 'purple'
35 | when 13 then labelClass = 'maroon'
36 | when 14 then labelClass = 'back'
37 | labelClass
38 |
39 | tagsRenderer: (instance, td, row, col, prop, value, cellProperties) ->
40 | if value? and value != ''
41 | tags = value.split(',')
42 | value = ''
43 | for tag in tags
44 | labelClass = ProjectsHelper.getTagColor(tag)
45 | value += "#{tag} "
46 | escaped = Handsontable.helper.stringify(value)
47 | td.innerHTML = escaped
48 | else
49 | Handsontable.renderers.TextRenderer.apply(this, arguments)
50 |
51 | deltaRenderer: (instance, td, row, col, prop, value, cellProperties) ->
52 | try
53 | if value < 0
54 | $(td).css({
55 | color: 'red'
56 | })
57 | else if value > 0
58 | $(td).css({
59 | color: 'green'
60 | })
61 | $(td).css({
62 | 'text-align': 'right'
63 | })
64 | value = Duration.stringify(value, {format: 'micro'})
65 | catch error
66 | value = "error"
67 | Handsontable.renderers.TextRenderer.apply(this, arguments)
68 |
69 | integerValidator: (value, callback) ->
70 | if value == null
71 | value = ''
72 | callback(/^\d*$/.test(value))
73 |
74 | durationRenderer: (instance, td, row, col, prop, value, cellProperties) ->
75 | if value != 0 and value != "0"
76 | try
77 | value = Duration.stringify(value, {format: 'micro'})
78 | catch error
79 | value = "error"
80 | Handsontable.renderers.TextRenderer.apply(this, arguments)
81 |
82 | durationValidator: (value, callback) ->
83 | try
84 | value = Duration.parse(value)
85 | callback(true)
86 | catch error
87 | callback(false)
88 |
--------------------------------------------------------------------------------
/app/assets/javascripts/views/profiles.js.coffee:
--------------------------------------------------------------------------------
1 | $ ->
2 | $('.js-choose-user-avatar-button').bind "click", ->
3 | form = $(this).closest("form")
4 | form.find(".js-user-avatar-input").click()
5 |
6 | $('.js-user-avatar-input').bind "change", ->
7 | form = $(this).closest("form")
8 | filename = $(this).val().replace(/^.*[\\\/]/, '')
9 | form.find(".js-avatar-filename").text(filename)
--------------------------------------------------------------------------------
/app/assets/javascripts/views/users.js.coffee:
--------------------------------------------------------------------------------
1 | class @UsersCardsView
2 | constructor: (@model) ->
3 | @model.subscribe @onUpdate
4 |
5 | initialize: ->
6 | $('#usersSort').change =>
7 | @render()
8 |
9 | $('#usersFilter').on 'keyup', (e) =>
10 | @render()
11 |
12 | onUpdate: =>
13 | @render()
14 |
15 | getUsers: ->
16 | filter = $('#usersFilter').val()
17 | sort = $('#usersSort').val()
18 | order = $('#usersSort').find('option:selected').data('order')
19 | @model.getUsers(filter, sort, order)
20 |
21 | render: ->
22 | tpl = $('#user-card-tpl').html()
23 | target = $('#users')
24 | target.html('')
25 | users = @getUsers()
26 | for user in users
27 | html = tpl.replace('data-src', 'src')
28 | html = html.replace('%%card-item%%', 'user-card')
29 | html = html.replace( /%%id%%/g, user.id)
30 | html = html.replace('%%avatar_url%%', user.avatar_url)
31 | html = html.replace( /%%name%%/g, user.name)
32 | html = html.replace( /%%email%%/g, user.email)
33 |
34 | html = html.replace('%%created%%', moment(user.created_at).fromNow())
35 | html = html.replace('%%last_login%%', moment(user.current_sign_in_at).fromNow())
36 | html = html.replace('%%last_ip%%', user.current_sign_in_ip)
37 | html = html.replace('%%sign_in_count%%', user.sign_in_count)
38 |
39 | if gon.user_id == user.id
40 | html = html.replace('%%hide_delete%%', 'hide')
41 | else
42 | html = html.replace('%%hide_delete%%', '')
43 |
44 | target.append(html)
45 |
46 | # Must tooltip after dynamic creation
47 | $("[data-toggle='tooltip']").tooltip({container: 'body'})
48 |
49 | $(".user-card .inner").flip( {trigger: 'manual'} )
50 | $(".user-info.flip-on").on 'click', (e) ->
51 | $(e.target).parents('.inner').flip(true)
52 | $(".user-info.flip-off").on 'click', (e) ->
53 | $(e.target).parents('.inner').flip(false)
54 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/application.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the top of the
9 | * compiled file, but it's generally better to create a new file per style scope.
10 | *
11 | *= require_self
12 | *= require select2
13 | *= require handsontable.full
14 | *= require datepicker
15 | *= require daterangepicker-bs3
16 | *= require morris
17 | *= require mousetrap
18 | */
19 | @import "bootstrap";
20 | @import "font-awesome";
21 | @import "admin_lte";
22 | @import "devise.scss";
23 | @import "common.scss";
24 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/home.css.scss:
--------------------------------------------------------------------------------
1 | // Place all the styles related to the Home controller here.
2 | // They will automatically be included in application.css.
3 | // You can use Sass (SCSS) here: http://sass-lang.com/
4 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/morris.css:
--------------------------------------------------------------------------------
1 | .morris-hover{position:absolute;z-index:1090;}.morris-hover.morris-default-style{border-radius:10px;padding:6px;color:#f9f9f9;background:rgba(0, 0, 0, 0.8);border:solid 2px rgba(0, 0, 0, 0.9);font-weight: 600;font-size:14px;text-align:center;}.morris-hover.morris-default-style .morris-hover-row-label{font-weight:bold;margin:0.25em 0;}
2 | .morris-hover.morris-default-style .morris-hover-point{white-space:nowrap;margin:0.1em 0;}
3 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/users.css.scss:
--------------------------------------------------------------------------------
1 | // Place all the styles related to the users controller here.
2 | // They will automatically be included in application.css.
3 | // You can use Sass (SCSS) here: http://sass-lang.com/
4 |
--------------------------------------------------------------------------------
/app/controllers/admin/admin_controller.rb:
--------------------------------------------------------------------------------
1 | class Admin::AdminController < ApplicationController
2 | before_action :authenticate_admin!
3 |
4 | def authenticate_admin!
5 | return render_404 unless current_user.is_admin?
6 | end
7 | end
--------------------------------------------------------------------------------
/app/controllers/admin/projects_controller.rb:
--------------------------------------------------------------------------------
1 | class Admin::ProjectsController < Admin::AdminController
2 | add_breadcrumb "Home", :root_path
3 | add_breadcrumb "Projects", :admin_projects_path
4 |
5 | def index
6 | @projects = Project.order(:name).page(params[:page])
7 | end
8 |
9 | end
10 |
--------------------------------------------------------------------------------
/app/controllers/admin/users_controller.rb:
--------------------------------------------------------------------------------
1 | class Admin::UsersController < Admin::AdminController
2 | before_action :user, only: [:edit, :update, :destroy]
3 |
4 | add_breadcrumb "Home", :root_path
5 | add_breadcrumb "Users", :admin_users_path
6 |
7 | def index
8 | gon.user_id = current_user.id
9 | @users = User.order(:name).page(params[:page])
10 | end
11 |
12 | def new
13 | add_breadcrumb "New", :new_admin_user_path
14 | @user = User.new
15 | end
16 |
17 | def edit
18 | add_breadcrumb "Edit", :edit_admin_user_path
19 | end
20 |
21 | def create
22 | @user = User.new(user_params)
23 | if @user.save
24 | flash[:notice] = "User was successfully created"
25 | redirect_to admin_users_path
26 | else
27 | render "new"
28 | end
29 | end
30 |
31 | def update
32 | if user_params[:password].blank?
33 | user_params.delete("password")
34 | end
35 |
36 | successfully_updated = if needs_password?(@user, params)
37 | @user.update(user_params)
38 | else
39 | @user.update_without_password(user_params)
40 | end
41 |
42 | if successfully_updated
43 | flash[:notice] = "User was successfully updated"
44 | redirect_to admin_users_path
45 | else
46 | render "edit"
47 | end
48 | end
49 |
50 | def destroy
51 | user.destroy
52 | redirect_to admin_users_path
53 | end
54 |
55 | def remove_avatar
56 | @user = User.find(params[:user_id])
57 | @user.avatar = nil
58 | @user.save
59 | flash[:notice] = "Avatar was successfully removed"
60 | redirect_to :back
61 | end
62 |
63 | protected
64 |
65 | def user
66 | @user = User.find(params[:id])
67 | end
68 |
69 | def user_params
70 | params.require(:user).permit(:name, :email, :bio, :avatar, :password, :admin)
71 | end
72 |
73 | # check if we need password to update user data
74 | # ie if password or email was changed
75 | # extend this as needed
76 | def needs_password?(user, params)
77 | user.email != params[:user][:email] ||
78 | params[:user][:password].present?
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | require 'gon'
2 |
3 | class ApplicationController < ActionController::Base
4 | protect_from_forgery with: :exception
5 |
6 | before_action :authenticate_user!
7 | before_action :configure_permitted_parameters, if: :devise_controller?
8 | before_action :add_abilities
9 | before_action :add_gon_variables
10 |
11 | helper_method :abilities, :can?
12 |
13 |
14 | rescue_from ActiveRecord::RecordNotFound do |exception|
15 | render "errors/not_found", layout:"application", status: 404
16 | end
17 |
18 | protected
19 |
20 | def configure_permitted_parameters
21 | devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
22 | end
23 |
24 | def abilities
25 | @abilities ||= Six.new
26 | end
27 |
28 | def can?(object, action, subject)
29 | abilities.allowed?(object, action, subject)
30 | end
31 |
32 | def add_abilities
33 | abilities << Ability
34 | end
35 |
36 | def render_403
37 | head :forbidden
38 | end
39 |
40 | def render_404
41 | render "errors/not_found", layout:"application", status: 404
42 | end
43 |
44 | def add_gon_variables
45 | gon.api_version = API::API.version
46 | gon.api_token = current_user.private_token if current_user
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frocher/sofreakingboring/37ccfa946e3764652cc552f644e0d9affbcaa4a3/app/controllers/concerns/.keep
--------------------------------------------------------------------------------
/app/controllers/errors_controller.rb:
--------------------------------------------------------------------------------
1 | class ErrorsController < ApplicationController
2 |
3 | add_breadcrumb "Home", :root_path
4 |
5 | def not_found
6 | render :status => 404
7 | end
8 |
9 | def internal_error
10 | render :status => 500
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/controllers/home_controller.rb:
--------------------------------------------------------------------------------
1 | class HomeController < ApplicationController
2 |
3 | add_breadcrumb "Home", :root_path
4 |
5 | def index
6 | add_breadcrumb "Dashboard", :root_path
7 |
8 | @recent_openings = current_user.project_openings.includes(:project).order(updated_at: :desc).limit(3)
9 | today = Date.today
10 | @period_start = today.beginning_of_week
11 | @period_end = @period_start + 6
12 | gon.period_start = @period_start.strftime("%Y%m%d")
13 | gon.period_end = @period_end.strftime("%Y%m%d")
14 |
15 | @work_week = current_user.work_week(@period_start, @period_end)
16 | end
17 |
18 | def work_for_period
19 | period_start = DateTime.strptime(params[:period_start], "%Y%m%d")
20 | period_end = DateTime.strptime(params[:period_end], "%Y%m%d")
21 | @work_week = current_user.work_week(period_start, period_end)
22 | respond_to do |format|
23 | format.js
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/app/controllers/profiles/avatars_controller.rb:
--------------------------------------------------------------------------------
1 | class Profiles::AvatarsController < ApplicationController
2 | def destroy
3 | @user = current_user
4 | @user.avatar = nil
5 | @user.save
6 |
7 | redirect_to :back
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/controllers/profiles/passwords_controller.rb:
--------------------------------------------------------------------------------
1 | class Profiles::PasswordsController < ApplicationController
2 | before_action :user
3 |
4 | add_breadcrumb "Home", :root_path
5 | add_breadcrumb "Profile", :profile_path
6 |
7 | def edit
8 | add_breadcrumb "Change password", :edit_profile_password_path
9 | end
10 |
11 | def update
12 | if @user.update_attributes(user_params)
13 | flash[:notice] = "Profile was successfully updated"
14 | sign_in(@user, :bypass => true)
15 | redirect_to profile_path
16 | else
17 | render "edit"
18 | end
19 | end
20 |
21 | private
22 |
23 | def user
24 | @user = current_user
25 | end
26 |
27 | def user_params
28 | params.require(:user).permit(:password, :password_confirmation)
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/app/controllers/profiles_controller.rb:
--------------------------------------------------------------------------------
1 | class ProfilesController < ApplicationController
2 | before_action :user, except: [:avatar]
3 |
4 | add_breadcrumb "Home", :root_path
5 | add_breadcrumb "Profile", :profile_path
6 |
7 | def edit
8 | add_breadcrumb "Edit", :edit_profile_path
9 | end
10 |
11 | def update
12 | if @user.update_attributes(user_params)
13 | flash[:notice] = "Profile was successfully updated"
14 | redirect_to profile_path
15 | else
16 | render "edit"
17 | end
18 | end
19 |
20 | private
21 |
22 | def user
23 | @user = current_user
24 | end
25 |
26 | def user_params
27 | params.require(:user).permit(:name, :email, :bio, :avatar)
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/app/controllers/projects/items_controller.rb:
--------------------------------------------------------------------------------
1 | class Projects::ItemsController < ApplicationController
2 | before_action :project
3 | layout "project"
4 |
5 | add_breadcrumb "Home", :root_path
6 | add_breadcrumb "Projects", :projects_path
7 |
8 | protected
9 |
10 | def add_project_breadcrumb
11 | add_breadcrumb @project.name, project_path(params[:project_id])
12 | end
13 |
14 | def project
15 | @project = Project.find(params[:project_id])
16 | return render_404 unless can?(current_user, :read_project, @project)
17 |
18 | @project.touch_opening(current_user.id)
19 | gon.project_id = @project.id
20 | gon.project_state = @project.state
21 | end
22 |
23 | end
--------------------------------------------------------------------------------
/app/controllers/projects/members_controller.rb:
--------------------------------------------------------------------------------
1 | class Projects::MembersController < Projects::ItemsController
2 |
3 | def index
4 | @members = @project.project_members.by_name.page(params[:page])
5 | @member = ProjectMember.new
6 | add_project_breadcrumb
7 | add_breadcrumb "Members", :project_members_path
8 | init_new_options
9 | end
10 |
11 | def new
12 | return render_404 unless can?(current_user, :create_project_member, @project)
13 |
14 | add_new_breadcrumb
15 | init_new_options
16 | @errors = []
17 | end
18 |
19 | def create
20 | return render_404 unless can?(current_user, :create_project_member, @project)
21 |
22 | ids = params['user-search'].split(',').map { |s| s.to_i }
23 | @errors = []
24 | if ids.empty?
25 | @errors << "You must select at least one user"
26 | else
27 | ids.each do |id|
28 | if @project.users.exists?(id)
29 | user = User.find(id)
30 | @errors << "User #{user.name} is already a member of this project"
31 | end
32 | end
33 | end
34 |
35 | unless can?(current_user, :create_project_member_admin, @project)
36 | if params[:role] == 'admin'
37 | @errors << "You are not allowed to give admin role"
38 | end
39 | end
40 |
41 | if @errors.any?
42 | add_new_breadcrumb
43 | init_new_options
44 | return render "new"
45 | end
46 |
47 | ids.each do |id|
48 | member = ProjectMember.new
49 | member.user = User.find(id)
50 | member.project = @project
51 | member.role = params[:role]
52 | member.save
53 | end
54 | flash[:notice] = "Members was successfully added"
55 |
56 | redirect_to project_members_path
57 | end
58 |
59 | def update
60 | return render_404 unless can?(current_user, :update_project_member, @project)
61 | member = ProjectMember.find(params[:id])
62 |
63 | project = member.project
64 | can_update = member_params[:role] == 'admin'
65 |
66 | unless can_update
67 | project.project_members.each do |current|
68 | if current.id != member.id && current.role == 'admin'
69 | can_update = true
70 | end
71 | end
72 | end
73 |
74 | if can_update
75 | if member.update_attributes(member_params)
76 | flash[:notice] = "Member role was successfully updated"
77 | end
78 | else
79 | flash[:alert] = "Member role was not updated. A project must have at least one admin"
80 | end
81 |
82 | redirect_to project_members_path
83 | end
84 |
85 |
86 | def destroy
87 | return render_404 unless can?(current_user, :delete_project_member, @project)
88 | member = ProjectMember.find(params[:id])
89 | if member.assigned_tasks.count == 0
90 | if member.destroy
91 | flash[:notice] = "Member was successfully removed"
92 | end
93 | else
94 | flash[:alert] = "Can't delete member with assigned tasks"
95 | end
96 | redirect_to project_members_path
97 | end
98 |
99 | private
100 |
101 | def init_new_options
102 | @new_options = [:guest, :developer, :master]
103 | @new_options << :admin if can?(current_user, :create_project_member_admin, @project)
104 | end
105 |
106 | def add_new_breadcrumb
107 | add_project_breadcrumb
108 | add_breadcrumb "Members", :project_members_path
109 | add_breadcrumb "New", :new_project_member_path
110 | end
111 |
112 |
113 | def member_params
114 | params.require(:project_member).permit(:role)
115 | end
116 | end
117 |
--------------------------------------------------------------------------------
/app/controllers/projects/stats_controller.rb:
--------------------------------------------------------------------------------
1 | class Projects::StatsController < Projects::ItemsController
2 | def index
3 | add_project_breadcrumb
4 | add_breadcrumb "Statistics", :project_stats_path
5 | end
6 |
7 | def show
8 | period_start = params[:start]
9 | period_end = params[:end]
10 |
11 | first = Date.strptime(period_start, '%Y%m%d')
12 | last = Date.strptime(period_end, '%Y%m%d')
13 |
14 | if (last - first).to_i > 366
15 | @error = "Maximum allowed range is 366 days."
16 | else
17 | @members = @project.project_members
18 |
19 | # create an empty two dimensional array
20 | # columns are for members
21 | # rows are for days
22 | @results = Array.new
23 | @js_results = Array.new
24 |
25 | first.upto(last) do |date|
26 | row = Array.new(@members.size + 1, 0)
27 | row[0] = date.strftime("%b %d, %Y")
28 | @results << row
29 |
30 | js_hash = Hash.new
31 | js_hash['day'] = date.strftime("%Y-%m-%d")
32 | @members.each do |member|
33 | js_hash[member.email] = 0
34 | end
35 | @js_results << js_hash
36 | end
37 |
38 | @total = Array.new(@members.size, 0)
39 |
40 | # fill array
41 | @members.each_with_index do |member, index|
42 | logs = member.work_logs(period_start, period_end)
43 | logs.each do |log|
44 | date = Date.strptime(log.day, '%Y%m%d')
45 | diff = (date - first).to_i
46 | @results[diff][index+1] = view_context.duration(log.total_worked)
47 | @js_results[diff][member.email] = view_context.to_days(log.total_worked)
48 |
49 | @total[index] += log.total_worked
50 | end
51 | end
52 |
53 | @total.each_with_index do |item, index|
54 | @total[index] = view_context.duration(item)
55 | end
56 | end
57 |
58 | respond_to do |format|
59 | format.html { render :layout => !request.xhr? }
60 | end
61 |
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/app/controllers/projects/tasks_controller.rb:
--------------------------------------------------------------------------------
1 | class Projects::TasksController < Projects::ItemsController
2 |
3 | def index
4 | add_project_breadcrumb
5 | add_breadcrumb "Tasks", :project_tasks_path
6 |
7 | gon.user_id = current_user.id
8 | gon.can_update_tasks = can?(current_user, :update_project_task, @project)
9 | gon.can_take_unassigned_task = can?(current_user, :take_unassigned_task, @project)
10 | end
11 |
12 | end
13 |
--------------------------------------------------------------------------------
/app/controllers/projects/timesheets_controller.rb:
--------------------------------------------------------------------------------
1 | class Projects::TimesheetsController < Projects::ItemsController
2 |
3 | # Start editing timesheet
4 | def edit
5 | add_project_breadcrumb
6 | add_breadcrumb "My Timesheet", :edit_project_timesheet_path
7 |
8 | today = Date.today
9 | @period_start = today.beginning_of_week
10 | @period_end = @period_start + 4
11 | gon.user_id = current_user.id
12 | gon.period_start = @period_start.strftime("%Y%m%d")
13 | gon.period_end = @period_end.strftime("%Y%m%d")
14 | gon.can_update_members_task_work = can?(current_user, :update_members_task_work, @project)
15 | end
16 |
17 | end
18 |
--------------------------------------------------------------------------------
/app/controllers/projects_controller.rb:
--------------------------------------------------------------------------------
1 | class ProjectsController < ApplicationController
2 | before_action :project, only: [:show, :edit, :update, :destroy]
3 |
4 | layout "project", only: [:show, :show_export]
5 |
6 | add_breadcrumb "Home", :root_path
7 | add_breadcrumb "Projects", :projects_path
8 |
9 | def index
10 | @projects = current_user.projects.order(:name).page(params[:page])
11 | end
12 |
13 | def show
14 | add_breadcrumb @project.name, :project_path
15 | end
16 |
17 | def new
18 | add_breadcrumb "New", :new_project_path
19 | session[:return_to] = request.referer
20 | @project = Project.new
21 | end
22 |
23 | def create
24 | @project = Project.new(project_params)
25 | if @project.save
26 | member = ProjectMember.new
27 | member.project = @project
28 | member.user = current_user
29 | member.role = :admin
30 | member.save!
31 |
32 | flash[:notice] = "Project was successfully created"
33 | redirect_to project_path(@project)
34 | else
35 | render "new"
36 | end
37 | end
38 |
39 | def edit
40 | return render_404 unless can?(current_user, :update_project, @project)
41 | add_breadcrumb "Edit #{@project.name}", :edit_project_path
42 | session[:return_to] = request.referer
43 | end
44 |
45 | def update
46 | return render_404 unless can?(current_user, :update_project, @project)
47 | if @project.update_attributes(project_params)
48 | flash[:notice] = "Project was successfully updated"
49 | redirect_to session.delete(:return_to)
50 | else
51 | render "edit"
52 | end
53 | end
54 |
55 | def destroy
56 | return render_404 unless can?(current_user, :delete_project, @project)
57 | if @project.destroy
58 | flash[:notice] = "Project was successfully removed"
59 | end
60 | redirect_to projects_path
61 | end
62 |
63 | def remove_attachment
64 | @project = Project.find(params[:project_id])
65 | return render_404 unless can?(current_user, :update_project, @project)
66 | @project.attachment = nil
67 | @project.save
68 | flash[:notice] = "Picture was successfully removed"
69 | redirect_to :back
70 | end
71 |
72 | def show_export
73 | @project = Project.find(params[:project_id])
74 | return render_404 unless can?(current_user, :export_project, @project)
75 | add_breadcrumb @project.name, :project_show_export_path
76 | end
77 |
78 | def export
79 | @project = Project.find(params[:project_id])
80 | return render_404 unless can?(current_user, :export_project, @project)
81 | respond_to do |format|
82 | format.xlsx
83 | end
84 | end
85 |
86 | private
87 |
88 | def project
89 | @project = Project.find(params[:id])
90 | return render_404 unless can?(current_user, :read_project, @project)
91 |
92 | @project.touch_opening(current_user.id)
93 | gon.project_id = @project.id
94 | end
95 |
96 | def project_params
97 | params.require(:project).permit(:name, :code, :description, :state, :attachment)
98 | end
99 | end
100 |
--------------------------------------------------------------------------------
/app/controllers/users/omniauth_callbacks_controller.rb:
--------------------------------------------------------------------------------
1 | class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
2 | def facebook
3 | handle_omniauth("Facebook", "devise.facebook_data")
4 | end
5 |
6 | def google_oauth2
7 | handle_omniauth("Google", "devise.google_data")
8 | end
9 |
10 | def github
11 | handle_omniauth("Github", "devise.github_data")
12 | end
13 | private
14 |
15 | def handle_omniauth(kind, session_key)
16 | @user = User.from_omniauth(request.env["omniauth.auth"].except("extra"))
17 |
18 | if @user.persisted?
19 | sign_in_and_redirect @user, event: :authentication #this will throw if @user is not activated
20 | set_flash_message(:notice, :success, kind: kind) if is_navigational_format?
21 | else
22 | session[session_key] = request.env["omniauth.auth"]
23 | redirect_to new_user_registration_url
24 | end
25 | end
26 |
27 | end
--------------------------------------------------------------------------------
/app/controllers/users_controller.rb:
--------------------------------------------------------------------------------
1 | class UsersController < ApplicationController
2 | def avatar
3 | @user = User.find(params[:id])
4 | @size = params[:size].to_i
5 |
6 | url = view_context.avatar_icon(@user.email, @size)
7 | data = open(url).read
8 | send_data data, disposition: 'inline'
9 | end
10 | end
--------------------------------------------------------------------------------
/app/helpers/admin/projects_helper.rb:
--------------------------------------------------------------------------------
1 | module Admin::ProjectsHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/helpers/admin/users_helper.rb:
--------------------------------------------------------------------------------
1 | module Admin::UsersHelper
2 | def user_tags(user)
3 | tags = Array.new
4 | if user.admin?
5 | tags.push "admin"
6 | end
7 | # TODO : test if blocked
8 | tags.push "active"
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | require 'chronic_duration'
2 |
3 | module ApplicationHelper
4 |
5 | def avatar_icon(user_email = '', size = nil)
6 | user = User.find_by(email: user_email)
7 | if user && user.avatar.present?
8 | size = 120 if size.nil? || size <= 0
9 | tag = size <= 100 ? :thumb : :medium
10 | if request.nil?
11 | user.avatar.url(tag)
12 | else
13 | URI.join(request.url, user.avatar.url(tag))
14 | end
15 | else
16 | gravatar_icon(user_email, size)
17 | end
18 | end
19 |
20 | def gravatar_icon(user_email = '', size = nil)
21 | size = 120 if size.nil? || size <= 0
22 |
23 | plain_url = 'http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon'
24 | ssl_url = 'https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon'
25 | gravatar_url = ENV.fetch("GRAVATAR_HTTPS", false) ? ssl_url : plain_url
26 | user_email.strip!
27 | sprintf gravatar_url, hash: Digest::MD5.hexdigest(user_email.downcase), size: size
28 | end
29 |
30 | def randomized_background_image
31 | images = ["/assets/intro-bug.jpg", "/assets/intro-mantis.jpg", "/assets/intro-town.jpg"]
32 | images[rand(images.size)]
33 | end
34 |
35 | def date_only(datetime)
36 | datetime.strftime("%b %d, %Y")
37 | end
38 |
39 | def to_days(minutes)
40 | to_day = 60 * 8
41 | (Float(minutes) / to_day).round(1)
42 | end
43 |
44 | def to_date(day)
45 | DateTime.strptime(day, "%Y%m%d")
46 | end
47 |
48 | def duration(minutes)
49 | if minutes == 0
50 | "0"
51 | elsif minutes < 0
52 | '-' + ChronicDuration.output(-minutes, format: :short)
53 | else
54 | ChronicDuration.output(minutes, format: :short)
55 | end
56 | end
57 |
58 | def body_data_page
59 | path = controller.controller_path.split('/')
60 | namespace = path.first if path.second
61 |
62 | [namespace, controller.controller_name, controller.action_name].compact.join(":")
63 | end
64 |
65 | end
66 |
--------------------------------------------------------------------------------
/app/helpers/devise_helper.rb:
--------------------------------------------------------------------------------
1 | module DeviseHelper
2 | def devise_error_messages!
3 | return "" if resource.errors.empty?
4 |
5 | messages = resource.errors.full_messages.map { |msg| content_tag(:li, msg) }.join
6 | sentence = I18n.t("errors.messages.not_saved",
7 | :count => resource.errors.count,
8 | :resource => resource.class.model_name.human.downcase)
9 |
10 | html = <<-HTML
11 |
12 |
×
13 |
#{sentence}
14 |
15 |
16 | HTML
17 |
18 | html.html_safe
19 | end
20 |
21 | def devise_error_messages?
22 | resource.errors.empty? ? false : true
23 | end
24 |
25 | end
--------------------------------------------------------------------------------
/app/helpers/errors_helper.rb:
--------------------------------------------------------------------------------
1 | module ErrorsHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/helpers/home_helper.rb:
--------------------------------------------------------------------------------
1 | module HomeHelper
2 | def callout
3 | if current_user.projects.count == 0
4 | description = "You don't have a project yet. You could be invited by others or " + link_to("create your own new project.", new_project_path)
5 | description = description.html_safe
6 | render partial: "shared/callout_info", locals: {title: "Welcome", description: description}
7 | elsif current_user.project_openings == 0
8 | description = "You still haven't opened any project. Just go to your " + link_to("projects page.", projects_path)
9 | description = description.html_safe
10 | render partial: "shared/callout_info", locals: {title: "Welcome", description: description}
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/helpers/profiles/avatars_helper.rb:
--------------------------------------------------------------------------------
1 | module Profiles::AvatarsHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/helpers/profiles/passwords_helper.rb:
--------------------------------------------------------------------------------
1 | module Profiles::PasswordsHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/helpers/profiles_helper.rb:
--------------------------------------------------------------------------------
1 | module ProfilesHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/helpers/projects/members_helper.rb:
--------------------------------------------------------------------------------
1 | module Projects::MembersHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/helpers/projects/stats_helper.rb:
--------------------------------------------------------------------------------
1 | module Projects::StatsHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/helpers/projects/tasks_helper.rb:
--------------------------------------------------------------------------------
1 | module Projects::TasksHelper
2 | def tasks_callout(project)
3 | if project.tasks.count == 0
4 | description = "To create a new task, just clic on the 'Add Task' button. It will create a new line. "
5 | description << "You can then complete task informations. For estimates, you can enter time in days (e.g., 1d), hours (e.g., 4h) or minutes (e.g., 30m), "
6 | description << "and of course combine them (e.g., 1d 4h 30m)."
7 | description = description.html_safe
8 | render partial: "shared/callout_info", locals: {title: "No tasks yet", description: description}
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/helpers/projects/timesheets_helper.rb:
--------------------------------------------------------------------------------
1 | module Projects::TimesheetsHelper
2 | def format_period(period_start, period_end)
3 | period_start.strftime("%b %d, %Y") + " - " + period_end.strftime("%b %d, %Y")
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/helpers/projects_helper.rb:
--------------------------------------------------------------------------------
1 | module ProjectsHelper
2 | def dashboard_callout(project)
3 | if project.tasks.count == 0
4 | description = "Your project doesn't have any task. You should go to the " + link_to("tasks page.", project_tasks_path(project))
5 | description = description.html_safe
6 | render partial: "shared/callout_info", locals: {title: "No tasks yet", description: description}
7 | end
8 | end
9 |
10 | def options_for_project(project)
11 | if project.draft?
12 | ['draft', 'opened']
13 | else
14 | ['opened', 'closed']
15 | end
16 | end
17 |
18 | def class_for_project_state(project)
19 | case project.state
20 | when 'draft'
21 | 'warning'
22 | when 'opened'
23 | 'success'
24 | else
25 | 'danger'
26 | end
27 | end
28 |
29 | def members_done(project)
30 | members_done = "["
31 | project.project_members.by_name.each do |member|
32 | if member.work_logged > 0 || member.remaining_estimate > 0
33 | members_done << "," unless members_done == "["
34 | name = escape_javascript(member.name)
35 | members_done << "[#{to_days(member.work_logged)}, '#{name}']"
36 | end
37 | end
38 | members_done << "]"
39 | end
40 |
41 | def members_remaining(project)
42 | members_tbd = "["
43 | project.project_members.by_name.each do |member|
44 | if member.work_logged > 0 || member.remaining_estimate > 0
45 | members_tbd << "," unless members_tbd == "["
46 | name = escape_javascript(member.name)
47 | members_tbd << "[#{to_days(member.remaining_estimate)}, '#{name}']"
48 | end
49 | end
50 | members_tbd << "]"
51 | end
52 |
53 | def tags_done(project)
54 | worked = Hash.new
55 | project.tasks.each do |task|
56 | task.tag_list.each do |tag|
57 | val = worked[tag] || 0
58 | val += task.work_logged
59 | worked[tag] = val
60 | end
61 | end
62 |
63 | tags_done = "["
64 | worked.sort.each do |key,value|
65 | name = escape_javascript(key)
66 | tags_done << "," unless tags_done == "["
67 | tags_done << "[#{to_days(value)}, '#{name}']"
68 | end
69 | tags_done << "]"
70 | end
71 |
72 | def tags_remaining(project)
73 | remaining = Hash.new
74 | project.tasks.each do |task|
75 | task.tag_list.each do |tag|
76 | val = remaining[tag] || 0
77 | val += task.remaining_estimate
78 | remaining[tag] = val
79 | end
80 | end
81 |
82 | tags_tbd = "["
83 | remaining.sort.each do |key,value|
84 | name = escape_javascript(key)
85 | tags_tbd << "," unless tags_tbd == "["
86 | tags_tbd << "[#{to_days(value)}, '#{name}']"
87 | end
88 | tags_tbd << "]"
89 | end
90 |
91 | def user_role(project)
92 | member = project.find_member(current_user)
93 | member.nil? ? '' : member.role
94 | end
95 |
96 | end
97 |
--------------------------------------------------------------------------------
/app/mailers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frocher/sofreakingboring/37ccfa946e3764652cc552f644e0d9affbcaa4a3/app/mailers/.keep
--------------------------------------------------------------------------------
/app/models/ability.rb:
--------------------------------------------------------------------------------
1 | class Ability
2 | class << self
3 | def allowed(user, subject)
4 | return [] unless user.kind_of?(User)
5 |
6 | case subject.class.name
7 | when "Project" then project_abilities(user, subject)
8 | else []
9 | end.concat(global_abilities(user))
10 | end
11 |
12 | def global_abilities(user)
13 | rules = []
14 | rules
15 | end
16 |
17 | def project_abilities(user, project)
18 | rules = []
19 |
20 | members = project.project_members
21 | member = project.project_members.find_by_user_id(user.id)
22 |
23 | # Rules based on role in project
24 | if user.admin? || members.admins.include?(member)
25 | rules << project_admin_rules
26 |
27 | elsif members.masters.include?(member)
28 | rules << project_master_rules
29 |
30 | elsif members.developers.include?(member)
31 | rules << project_dev_rules
32 |
33 | elsif members.guests.include?(member)
34 | rules << project_guest_rules
35 |
36 | end
37 |
38 | rules.flatten
39 | end
40 |
41 |
42 | def project_guest_rules
43 | [
44 | :read_project,
45 | :read_project_member
46 | ]
47 | end
48 |
49 | def project_dev_rules
50 | project_guest_rules + [
51 | :update_task_work,
52 | :take_unassigned_task
53 | ]
54 | end
55 |
56 | def project_master_rules
57 | project_dev_rules + [
58 | :export_project,
59 | :create_project_member,
60 | :update_project_member,
61 | :delete_project_member,
62 | :create_project_task,
63 | :update_project_task,
64 | :delete_project_task,
65 | :update_members_task_work
66 | ]
67 | end
68 |
69 | def project_admin_rules
70 | project_master_rules + [
71 | :create_project_member_admin,
72 | :delete_project,
73 | :update_project
74 | ]
75 | end
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/app/models/concerns/internal_id.rb:
--------------------------------------------------------------------------------
1 | module InternalId
2 | extend ActiveSupport::Concern
3 |
4 | included do
5 | validate :set_iid, on: :create
6 | validates :iid, presence: true, numericality: true
7 | end
8 |
9 | def set_iid
10 | max_iid = project.send(self.class.name.tableize).maximum(:iid)
11 | self.iid = max_iid.to_i + 1
12 | end
13 |
14 | def to_param
15 | iid.to_s
16 | end
17 | end
--------------------------------------------------------------------------------
/app/models/identity.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: identities
4 | #
5 | # id :integer not null, primary key
6 | # provider :string(255) not null
7 | # uid :string(255) not null
8 | # user_id :integer not null
9 | # created_at :datetime
10 | # updated_at :datetime
11 | #
12 |
13 | class Identity < ActiveRecord::Base
14 | belongs_to :user
15 | end
16 |
--------------------------------------------------------------------------------
/app/models/project.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: projects
4 | #
5 | # id :integer not null, primary key
6 | # code :string(255) default(""), not null
7 | # name :string(255) default(""), not null
8 | # description :text
9 | # created_at :datetime
10 | # updated_at :datetime
11 | # attachment_file_name :string(255)
12 | # attachment_content_type :string(255)
13 | # attachment_file_size :integer
14 | # attachment_updated_at :datetime
15 | # state :string(255)
16 | #
17 |
18 | class Project < ActiveRecord::Base
19 | has_attached_file :attachment, :styles => { :medium => "160x180#", :thumb => "100>", :small_thumb => "50x50#" }
20 |
21 | has_many :project_members, dependent: :destroy
22 | has_many :users, through: :project_members
23 | has_many :tasks, dependent: :destroy
24 | has_many :project_snapshots, dependent: :destroy
25 | has_many :project_openings, dependent: :destroy
26 |
27 |
28 | #
29 | # Validations
30 | #
31 | validates :code, presence: true
32 | validates_uniqueness_of :code
33 | validates_length_of :code, :maximum => 8
34 | validates :name, presence: true
35 | validates_attachment_content_type :attachment, :content_type => /\Aimage/
36 | validates_attachment_size :attachment, :in => 0.kilobytes..200.kilobytes
37 |
38 | #
39 | # States
40 | #
41 | state_machine :state, initial: :draft do
42 | event :openit do
43 | transition all => :opened
44 | end
45 |
46 | event :closeit do
47 | transition :opened => :closed
48 | end
49 | end
50 |
51 | #
52 | # Scopes
53 | #
54 | scope :opened, -> { where(state: "opened") }
55 |
56 | def picture_url
57 | attachment.url
58 | end
59 |
60 | def original_estimate
61 | tasks.sum(:original_estimate)
62 | end
63 |
64 | def work_logged
65 | resu = 0
66 | found = Task.includes(:work_logs).where('project_id=?', id)
67 | found.each do |task|
68 | resu += task.work_logged
69 | end
70 | return resu
71 | end
72 |
73 | def delta
74 | resu = 0
75 | found = Task.includes(:work_logs).where('project_id=?', id)
76 | found.each do |task|
77 | resu += task.delta
78 | end
79 | return resu
80 | end
81 |
82 | def remaining_estimate
83 | tasks.sum(:remaining_estimate)
84 | end
85 |
86 | def total
87 | work_logged + remaining_estimate
88 | end
89 |
90 | def progress
91 | total > 0 ? work_logged * 100 / total : 0
92 | end
93 |
94 | def touch_opening(user_id)
95 | opening = project_openings.find_by user_id: user_id
96 | unless opening
97 | opening = ProjectOpening.new
98 | opening.user_id = user_id
99 | opening.project_id = id
100 | end
101 | opening.touched = Random.rand(9999)
102 | opening.save
103 | end
104 |
105 | def find_member(user)
106 | project_members.find_by(user_id:user.id)
107 | end
108 |
109 | # Create a snapshot of all opened projects
110 | def self.snapshot
111 | Project.opened.each do |project|
112 | snap = ProjectSnapshot.new
113 | snap.project = project
114 | snap.task_count = project.tasks.count
115 | snap.original_estimate = project.original_estimate
116 | snap.work_logged = project.work_logged
117 | snap.remaining_estimate = project.remaining_estimate
118 | unless snap.save
119 | logger.error snap.errors.messages
120 | end
121 | end
122 | end
123 |
124 | end
125 |
--------------------------------------------------------------------------------
/app/models/project_member.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: project_members
4 | #
5 | # id :integer not null, primary key
6 | # user_id :integer not null
7 | # project_id :integer not null
8 | # role :integer default("0"), not null
9 | # created_at :datetime
10 | # updated_at :datetime
11 | #
12 |
13 | class ProjectMember < ActiveRecord::Base
14 | enum role: {guest: 0, developer: 1, master: 2, admin: 3}
15 |
16 | belongs_to :user
17 | belongs_to :project
18 |
19 | delegate :name, to: :user
20 | delegate :email, to: :user
21 |
22 | validates :user, presence: true
23 | validates :project, presence: true
24 | validates :user_id, uniqueness: { scope: [:project_id], message: "already exists in project" }
25 |
26 | scope :by_name, -> { joins(:user).order('LOWER(users.name)') }
27 | scope :guests, -> { where("role = :role", role: 0) }
28 | scope :developers, -> { where("role = :role", role: 1) }
29 | scope :masters, -> { where("role = :role", role: 2) }
30 | scope :admins, -> { where("role = :role", role: 3) }
31 |
32 |
33 | def assigned_tasks
34 | Task.where("assignee_id=? and project_id=?", user.id, project.id)
35 | end
36 |
37 | def work_logs(day_start, day_end)
38 | WorkLog.joins('INNER JOIN tasks ON work_logs.task_id = tasks.id').joins('INNER JOIN projects ON tasks.project_id = projects.id').select("day, sum(worked) as total_worked").where(tasks:{assignee_id:user.id}).where(projects:{id:project.id}).where(day:day_start..day_end).group("day").order(:day)
39 | end
40 |
41 | def work_logged
42 | WorkLog.joins('INNER JOIN tasks ON work_logs.task_id = tasks.id').joins('INNER JOIN projects ON tasks.project_id = projects.id').select("sum(worked) as work_sum").where(tasks:{assignee_id:user.id}).where(projects:{id:project.id}).take.work_sum || 0
43 | end
44 |
45 | def remaining_estimate
46 | Task.joins('INNER JOIN projects ON tasks.project_id = projects.id').select("sum(remaining_estimate) as remaining_sum").where(tasks:{assignee_id:user.id}).where(projects:{id:project.id}).take.remaining_sum || 0
47 | end
48 |
49 | def username
50 | user.name
51 | end
52 |
53 | def avatar_url
54 | ApplicationController.helpers.avatar_icon(user.email)
55 | end
56 |
57 | end
58 |
--------------------------------------------------------------------------------
/app/models/project_opening.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: project_openings
4 | #
5 | # id :integer not null, primary key
6 | # project_id :integer not null
7 | # user_id :integer not null
8 | # touched :integer
9 | # created_at :datetime
10 | # updated_at :datetime
11 | #
12 |
13 | class ProjectOpening < ActiveRecord::Base
14 | belongs_to :user
15 | belongs_to :project
16 | end
17 |
--------------------------------------------------------------------------------
/app/models/project_snapshot.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: project_snapshots
4 | #
5 | # id :integer not null, primary key
6 | # project_id :integer not null
7 | # task_count :integer
8 | # original_estimate :integer
9 | # work_logged :integer
10 | # remaining_estimate :integer
11 | # created_at :datetime
12 | # updated_at :datetime
13 | #
14 | class ProjectSnapshot < ActiveRecord::Base
15 |
16 | belongs_to :project
17 |
18 |
19 | def delta
20 | original_estimate - (work_logged + remaining_estimate)
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/app/models/task.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: tasks
4 | #
5 | # id :integer not null, primary key
6 | # name :string(255) default(""), not null
7 | # description :text
8 | # original_estimate :integer
9 | # remaining_estimate :integer
10 | # project_id :integer not null
11 | # assignee_id :integer
12 | # created_at :datetime
13 | # updated_at :datetime
14 | # iid :integer
15 | #
16 |
17 | class Task < ActiveRecord::Base
18 | include InternalId
19 |
20 | acts_as_taggable
21 |
22 | belongs_to :assignee, class_name: "User"
23 | belongs_to :project
24 |
25 | has_many :work_logs, dependent: :destroy
26 |
27 | #
28 | # Versioning
29 | #
30 | has_paper_trail unless: Proc.new { |t| t.project.draft? }
31 |
32 | #
33 | # Validation
34 | #
35 | validates :name, presence: true
36 | validates :original_estimate, numericality: { greater_than_or_equal_to: 0 }
37 | validates :remaining_estimate, numericality: { greater_than_or_equal_to: 0 }
38 |
39 | #
40 | # Scopes
41 | #
42 | scope :done, -> { where("remaining_estimate = 0") }
43 |
44 | def self.not_started
45 | all.select {|s| s.remaining_estimate != 0 && s.work_logged == 0}
46 | end
47 |
48 | def self.in_progress
49 | all.select {|s| s.remaining_estimate != 0 && s.work_logged != 0}
50 | end
51 |
52 | def code
53 | project.code + "-" + iid.to_s
54 | end
55 |
56 |
57 | def work_logged
58 | count = 0
59 | work_logs.each do |log|
60 | count += log.worked
61 | end
62 |
63 | count
64 | end
65 |
66 | def delta
67 | original_estimate - (work_logged + remaining_estimate)
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/app/models/timesheet_task.rb:
--------------------------------------------------------------------------------
1 | # Transitive class used to manipulate tasks on timesheets
2 | class TimesheetTask
3 |
4 | attr_accessor :id
5 | attr_accessor :code
6 | attr_accessor :name
7 | attr_accessor :description
8 | attr_accessor :tag_list
9 | attr_accessor :monday
10 | attr_accessor :tuesday
11 | attr_accessor :wednesday
12 | attr_accessor :thursday
13 | attr_accessor :friday
14 | attr_accessor :saturday
15 | attr_accessor :sunday
16 | attr_accessor :original_estimate
17 | attr_accessor :remaining_estimate
18 | attr_accessor :work_logged
19 |
20 | def initialize(task, period_start, period_end)
21 | @id = task.id
22 | @code = task.code
23 | @name = task.name
24 | @description = task.description
25 | @tag_list = task.tag_list
26 | @monday = 0
27 | @tuesday = 0
28 | @wednesday = 0
29 | @thursday = 0
30 | @friday = 0
31 | @saturday = 0
32 | @sunday = 0
33 | @original_estimate = task.original_estimate
34 | @remaining_estimate = task.remaining_estimate
35 | @work_logged = 0
36 |
37 |
38 | task.work_logs.each do |log|
39 |
40 | # if log is in period, add it to one of the days
41 | # else add it to the other work_logged
42 | if log.day >= period_start && log.day <= period_end
43 | date = DateTime.parse(log.day)
44 | weekday = date.strftime("%u")
45 |
46 | case weekday
47 | when '1'
48 | @monday += log.worked
49 | when '2'
50 | @tuesday += log.worked
51 | when '3'
52 | @wednesday += log.worked
53 | when '4'
54 | @thursday += log.worked
55 | when '5'
56 | @friday += log.worked
57 | when '6'
58 | @saturday += log.worked
59 | when '7'
60 | @sunday += log.worked
61 | end
62 | else
63 | @work_logged += log.worked
64 | end
65 | end
66 |
67 | end
68 |
69 | def delta
70 | @original_estimate - (@work_logged + @remaining_estimate)
71 | end
72 |
73 | end
--------------------------------------------------------------------------------
/app/models/work_log.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: work_logs
4 | #
5 | # id :integer not null, primary key
6 | # description :string(255)
7 | # day :string(255)
8 | # worked :integer
9 | # task_id :integer
10 | # created_at :datetime
11 | # updated_at :datetime
12 | #
13 |
14 | class WorkLog < ActiveRecord::Base
15 | belongs_to :task
16 |
17 | validates :day, presence: true
18 | validates :worked, numericality: { greater_than_or_equal_to: 0 }
19 | end
20 |
--------------------------------------------------------------------------------
/app/models/work_week.rb:
--------------------------------------------------------------------------------
1 | class WorkWeek
2 | attr_accessor :projects, :total
3 |
4 | def initialize(logs)
5 | @projects = Hash.new
6 | @total = [0, 0, 0, 0, 0, 0, 0]
7 | logs.each do |log|
8 | value = @projects[log.name]
9 | value = [0, 0, 0, 0, 0, 0, 0] if value.nil?
10 | date = DateTime.parse(log.day, "%Y%m%d")
11 | value[date.wday-1] += log.total_worked
12 | @total[date.wday-1] += log.total_worked
13 | @projects[log.name] = value
14 | end
15 | end
16 |
17 | end
--------------------------------------------------------------------------------
/app/views/admin/projects/index.html.slim:
--------------------------------------------------------------------------------
1 | - @title="Projects"
2 | .row
3 | .col-sm-2
4 | = render '/shared/projects_summary'
5 | .col-sm-10
6 | = render '/shared/projects_box'
7 |
8 | = render '/shared/project_card'
--------------------------------------------------------------------------------
/app/views/admin/users/_form.html.slim:
--------------------------------------------------------------------------------
1 | = form_for([:admin, @user], html: { multipart: true }) do |f|
2 | .col-sm-8.col-md-6
3 | .box.box-solid
4 | .box-body
5 | - if @user.errors.any?
6 | .alert.alert-danger.alert-dismissable
7 | button aria-hidden="true" data-dismiss="alert" class="close" type="button" ×
8 | ul
9 | - @user.errors.full_messages.each do |msg|
10 | li = msg
11 | h4.bottom-15 General Information
12 | .form-group
13 | = f.label :name
14 | = f.text_field :name, :autofocus => true, 'placeholder' => "Enter user name", class: "form-control"
15 | .form-group
16 | = f.label :password, "New password"
17 | = f.password_field :password, :autofocus => true, 'placeholder' => "Enter user password", class: "form-control"
18 | .form-group
19 | = f.label :email
20 | = f.email_field :email, 'placeholder' => "Enter user email", class: "form-control"
21 | .form-group
22 | = f.label :bio
23 | = f.text_area :bio, rows: "6", class: "form-control"
24 | .form-group
25 | = f.check_box :admin, class: "form-control"
26 | = f.label :admin
27 | .box-footer.clearfix
28 | .pull-right
29 | =link_to "Cancel", admin_users_path
30 | | or
31 | button.btn.btn-primary type="submit" Submit
32 | .col-sm-8.col-sm-offset-4.col-md-3.col-md-offset-0
33 | - if @user.id?
34 | .box.box-solid
35 | .box-header
36 | h3.box-title Avatar
37 | .box-body
38 | - if @user.avatar?
39 | | You can change user avatar here
40 | br
41 | | or remove the current avatar to revert to #{link_to "gravatar.com", "http://gravatar.com"}
42 | - else
43 | | You can upload an avatar here
44 | br
45 | | or change it at #{link_to "gravatar.com", "http://gravatar.com"}
46 | hr
47 | button type="button" class="btn btn-default btn-sm js-choose-user-avatar-button"
48 | i.fa.fa-paperclip
49 | span Choose File...
50 | |
51 | span.file_name.js-avatar-filename File name...
52 | = f.file_field :avatar, class: "js-user-avatar-input hidden"
53 | .light The maximum file size allowed is 100KB.
54 | - if @user.avatar?
55 | hr
56 | = link_to 'Remove avatar', admin_user_remove_avatar_path(@user), data: { confirm: "Avatar will be removed. Are you sure?"}, method: :put, class: "btn btn-danger btn-small remove-avatar"
--------------------------------------------------------------------------------
/app/views/admin/users/_user_card.html.slim:
--------------------------------------------------------------------------------
1 | #user-card-tpl.hide
2 | div class='%%card-item%%'
3 | .inner
4 | .front
5 | .thumbnail.user-card id='user-%%id%%'
6 | .card-body
7 | i.fa.fa-info-circle.user-info.flip-on title='click to flip me' data-toggle='tooltip' data-user_id='user-%%id%%'
8 | div style='text-align:center'
9 | img class='avatar' data-src='%%avatar_url%%' width='120' height='120'
10 | .truncate title='%%name%%' data-toggle='tooltip' %%name%%
11 | .truncate title='%%email%%' data-toggle='tooltip' %%email%%
12 | div
13 | span style="float:right"
14 | a class="btn btn-primary btn-sm" href="/admin/users/%%id%%/edit" title="Edit user" data-toggle='tooltip'
15 | i class="fa fa-pencil"
16 | |
17 | a class="btn btn-danger btn-sm %%hide_delete%%" data-confirm="User will be removed. Are you sure?" data-method="delete" href="/admin/users/%%id%%" rel="nofollow" title="Delete user" data-toggle='tooltip'
18 | i class="fa fa-trash-o bg-red"
19 | .back
20 | .thumbnail.user-card id='user-%%id%%'
21 | .card-body
22 | i.fa.fa-info-circle.user-info.flip-off title='click to flip me' data-toggle='tooltip' data-user_id='user-%%id%%'
23 | ul
24 | li created : %%created%%
25 | li last login : %%last_login%%
26 | li last IP : %%last_ip%%
27 | li sign in count : %%sign_in_count%%
28 |
29 |
--------------------------------------------------------------------------------
/app/views/admin/users/edit.html.slim:
--------------------------------------------------------------------------------
1 | - @title="Edit user"
2 | = render 'form'
--------------------------------------------------------------------------------
/app/views/admin/users/index.html.slim:
--------------------------------------------------------------------------------
1 | - @title="Users"
2 | .row
3 | .col-sm-2
4 | =link_to "Add a new user", new_admin_user_path, class: "btn btn-primary btn-block"
5 |
6 | .col-sm-10
7 | .box
8 | .box-body
9 | form.navbar-form
10 | .form-group
11 | input#usersFilter.form-control type='search' placeholder="Search..."
12 | |
13 | select#usersSort.form-control
14 | option value="name" data-order="asc" Sort by name
15 | option value="email" data-order="asc" Sort by email
16 | option value="created_at" data-order="desc" Sort by creation time
17 | option value="sign_in_count" data-order="asc" Sort by login count
18 | option value="current_sign_in_at" data-order="desc" Sort by login time
19 | = render '/shared/loading_spinner'
20 | #users
21 |
22 | = render 'user_card'
--------------------------------------------------------------------------------
/app/views/admin/users/new.html.slim:
--------------------------------------------------------------------------------
1 | - @title="Create user"
2 | = render 'form'
3 |
--------------------------------------------------------------------------------
/app/views/devise/confirmations/new.html.erb:
--------------------------------------------------------------------------------
1 | Resend confirmation instructions
2 |
3 | <%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %>
4 | <%= devise_error_messages! %>
5 |
6 | <%= f.label :email %>
7 | <%= f.email_field :email, autofocus: true %>
8 |
9 | <%= f.submit "Resend confirmation instructions" %>
10 | <% end %>
11 |
12 | <%= render "devise/shared/links" %>
13 |
--------------------------------------------------------------------------------
/app/views/devise/mailer/confirmation_instructions.html.erb:
--------------------------------------------------------------------------------
1 | Welcome <%= @email %>!
2 |
3 | You can confirm your account email through the link below:
4 |
5 | <%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>
6 |
--------------------------------------------------------------------------------
/app/views/devise/mailer/reset_password_instructions.html.erb:
--------------------------------------------------------------------------------
1 | Hello <%= @resource.email %>!
2 |
3 | Someone has requested a link to change your password. You can do this through the link below.
4 |
5 | <%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>
6 |
7 | If you didn't request this, please ignore this email.
8 | Your password won't change until you access the link above and create a new one.
9 |
--------------------------------------------------------------------------------
/app/views/devise/mailer/unlock_instructions.html.erb:
--------------------------------------------------------------------------------
1 | Hello <%= @resource.email %>!
2 |
3 | Your account has been locked due to an excessive number of unsuccessful sign in attempts.
4 |
5 | Click the link below to unlock your account:
6 |
7 | <%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>
8 |
--------------------------------------------------------------------------------
/app/views/devise/passwords/edit.html.slim:
--------------------------------------------------------------------------------
1 | #login-box.form-box
2 | .header Change your password
3 |
4 | = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f|
5 | .body.bg-gray
6 | = devise_error_messages!
7 |
8 | = f.hidden_field :reset_password_token
9 |
10 | .form-group
11 | = f.password_field :password, autofocus: true, autocomplete: "off", class: "form-control", placeholder: "New Password"
12 |
13 | .form-group
14 | = f.password_field :password_confirmation, autocomplete: "off", class: "form-control", placeholder: "Confirm your Password"
15 |
16 | .footer
17 | = f.submit "Change my password", class: "btn bg-olive btn-block"
18 | = render "devise/shared/links"
19 |
--------------------------------------------------------------------------------
/app/views/devise/passwords/new.html.slim:
--------------------------------------------------------------------------------
1 | #login-box.form-box
2 | .header Forgot your password?
3 |
4 | = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f|
5 | .body.bg-gray
6 | = devise_error_messages!
7 |
8 | .form-group
9 | = f.email_field :email, placeholder:"Enter your email", class: "form-control", autofocus: true
10 |
11 | .footer
12 | = f.submit "Send me reset password instructions", class:"btn bg-olive btn-block"
13 | = render "devise/shared/links"
14 |
--------------------------------------------------------------------------------
/app/views/devise/registrations/edit.html.erb:
--------------------------------------------------------------------------------
1 | Edit <%= resource_name.to_s.humanize %>
2 |
3 | <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
4 | <%= devise_error_messages! %>
5 |
6 | <%= f.label :email %>
7 | <%= f.email_field :email, autofocus: true %>
8 |
9 | <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
10 | Currently waiting confirmation for: <%= resource.unconfirmed_email %>
11 | <% end %>
12 |
13 | <%= f.label :password %> (leave blank if you don't want to change it)
14 | <%= f.password_field :password, autocomplete: "off" %>
15 |
16 | <%= f.label :password_confirmation %>
17 | <%= f.password_field :password_confirmation, autocomplete: "off" %>
18 |
19 | <%= f.label :current_password %> (we need your current password to confirm your changes)
20 | <%= f.password_field :current_password, autocomplete: "off" %>
21 |
22 | <%= f.submit "Update" %>
23 | <% end %>
24 |
25 | Cancel my account
26 |
27 | Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>
28 |
29 | <%= link_to "Back", :back %>
30 |
--------------------------------------------------------------------------------
/app/views/devise/registrations/new.html.slim:
--------------------------------------------------------------------------------
1 | #login-box.form-box
2 | .header Register New Membership
3 |
4 | = form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|
5 |
6 | .body.bg-gray
7 | = devise_error_messages!
8 |
9 | .form-group
10 | = f.text_field :name, placeholder: "Full name", class: "form-control", autofocus: true
11 |
12 | .form-group
13 | = f.email_field :email, placeholder: "E-mail", class: "form-control"
14 |
15 | .form-group
16 | = f.password_field :password, placeholder: "Password", class: "form-control", autocomplete: "off"
17 |
18 | .form-group
19 | = f.password_field :password_confirmation, placeholder: "Retype password", class: "form-control", autocomplete: "off"
20 |
21 | .footer
22 | = f.submit "Sign me up", class:"btn bg-olive btn-block"
23 | = render "devise/shared/social"
24 | = render "devise/shared/links"
25 |
--------------------------------------------------------------------------------
/app/views/devise/sessions/new.html.slim:
--------------------------------------------------------------------------------
1 | #login-box.form-box
2 | .header
3 | | Sign In
4 | = form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
5 | .body.bg-gray
6 | .form-group
7 | = f.email_field :email, class: "form-control", placeholder: "User Email", autofocus: true
8 |
9 | .form-group
10 | = f.password_field :password, class: "form-control", autocomplete: "off", placeholder: "Password"
11 |
12 | .form-group
13 | -if devise_mapping.rememberable?
14 | div
15 | = f.check_box :remember_me
16 | |
17 | = f.label :remember_me
18 | .footer
19 | = f.submit "Sign me in", class: "btn bg-olive btn-block"
20 | = render "devise/shared/social"
21 | = render "devise/shared/links"
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/views/devise/shared/_links.html.slim:
--------------------------------------------------------------------------------
1 | - if controller_name != 'sessions'
2 | = link_to "I already have a membership", new_session_path(resource_name)
3 | br
4 |
5 | - if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations'
6 | = link_to "I forgot my password", new_password_path(resource_name)
7 | br
8 |
9 | - if devise_mapping.registerable? && controller_name != 'registrations'
10 | = link_to "Register a new membership", new_registration_path(resource_name)
11 | br
12 |
13 | - if devise_mapping.confirmable? && controller_name != 'confirmations'
14 | = link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name)
15 | br
16 |
17 | - if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks'
18 | = link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name)
19 | br
20 |
--------------------------------------------------------------------------------
/app/views/devise/shared/_social.html.slim:
--------------------------------------------------------------------------------
1 | .margin.text-center
2 | = link_to user_facebook_omniauth_authorize_path(:facebook), class: "btn bg-light-blue btn-circle", title: "Sign in with FaceBook"
3 | i.fa.fa-facebook
4 | |
5 | = link_to user_google_oauth2_omniauth_authorize_path(:google_oauth2), class: "btn bg-red btn-circle", title: "Sign in with Google"
6 | i.fa.fa-google
7 | |
8 | = link_to user_github_omniauth_authorize_path(:github), class: "btn bg-black btn-circle", title: "Sign in with Github"
9 | i.fa.fa-github-alt
10 |
--------------------------------------------------------------------------------
/app/views/devise/unlocks/new.html.erb:
--------------------------------------------------------------------------------
1 | Resend unlock instructions
2 |
3 | <%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %>
4 | <%= devise_error_messages! %>
5 |
6 | <%= f.label :email %>
7 | <%= f.email_field :email, autofocus: true %>
8 |
9 | <%= f.submit "Resend unlock instructions" %>
10 | <% end %>
11 |
12 | <%= render "devise/shared/links" %>
13 |
--------------------------------------------------------------------------------
/app/views/errors/internal_error.html.slim:
--------------------------------------------------------------------------------
1 | - @title="Internal error"
2 | .error-page
3 | h2.headline.text-info 500
4 | .error-content
5 | h3
6 | i.fa.fa-warning.text-yellow
7 | | Oops! Something went wrong.
8 | p
9 | | We will work on fixing that right away.
10 | | Meanwhile, you may
11 | = link_to root_path, title: "Home" do
12 | | return to home page.
13 |
14 |
--------------------------------------------------------------------------------
/app/views/errors/not_found.html.slim:
--------------------------------------------------------------------------------
1 | - @title="Page not found"
2 | .error-page
3 | h2.headline.text-info 404
4 | .error-content
5 | h3
6 | i.fa.fa-warning.text-yellow
7 | | Oops! Page not found.
8 | p
9 | | We could not find the page you were looking for.
10 | | Meanwhile, you may
11 | = link_to root_path, title: "Home" do
12 | | return to home page.
13 |
14 |
--------------------------------------------------------------------------------
/app/views/home/_work_week_table.html.slim:
--------------------------------------------------------------------------------
1 | table#work_week_table.table.table-bordered
2 | tbody
3 | tr
4 | th Project
5 | th Mon.
6 | th Tue.
7 | th Wed.
8 | th Thu.
9 | th Fri.
10 | th Sat.
11 | th Sun.
12 | - work_week.projects.sort.map do |name, days|
13 | tr
14 | td = name
15 | - days.each do |day|
16 | td = duration(day)
17 | tr
18 | th Total
19 | - work_week.total.each do |day|
20 | th = duration(day)
--------------------------------------------------------------------------------
/app/views/home/index.html.slim:
--------------------------------------------------------------------------------
1 | - @title="Dashboard"
2 | .row
3 | .col-lg-12
4 | = callout
5 | .row
6 | .col-lg-3.col-xs-6
7 | .small-box.bg-aqua
8 | .inner
9 | h3
10 | = current_user.projects.opened.count
11 | p Active projects
12 | .icon
13 | i.fa.fa-flask
14 | .col-lg-3.col-xs-6
15 | .small-box.bg-green
16 | .inner
17 | h3
18 | = current_user.tasks.includes(:work_logs).not_started.count + current_user.tasks.includes(:work_logs).in_progress.count
19 | p Assigned tasks
20 | .icon
21 | i.fa.fa-tasks
22 | .col-lg-3.col-xs-6
23 | .small-box.bg-yellow
24 | .inner
25 | h3
26 | = duration(current_user.tasks.sum(:remaining_estimate))
27 | p Remaining work
28 | .icon
29 | i.fa.fa-paper-plane
30 | .col-lg-3.col-xs-6
31 | .small-box.bg-red
32 | .inner
33 | h3
34 | = duration(current_user.work_logged)
35 | p Total work done
36 | .icon
37 | i.fa.fa-beer
38 | .row
39 | .col-lg-6
40 | .box.box-solid.box-primary
41 | .box-header
42 | h3.box-title Recent projects
43 | .box-tools.pull-right
44 | button.btn.btn-primary.btn-sm data-widget="collapse"
45 | i.fa.fa-minus
46 | .box-body
47 | .row
48 | - @recent_openings.each do |opening|
49 | .col-sm-6.col-md-4
50 | = render partial: "projects/project_card", locals: {project: opening.project}
51 | .col-lg-6
52 | .box.box-solid.box-info
53 | .box-header
54 | h3.box-title Work done
55 | .box-tools.pull-right
56 | .input-group.no-margin
57 | input#periodInput.form-control name="period" readonly="readonly" value=(format_period(@period_start, @period_end)) style="width:200px"
58 | button#previousPeriod.btn.btn-default.btn-flat type="button" title="Previous period" style="background-color:white"
59 | i.fa.fa-caret-left
60 | button#nextPeriod.btn.btn-default.btn-flat type="button" title="Next period" style="background-color:white"
61 | i.fa.fa-caret-right
62 | span#dpPeriod.input-append.date data-date=(@period_start.strftime("%d-%m-%Y")) data-date-format="dd-mm-yyyy"
63 | button.btn.btn-default.btn-flat type="button" title="Choose" style="background-color:white"
64 | i.fa.fa-caret-down
65 | .box-body
66 | = render partial: 'work_week_table', locals: { work_week: @work_week}
67 |
68 | = render '/shared/project_card'
69 |
70 |
--------------------------------------------------------------------------------
/app/views/home/work_for_period.js.erb:
--------------------------------------------------------------------------------
1 | $("#work_week_table").html("<%= escape_javascript(render partial: 'work_week_table', locals: { work_week: @work_week } ) %>");
--------------------------------------------------------------------------------
/app/views/layouts/_flash.html.slim:
--------------------------------------------------------------------------------
1 | .flash-container
2 | - if alert
3 | .flash-alert
4 | = alert
5 |
6 | - elsif notice
7 | .flash-notice
8 | = notice
--------------------------------------------------------------------------------
/app/views/layouts/_footer_panel.html.slim:
--------------------------------------------------------------------------------
1 | - if ENV.has_key?("GOOGLE_ANALYTICS_KEY")
2 | javascript:
3 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
4 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
5 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
6 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
7 |
8 | ga('create', "#{ENV.fetch('GOOGLE_ANALYTICS_KEY')}", 'auto');
9 | ga('send', 'pageview');
10 |
--------------------------------------------------------------------------------
/app/views/layouts/_head.html.slim:
--------------------------------------------------------------------------------
1 | head
2 | title
3 | = "#{@title} | " if defined?(@title)
4 | | So Freaking Boring
5 | - if defined?(@description)
6 | meta name="description" content=@description
7 | - else
8 | meta name="description" content="So Freaking Boring is an open source and simple project management and time tracking application"
9 | meta name="viewport" content="width=device-width, initial-scale=1.0"
10 |
11 | link rel="shortcut icon" href="/favicon.ico" type="image/x-icon"
12 | link rel="icon" href="/favicon.ico" type="image/x-icon"
13 |
14 | css:
15 | @media screen and (min-width: 767px) {
16 | .login-header {
17 | background: url(#{randomized_background_image}) no-repeat center scroll;
18 | }
19 | }
20 |
21 | = stylesheet_link_tag "application"
22 | = csrf_meta_tags
23 | = include_gon
--------------------------------------------------------------------------------
/app/views/layouts/_header_panel.html.slim:
--------------------------------------------------------------------------------
1 | header.header
2 | = link_to root_path, class: "logo", title: "Home" do
3 | | So Freaking Boring
4 | nav.navbar.navbar-static-top role="navigation"
5 | a role="button" data-toggle="offcanvas" class="navbar-btn sidebar-toggle" href="#"
6 | span.sr-only Toggle navigation
7 | span.icon-bar
8 | span.icon-bar
9 | span.icon-bar
10 | ul.nav.navbar-nav
11 | li
12 | = link_to projects_path, title: "Projects" do
13 | i.fa.fa-briefcase
14 | span My Projects
15 | - if current_user.is_admin?
16 | li.dropdown
17 | a.dropdown-toggle href="#" data-toggle="dropdown"
18 | i class="fa fa-laptop"
19 | | Admin
20 | b.caret
21 | ul.dropdown-menu
22 | li
23 | = link_to admin_projects_path, title: "Projects", style:"margin-left: 10px;" do
24 | i.fa.fa-rocket
25 | | Projects
26 | li
27 | = link_to admin_users_path, title: "Users", style:"margin-left: 10px;" do
28 | i.fa.fa-users
29 | | Users
30 | .navbar-right
31 | ul.nav.navbar-nav
32 | li.dropdown.user.user-menu
33 | a.dropdown-toggle href="#" data-toggle="dropdown"
34 | i.glyphicon.glyphicon-user
35 | span
36 | = current_user.name
37 | i.caret
38 | ul.dropdown-menu
39 | li.user-header.bg-light-blue
40 | = image_tag avatar_icon(current_user.email, 100), class: "img-circle"
41 | p
42 | = current_user.name
43 | small
44 | | Member since
45 | = current_user.created_at.strftime("%b. %Y")
46 | li.user-footer
47 | .pull-left
48 | = link_to profile_path, class: "btn btn-default btn-flat" do
49 | | Profile
50 | .pull-right
51 | = link_to destroy_user_session_path, class: "btn btn-default btn-flat", method: :delete, title: "Logout" do
52 | | Sign out
53 |
54 |
--------------------------------------------------------------------------------
/app/views/layouts/_sidebar.html.slim:
--------------------------------------------------------------------------------
1 | section.sidebar
2 | .project-panel
3 | - if @project.attachment?
4 | .image.pull-left
5 | = image_tag @project.attachment.url(:small_thumb), class: "img-circle"
6 | .info.pull-left
7 | p
8 | = @project.name
9 | a
10 | i class=('fa fa-circle text-' + class_for_project_state(@project))
11 | = @project.state
12 | - if can?(current_user, :update_project, @project)
13 | |
14 | =link_to edit_project_path(@project), title: "Edit project"
15 | i.fa.fa-pencil
16 | ul.sidebar-menu
17 | li
18 | = link_to project_path(@project) do
19 | i.fa.fa-dashboard
20 | span Dashboard
21 | li
22 | = link_to edit_project_timesheet_path(@project) do
23 | i.fa.fa-calendar
24 | span Timesheet
25 | li
26 | = link_to project_tasks_path(@project) do
27 | i.fa.fa-tasks
28 | span Tasks
29 | li
30 | = link_to project_members_path(@project) do
31 | i.fa.fa-users
32 | span Members
33 | li
34 | = link_to project_stats_path(@project) do
35 | i.fa.fa-line-chart
36 | span Statistics
37 | - if can?(current_user, :export_project, @project)
38 | li
39 | = link_to project_show_export_path(@project)
40 | i.fa.fa-download
41 | span Export
42 |
43 |
--------------------------------------------------------------------------------
/app/views/layouts/application.html.slim:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | = render "layouts/head", title: "Home"
4 | body.pace-done.skin-black data-page=(body_data_page)
5 | = render "layouts/header_panel", title: "Home"
6 | .wrapper.row-offcanvas.row-offcanvas-left
7 | aside.right-side style="margin-left:0"
8 | section.content-header
9 | h1
10 | = "#{@title}" if defined?(@title)
11 | -if defined?(@subtitle)
12 | small =@subtitle
13 | .breadcrumb
14 | = render_breadcrumbs separator: ' > '
15 | section.content =yield
16 | = render "layouts/footer_panel"
17 | = render "layouts/flash"
18 |
19 | = javascript_include_tag "application"
--------------------------------------------------------------------------------
/app/views/layouts/project.html.slim:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | = render "layouts/head", title: "Home"
4 | body.pace-done.skin-black data-page=(body_data_page)
5 | = render "layouts/header_panel", title: "Home"
6 | .wrapper.row-offcanvas.row-offcanvas-left
7 | aside.left-side.sidebar-offcanvas
8 | = render "layouts/sidebar"
9 | aside.right-side
10 | section.content-header
11 | h1
12 | = "#{@title}" if defined?(@title)
13 | -if defined?(@subtitle)
14 | small =@subtitle
15 | .breadcrumb
16 | = render_breadcrumbs separator: ' > '
17 | section.content =yield
18 | = render "layouts/footer_panel"
19 | = render "layouts/flash"
20 |
21 | = javascript_include_tag "application"
--------------------------------------------------------------------------------
/app/views/profiles/edit.html.slim:
--------------------------------------------------------------------------------
1 | - @title="Edit profile"
2 | = form_for(@user, url: profile_path, html: { multipart: true }) do |f|
3 | .col-sm-8.col-md-6
4 | .box.box-solid
5 | .box-body
6 | h3 Please fill out the form below to update your profile.
7 |
8 | hr
9 | - if @user.errors.any?
10 | .alert.alert-danger.alert-dismissable
11 | button aria-hidden="true" data-dismiss="alert" class="close" type="button" ×
12 | ul
13 | - @user.errors.full_messages.each do |msg|
14 | li = msg
15 | h4.bottom-15 General Information
16 | .form-group
17 | = f.label :name
18 | = f.text_field :name, :autofocus => true, 'placeholder' => "Enter your name", class: "form-control"
19 | .form-group
20 | = f.label :email
21 | = f.email_field :email, 'placeholder' => "Enter new email", class: "form-control"
22 | .form-group
23 | = f.label :bio
24 | = f.text_area :bio, rows: "6", 'placeholder' => "Tell us about yourself in fewer than 500 characters.", class: "form-control"
25 | .box-footer.clearfix
26 | .pull-right
27 | =link_to "Cancel", profile_path
28 | | or
29 | button.btn.btn-primary type="submit" Save
30 | .col-sm-8.col-sm-offset-4.col-md-3.col-md-offset-0
31 | .box.box-solid
32 | .box-header
33 | h3.box-title Avatar
34 | .box-body
35 | - if @user.avatar?
36 | | You can change your avatar here
37 | br
38 | | or remove the current avatar to revert to #{link_to "gravatar.com", "http://gravatar.com"}
39 | - else
40 | | You can upload an avatar here
41 | br
42 | | or change it at #{link_to "gravatar.com", "http://gravatar.com"}
43 | hr
44 | button type="button" class="btn btn-default btn-sm js-choose-user-avatar-button"
45 | i.fa.fa-paperclip
46 | span Choose File...
47 | |
48 | span.file_name.js-avatar-filename File name...
49 | = f.file_field :avatar, class: "js-user-avatar-input hidden"
50 | .light The maximum file size allowed is 100KB.
51 | - if @user.avatar?
52 | hr
53 | = link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-danger btn-sm remove-avatar"
54 |
55 |
--------------------------------------------------------------------------------
/app/views/profiles/passwords/edit.html.slim:
--------------------------------------------------------------------------------
1 | - @title="Change password"
2 | = form_for(@user, url: profile_password_path) do |f|
3 | .col-sm-8.col-md-6
4 | .box.box-solid
5 | .box-body
6 | - if @user.errors.any?
7 | .alert.alert-danger.alert-dismissable
8 | button aria-hidden="true" data-dismiss="alert" class="close" type="button" ×
9 | ul
10 | - @user.errors.full_messages.each do |msg|
11 | li = msg
12 | .form-group
13 | = f.label :password, "New password"
14 | = f.password_field :password, :autofocus => true, placeholder: "Enter new password", class: "form-control"
15 | .form-group
16 | = f.label :password_confirmation, "Confirm password"
17 | = f.password_field :password_confirmation, placeholder: "Confirm new password", class: "form-control"
18 | .box-footer.clearfix
19 | .pull-right
20 | =link_to "Cancel", profile_path
21 | | or
22 | button.btn.btn-primary type="submit" Change my password
23 | .col-sm-8.col-sm-offset-4.col-md-3.col-md-offset-0
24 |
--------------------------------------------------------------------------------
/app/views/profiles/show.html.slim:
--------------------------------------------------------------------------------
1 | - @title = current_user && @user.id == current_user.id ? "My profile" : @user.name + "'s profile"
2 | .col-sm-8.col-md-8
3 | .box.box-solid
4 | .box-body
5 | .row
6 | .col-sm-6.col-md-2
7 | .thumbnail
8 | = image_tag avatar_icon(@user.email, 165)
9 | .col-sm-6.col-md-10
10 | h3
11 | = @user.name
12 | br
13 | small = @user.email
14 | hr
15 | = link_to edit_profile_path, class: "btn btn-default btn-flat" do
16 | | Edit Profile
17 | |
18 | = link_to edit_profile_password_path, class: "btn btn-default btn-flat" do
19 | | Change password
20 |
21 | .row
22 | .col-sm-12
23 | h4
24 | i.fa.fa-user
25 | | About me
26 | hr
27 | p.bottom-15
28 | = @user.bio
29 | .col-sm-4.col-sm-offset-4.col-md-4.col-md-offset-0
30 |
--------------------------------------------------------------------------------
/app/views/projects/_form.html.slim:
--------------------------------------------------------------------------------
1 | = form_for(@project, html: { multipart: true }) do |f|
2 | .col-sm-8.col-md-6
3 | .box.box-solid
4 | .box-body
5 | - if @project.errors.any?
6 | .alert.alert-danger.alert-dismissable
7 | button aria-hidden="true" data-dismiss="alert" class="close" type="button" ×
8 | ul
9 | - @project.errors.full_messages.each do |msg|
10 | li = msg
11 | h4.bottom-15 General Information
12 | .form-group
13 | = f.label :code
14 | = f.text_field :code, :autofocus => true, 'placeholder' => "Enter project code", class: "form-control"
15 | .form-group
16 | = f.label :name
17 | = f.text_field :name, 'placeholder' => "Enter project name", class: "form-control"
18 | .form-group
19 | = f.label :description
20 | = f.text_area :description, rows: "6", class: "form-control"
21 | .form-group
22 | = f.label :state
23 | = f.select :state, options_for_select(options_for_project(@project), @project.state), {}, {class: "form-control"}
24 | .box-footer.clearfix
25 | .pull-right
26 | =link_to "Cancel", session[:return_to]
27 | | or
28 | button.btn.btn-primary type="submit" Submit
29 | .col-sm-8.col-sm-offset-4.col-md-3.col-md-offset-0
30 | .box.box-solid
31 | .box-header
32 | h3.box-title Picture
33 | .box-body
34 | - if @project.attachment?
35 | | You can change project picture here
36 | | or remove the current project picture
37 | .project-picture
38 | = image_tag @project.attachment.url(:medium)
39 | - else
40 | | You can upload a picture here
41 | hr
42 | button type="button" class="btn btn-default btn-sm js-choose-project-picture-button"
43 | i.fa.fa-paperclip
44 | span Choose File...
45 | |
46 | span.file_name.js-picture-filename = @project.attachment.original_filename
47 | = f.file_field :attachment, class: "js-project-picture-input hidden"
48 | .light The maximum file size allowed is 200KB.
49 | - if @project.attachment?
50 | hr
51 | = link_to 'Remove picture', project_remove_attachment_path(@project), data: { confirm: "Picture will be removed. Are you sure?"}, method: :put, class: "btn btn-danger btn-small remove-picture"
52 | - if can?(current_user, :delete_project, @project) and @project.id != nil
53 | .box.box-solid
54 | .box-header
55 | h3.box-title Delete project
56 | .box-body
57 | =link_to project_path(@project), data: { confirm: "Project will be removed. That can't be undone. Are you sure?"}, method: :delete, class: "btn btn-danger", title: "Delete project"
58 | i.sign.fa.fa-trash-o.bg-red
59 | span Delete project
60 |
--------------------------------------------------------------------------------
/app/views/projects/_progress.html.slim:
--------------------------------------------------------------------------------
1 | .progress.xs data-toggle="tooltip" data-original-title=("#{project.progress}%")
2 | div class=("progress-bar progress-bar-" + (project.delta < 0 ? "red" : "green")) style=("width:#{project.progress}%")
--------------------------------------------------------------------------------
/app/views/projects/_project_card.html.slim:
--------------------------------------------------------------------------------
1 | .thumbnail.project-card
2 | = render partial: "projects/progress", locals: {project: project}
3 | = link_to project_path(project) do
4 | - if project.attachment?
5 | = image_tag project.attachment.url(:medium), title: project.description, data: {toggle:'tooltip'}
6 | - else
7 | = image_tag project.attachment.url(:medium), title: project.description, data: {toggle:'tooltip', src:"holder.js/160x180/text:No picture"}
8 | .caption
9 | h3
10 | = link_to project_path(project),title: project.description, data: {toggle:'tooltip'} do
11 | = project.name
12 | small
13 | | (
14 | = project.code
15 | | )
16 |
17 | - project.project_members.by_name.each do |member|
18 | = image_tag avatar_icon(member.user.email, 38), size:'38', title: member.name, style:'margin:1px', data: {toggle:'tooltip'}
19 |
--------------------------------------------------------------------------------
/app/views/projects/_projects_grid.html.slim:
--------------------------------------------------------------------------------
1 | .box
2 | .box-body.table-responsive.no-padding
3 | table#projects.table.table-hover
4 | thead
5 | tr role="row"
6 | th
7 | th Code
8 | th Name
9 | th Created at
10 | th Role
11 | th State
12 | th Progress
13 | th Actions
14 | tbody
15 | - @projects.each do |project|
16 | tr
17 | td style="width:54px"
18 | - if project.attachment?
19 | = image_tag project.attachment.url(:small_thumb), class: "img-rounded"
20 | td =link_to project.code, project_path(project)
21 | td =project.name
22 | td =project.created_at.strftime('%Y-%m-%d')
23 | td =user_role(project)
24 | td =project.state
25 | td
26 | = render partial: "projects/progress", locals: {project: project}
27 | td
28 | - if can?(current_user, :update_project, project)
29 | =link_to edit_project_path(project), class: "btn btn-primary btn-sm", title: "Edit project"
30 | i class="sign fa fa-pencil"
31 | |
32 | - if can?(current_user, :delete_project, project)
33 | =link_to project_path(project), data: { confirm: "Project will be removed. Are you sure?"}, method: :delete, class: "btn btn-danger btn-sm", title: "Delete project"
34 | i class="sign fa fa-trash-o bg-red"
35 | .box-footer.clearfix
36 | =paginate @projects
--------------------------------------------------------------------------------
/app/views/projects/edit.html.slim:
--------------------------------------------------------------------------------
1 | - @title="Edit project #{@project.name}"
2 | = render 'form'
--------------------------------------------------------------------------------
/app/views/projects/export.xlsx.axlsx:
--------------------------------------------------------------------------------
1 | wb = xlsx_package.workbook
2 | wb.add_worksheet(name: "Tasks") do |sheet|
3 | sheet.add_row [
4 | "Code",
5 | "Name",
6 | "Note",
7 | "Tags",
8 | "Assignee",
9 | "Original estimate",
10 | "Work logged",
11 | "Remaining estimate",
12 | "Delta",
13 | "Created at",
14 | "Updated at"
15 | ]
16 | @project.tasks.each do |task|
17 | sheet.add_row [
18 | task.code,
19 | task.name,
20 | task.description,
21 | task.tag_list,
22 | task.assignee.nil? ? "": task.assignee.name,
23 | task.original_estimate,
24 | task.work_logged,
25 | task.remaining_estimate,
26 | task.delta,
27 | task.created_at,
28 | task.updated_at]
29 | end
30 | end
31 |
32 | wb.add_worksheet(name: "Work") do |sheet|
33 | sheet.add_row [
34 | "Code",
35 | "Name",
36 | "Day",
37 | "Worked",
38 | "Created at",
39 | "Updated at"
40 | ]
41 | @project.tasks.each do |task|
42 | task.work_logs.each do |log|
43 | sheet.add_row [
44 | task.code,
45 | task.name,
46 | to_date(log.day),
47 | log.worked,
48 | log.created_at,
49 | log.updated_at]
50 | end
51 | end
52 | end
53 |
54 | wb.add_worksheet(name: "Snapshots") do |sheet|
55 | sheet.add_row [
56 | "Task count",
57 | "Original estimate",
58 | "Work logged",
59 | "Remaining estimate",
60 | "Delta",
61 | "Created at",
62 | "Updated at"
63 | ]
64 | @project.project_snapshots.each do |snap|
65 | sheet.add_row [
66 | snap.task_count,
67 | snap.original_estimate,
68 | snap.work_logged,
69 | snap.remaining_estimate,
70 | snap.delta,
71 | snap.created_at,
72 | snap.updated_at]
73 | end
74 | end
--------------------------------------------------------------------------------
/app/views/projects/index.html.slim:
--------------------------------------------------------------------------------
1 | - @title="Projects"
2 | .row
3 | .col-sm-2
4 | .form-group
5 | =link_to "Add a new project", new_project_path, class: "btn btn-primary btn-block"
6 | = render '/shared/projects_summary'
7 |
8 | .col-sm-10
9 | = render '/shared/projects_box'
10 |
11 |
12 | = render '/shared/project_card'
--------------------------------------------------------------------------------
/app/views/projects/members/index.html.slim:
--------------------------------------------------------------------------------
1 | - @title="Members"
2 | .row
3 | - if can?(current_user, :create_project_member, @project)
4 | .col-sm-2
5 | =link_to "Add members", new_project_member_path, class: "btn btn-primary btn-block", title: 'Add new members (a)', data: {toggle:'tooltip', keybinding: 'a'}
6 | .col-sm-10
7 | .box
8 | .box-body.table-responsive.no-padding
9 | table#members.table.table-hover
10 | thead
11 | tr role="row"
12 | th
13 | th Name
14 | th Member since
15 | th style="width:150px" Role
16 | th Actions
17 | tbody
18 | - @members.each do |member|
19 | tr
20 | td style="width:54px" = image_tag avatar_icon(member.email, 48), size: "48x48", class: "img-rounded"
21 | td = member.name
22 | td = date_only(member.created_at)
23 | td
24 | -if member.role == :admin || can?(current_user, :update_project_member, @project)
25 | = form_for(member, url: project_member_path(@project, member)) do |f|
26 | = f.select :role, options_for_select(@new_options, selected: member.role), {}, class: "form-control role-select"
27 | -else
28 | = member.role
29 | td
30 | -if can?(current_user, :delete_project_member, @project) && current_user.id != member.user_id
31 | =link_to project_member_path(id: member.id), data: { confirm: "Member will be removed from project. Are you sure?"}, method: :delete, class: "btn btn-danger btn-sm", title: "Remove member"
32 | i class="sign fa fa-trash-o bg-red"
33 | .box-footer.clearfix
34 | =paginate @members
35 |
--------------------------------------------------------------------------------
/app/views/projects/members/new.html.slim:
--------------------------------------------------------------------------------
1 | - @title="Add new members"
2 | = form_tag(project_members_path, method: :post) do
3 | .col-sm-8.col-md-6
4 | .box.box-solid
5 | .box-body
6 | - if @errors.any?
7 | .alert.alert-danger.alert-dismissable
8 | button aria-hidden="true" data-dismiss="alert" class="close" type="button" ×
9 | ul
10 | - @errors.each do |msg|
11 | li = msg
12 | .form-group
13 | = label_tag "id", "Choose the members"
14 | = hidden_field_tag "user-search", params['user-search'], style: "width:100%;display:block"
15 | .form-group
16 | = label_tag "role", "And select their role"
17 | select.form-control name="role"
18 | = options_for_select(@new_options, selected: params['role'])
19 | .box-footer.clearfix
20 | .pull-right
21 | =link_to "Cancel", project_members_path
22 | | or
23 | button.btn.btn-primary type="submit" Submit
24 | .col-sm-8.col-sm-offset-4.col-md-3.col-md-offset-0
--------------------------------------------------------------------------------
/app/views/projects/new.html.slim:
--------------------------------------------------------------------------------
1 | - @title="Create project"
2 | = render 'form'
--------------------------------------------------------------------------------
/app/views/projects/show_export.html.slim:
--------------------------------------------------------------------------------
1 | - @title = @project.name
2 | - @subtitle = @project.description
3 | .row
4 | .col-lg-12
5 | .box.box-primary
6 | .box-header
7 | h3.box-title Export data
8 | .box-body
9 | p
10 | | If you are anxious of losing your data or want to manipulate them to create your own reports,
11 | | you can export all your project data in Open Office XML format.
12 | p
13 | | To do so, just click on the button below :
14 | .box-footer
15 | =link_to "Export project data", project_export_path(@project, format: :xlsx), class: "btn btn-primary"
16 |
--------------------------------------------------------------------------------
/app/views/projects/stats/_total.html.slim:
--------------------------------------------------------------------------------
1 | tr
2 | th Total
3 | - total.each do |cell|
4 | th = cell
--------------------------------------------------------------------------------
/app/views/projects/stats/index.html.slim:
--------------------------------------------------------------------------------
1 | - @title="Stats"
2 | .row
3 | .col-sm-12
4 | .box
5 | .box-body
6 | form.navbar-form
7 | .form-group
8 | .input-group
9 | span.input-group-addon
10 | i.fa.fa-calendar
11 | input#periodPicker.form-control placeholder='Select date range...' style='width:190px'
12 |
13 | = render partial: '/shared/loading_spinner', locals: {hide_spinner: true}
14 | #results
15 | = render partial: '/shared/callout_info', locals: {title: "Select date range", description: 'You can select a date range to see the work log for the given period.'}
16 |
17 |
--------------------------------------------------------------------------------
/app/views/projects/stats/show.html.slim:
--------------------------------------------------------------------------------
1 | - if defined?(@error)
2 | = render partial: '/shared/callout_danger', locals: {title: 'Error', description: @error}
3 | - else
4 | .box
5 | .box-header
6 | i.fa.fa-line-chart
7 | h3.box-title Work Chart (in days)
8 | .box-body
9 | #chart.chart style="height: 350px;"
10 |
11 | .box
12 | .box-header
13 | i.fa.fa-th
14 | h3.box-title Work Grid
15 | .box-body
16 | table.table.table-bordered.table-striped
17 | thead
18 | tr
19 | th Day
20 | - @members.each do |member|
21 | th style='text-align:center'
22 | = image_tag avatar_icon(member.user.email, 40), size: '40', class: 'avatar', title: member.name, data: {toggle:'tooltip'}
23 | = render partial: 'total', locals: {total: @total}
24 | tbody
25 | - @results.each do |row|
26 | tr
27 | - row.each do |cell|
28 | td = cell
29 | tfoot
30 | = render partial: 'total', locals: {total: @total}
31 |
32 | javascript:
33 |
34 | Morris.Bar({
35 | element: 'chart',
36 | stacked: true,
37 | hideHover: true,
38 | data: #{raw @js_results.to_json()},
39 | xkey: 'day',
40 | ykeys: #{raw @members.map(&:email).as_json()},
41 | labels: #{raw @members.map(&:name).as_json()}
42 | });
--------------------------------------------------------------------------------
/app/views/projects/tasks/_task_card.html.slim:
--------------------------------------------------------------------------------
1 | #task-card-tpl.hide
2 | .card id='card-%%id%%'
3 | .progress data-toggle="tooltip" data-original-title="%%task-progress%%%"
4 | div class="progress-bar progress-bar-%%delta-color%%" style="width:%%task-progress%%%"
5 | .card-body
6 | div
7 | input class="card-check %%hide_cardcheck%%" type="checkbox" data-id="%%id%%"
8 | |
9 | a.card-code.card-clickable href='#taskModal' data-tab='informations' data-id="%%id%%" %%code%%
10 | img.card-avatar class="%%hide_avatar%%" data-toggle="tooltip" data-src="/users/%%user_id%%/avatar?size=30" title="%%username%%" width="30" height="30"
11 | div
12 | | %%name%%
13 | .card-bagdes
14 | a.card-badge.card-clickable href='#taskModal' data-tab='notes' data-id="%%id%%"
15 | | %%badge-description%%
16 | i.card-badge.task-work.fa.fa-clock-o title='%%work%%' data-toggle='tooltip'
17 |
18 | .card-tags
19 | | %%tags%%
20 |
--------------------------------------------------------------------------------
/app/views/projects/tasks/_task_dlg.html.slim:
--------------------------------------------------------------------------------
1 | #taskModal.modal.fade tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"
2 | .modal-dialog
3 | .modal-content
4 | .modal-header
5 | button.close type="button" data-dismiss="modal"
6 | span aria-hidden="true" ×
7 | span class="sr-only" Close
8 | h4.modal-title Edit Task
9 | .modal-body
10 | #task-errors.alert.alert-danger.alert-dismissable
11 | button aria-hidden="true" data-dismiss="alert" class="close" type="button" ×
12 | ul
13 | ul.nav.nav-tabs
14 | li
15 | a href="#tab_informations" data-toggle="tab" Informations
16 | li
17 | a href="#tab_notes" data-toggle="tab" Notes
18 | .tab-content
19 | #tab_informations.tab-pane
20 | form role="form"
21 | .form-group
22 | label ID
23 | input#task-id.form-control type="text" readonly="true"
24 | .form-group
25 | label Name
26 | input#task-name.form-control type="text"
27 | .form-group
28 | label Tags
29 | input#task-tags style="width:100%" type="hidden"
30 | .form-group
31 | label Assigned to
32 | select#task-assigned.form-control
33 | option value=""
34 | input#task-assigned-text.form-control type="text" readonly="true"
35 | .form-group
36 | label Estimate
37 | input#task-original.form-control type="text"
38 | .form-group
39 | label Remaining
40 | input#task-remaining.form-control type="text"
41 | #tab_notes.tab-pane
42 | form role="form"
43 | .form-group
44 | label Notes
45 | textarea#task-notes.form-control rows="6" placeholder="Enter notes about the task"
46 | .modal-footer
47 | button.btn.btn-default type="button" data-dismiss="modal" Close
48 | button#task-notes-save.btn.btn-primary type="button" Save changes
--------------------------------------------------------------------------------
/app/views/projects/tasks/index.html.slim:
--------------------------------------------------------------------------------
1 | - @title="Tasks"
2 | .row
3 | .col-lg-12
4 | = tasks_callout(@project)
5 | .row
6 | .col-sm-2
7 | - if can?(current_user, :create_project_task, @project)
8 | .form-group
9 | a#btn-add-task.btn.btn-primary.btn-block title="Add a new task (a)" data-toggle='tooltip' Add task
10 | - if can?(current_user, :delete_project_task, @project)
11 | .form-group
12 | a#btn-delete-task.btn.btn-danger.btn-block title="Delete selected tasks (d)" data-toggle='tooltip' Delete tasks
13 | .box.box-primary
14 | .box-header
15 | h3.box-title Summary
16 | .box-body
17 | | Tasks
18 | span#tasks_count.badge.pull-right 0
19 | br
20 | | Estimate
21 | span#tasks_estimate.badge.pull-right 0
22 | br
23 | | Logged
24 | span#tasks_logged.badge.pull-right 0
25 | br
26 | | Remaining
27 | span#tasks_remaining.badge.pull-right 0
28 | br
29 | | Delta
30 | span#tasks_delta.badge.pull-right 0
31 | .col-sm-10
32 | .row
33 | .col-sm-12
34 | .box
35 | .box-body
36 | form.navbar-form
37 | .form-group
38 | .btn-group
39 | button#show-cards.btn.btn-primary.active type="button" title='Cards view' data-toggle='tooltip'
40 | span class="fa fa-th-large"
41 | button#show-sheet.btn.btn-default type="button" title='Spreadsheet view' data-toggle='tooltip'
42 | span class="fa fa-table"
43 | |
44 | .form-group
45 | input#tasksFilter.form-control type='search' placeholder="Search..."
46 | |
47 | select#tasksSort.form-control
48 | option value="updated_at" data-order="desc" Sort by update time
49 | option value="created_at" data-order="desc" Sort by creation time
50 | option value="name" data-order="asc" Sort by name
51 | option value="original_estimate" data-order="desc" Sort by estimate
52 | option value="remaining_estimate" data-order="desc" Sort by remaining
53 | option value="delta" data-order="asc" Sort by delta
54 |
55 | |
56 | button#display_completed.btn.btn-default.hide type="button" title='Show completed tasks' data-toggle='tooltip'
57 | span class="fa fa-eye"
58 | .row
59 | .col-sm-12.handsontable-row
60 | = render '/shared/loading_spinner'
61 | #tab-cards
62 | .row
63 | .col-md-3
64 | .box
65 | .box-header
66 | h3.box-title To Do
67 | #tasks-cards-todo.box-body
68 | .col-md-3
69 | .box
70 | .box-header
71 | h3.box-title Assigned
72 | #tasks-cards-assigned.box-body
73 | .col-md-3
74 | .box
75 | .box-header
76 | h3.box-title In Progress
77 | #tasks-cards-inprogress.box-body
78 | .col-md-3
79 | .box
80 | .box-header
81 | h3.box-title Done
82 | #tasks-cards-done.box-body
83 |
84 | .row
85 | #tasksCards
86 | #tab-sheet
87 | #tasksGrid
88 |
89 | = render 'task_card'
90 | = render 'task_dlg'
--------------------------------------------------------------------------------
/app/views/projects/timesheets/edit.html.slim:
--------------------------------------------------------------------------------
1 | - @title="Timesheet"
2 | - unless @project.opened?
3 | .row
4 | .col-sm-12
5 | .alert.alert-warning Project is not opened, you can't edit timesheets.
6 | .row
7 | .col-sm-12
8 | .box
9 | .box-body
10 | form.form-inline
11 | .form-group
12 | select#members.form-control title='Select member' data-toggle='tooltip'
13 | = options_for_select(@project.project_members.by_name.collect {|m| [m.username, m.user.id]}, current_user.id)
14 | .form-group
15 | |
16 | .input-group
17 | input#periodInput.form-control name="period" readonly="readonly" value=(format_period(@period_start, @period_end)) style="width:200px"
18 | span.input-group-btn
19 | button#previousPeriod.btn.btn-default.btn-flat type="button" title="Previous period"
20 | i.fa.fa-caret-left
21 | span.input-group-btn
22 | button#nextPeriod.btn.btn-default.btn-flat type="button" title="Next period"
23 | i.fa.fa-caret-right
24 | span#dpPeriod.input-append.date.input-group-btn data-date=(@period_start.strftime("%d-%m-%Y")) data-date-format="dd-mm-yyyy"
25 | button.btn.btn-default.btn-flat type="button" title="Choose"
26 | i.fa.fa-caret-down
27 | |
28 | input#filter.form-control type='search' placeholder="Search..."
29 | |
30 | .form-group
31 | button#display_completed.btn.btn-default type="button" title='Show completed tasks' data-toggle='tooltip'
32 | span class="fa fa-eye"
33 | .col-sm-8
34 | .row
35 | .col-sm-12
36 | #timesheetGrid
37 | #calloutNoTasks style='display:none'
38 | = render partial: '/shared/callout_info', locals: {title: 'No tasks', description: 'No task has been assigned to the selected user.'}
39 | #calloutNoVisibleTasks style='display:none'
40 | = render partial: '/shared/callout_info', locals: {title: 'No tasks', description: 'No task to display. Clic on the eye button above to see completed tasks.'}
41 |
42 |
--------------------------------------------------------------------------------
/app/views/shared/_callout_danger.html.slim:
--------------------------------------------------------------------------------
1 | .callout.callout-danger
2 | h4 = title
3 | p = description
4 |
--------------------------------------------------------------------------------
/app/views/shared/_callout_info.html.slim:
--------------------------------------------------------------------------------
1 | .callout.callout-info
2 | h4 = title
3 | p = description
4 |
--------------------------------------------------------------------------------
/app/views/shared/_loading_spinner.html.slim:
--------------------------------------------------------------------------------
1 | - hide_spinner = false unless local_assigns.has_key?(:hide_spinner)
2 | #loadingSpinner style=('display:none' if hide_spinner)
3 | i.fa.fa-cog.fa-spin
4 | | loading...
--------------------------------------------------------------------------------
/app/views/shared/_project_card.html.slim:
--------------------------------------------------------------------------------
1 | #project-card-tpl.hide
2 | div class='%%card-item%%'
3 | .inner style="height:235px"
4 | .front
5 | .thumbnail.project-card-no-members id='project-%%id%%'
6 | .progress.xs data-toggle="tooltip" data-original-title="%%progress%%%"
7 | div class="progress-bar progress-bar-%%progress_color%%" style="width:%%progress%%%"
8 | .card-body
9 | i.fa.fa-info-circle.project-info.flip-on title='click to flip me' data-toggle='tooltip' data-project_id='%%id%%'
10 | div style='text-align:center'
11 | img class='picture' data-src='%%picture_url%%' width='160' height='180' title='%%description%%' data-toggle='tooltip'
12 | a.truncate href='/projects/%%id%%' title='%%name%% (%%code%%)' data-toggle='tooltip' %%name%%
13 | .back
14 | .thumbnail.project-card-no-members id='project-%%id%%'
15 | .card-body
16 | i.fa.fa-info-circle.project-info.flip-off title='click to flip me' data-toggle='tooltip' data-project_id='%%id%%'
17 | ul
18 | li state : %%state%%
19 | li created : %%created%%
20 | li estimate : %%original_estimate%%
21 | li logged : %%work_logged%%
22 | li remaining : %%remaining_estimate%%
23 | li delta : %%delta%%
24 | .members
25 |
26 |
--------------------------------------------------------------------------------
/app/views/shared/_projects_box.html.slim:
--------------------------------------------------------------------------------
1 | .box
2 | .box-body
3 | form.navbar-form
4 | .form-group
5 | input#projectsFilter.form-control type='search' placeholder="Search..."
6 | |
7 | select#projectsSort.form-control
8 | option value="name" data-order="asc" Sort by name
9 | option value="delta" data-order="asc" Sort by delta
10 | option value="original_estimate" data-order="asc" Sort by original estimate
11 | option value="remaining_estimate" data-order="asc" Sort by remaining estimate
12 | option value="work_logged" data-order="asc" Sort by work logged
13 | option value="created_at" data-order="desc" Sort by creation time
14 | |
15 | button#display_closed.btn.btn-default type="button" title='Show closed projects' data-toggle='tooltip'
16 | span class="fa fa-eye"
17 | = render '/shared/loading_spinner'
18 | #projects
--------------------------------------------------------------------------------
/app/views/shared/_projects_summary.html.slim:
--------------------------------------------------------------------------------
1 | .box.box-primary
2 | .box-header
3 | h3.box-title Summary
4 | .box-body
5 | | Projects
6 | span#projects_count.badge.pull-right 0
7 | br
8 | | Estimate
9 | span#projects_estimate.badge.pull-right 0
10 | br
11 | | Logged
12 | span#projects_logged.badge.pull-right 0
13 | br
14 | | Remaining
15 | span#projects_remaining.badge.pull-right 0
16 | br
17 | | Delta
18 | span#projects_delta.badge.pull-right 0
--------------------------------------------------------------------------------
/app/views/users/avatar.text.slim:
--------------------------------------------------------------------------------
1 | =avatar_icon(@user.email, @size)
--------------------------------------------------------------------------------
/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3 | load Gem.bin_path('bundler', 'bundle')
4 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path('../../config/application', __FILE__)
3 | require_relative '../config/boot'
4 | require 'rails/commands'
5 |
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require_relative '../config/boot'
3 | require 'rake'
4 | Rake.application.run
5 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require ::File.expand_path('../config/environment', __FILE__)
4 | run Rails.application
5 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | require 'rails/all'
4 |
5 | # Require the gems listed in Gemfile, including any gems
6 | # you've limited to :test, :development, or :production.
7 | Bundler.require(*Rails.groups)
8 |
9 | module Olb
10 | class Application < Rails::Application
11 | # Settings in config/environments/* take precedence over those specified here.
12 | # Application configuration should go into files in config/initializers
13 | # -- all .rb files in that directory are automatically loaded.
14 |
15 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
16 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
17 | # config.time_zone = 'Central Time (US & Canada)'
18 |
19 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
20 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
21 | # config.i18n.default_locale = :de
22 |
23 | config.exceptions_app = self.routes
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | # Set up gems listed in the Gemfile.
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3 |
4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
5 |
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 |
2 | default: &default
3 | adapter: mysql2
4 | pool: <%= ENV.fetch("DB_POOL") { 20 } %>
5 | database: <%= ENV.fetch("DB_NAME") { "sofreaking" } %>
6 | username: <%= ENV.fetch("DB_USERNAME") { "root" } %>
7 | password: <%= ENV.fetch("DB_PASSWORD") { "" } %>
8 | host: <%= ENV.fetch("DB_HOST") { "localhost" } %>
9 | port: <%= ENV.fetch("DB_PORT") { 3306 } %>
10 | encoding: utf8
11 | secure_auth: false
12 |
13 | development:
14 | <<: *default
15 |
16 | # Warning: The database defined as "test" will be erased and
17 | # re-generated from your development database when you run "rake".
18 | # Do not set this db to the same as development or production.
19 | test:
20 | <<: *default
21 | database: <%= ENV.fetch("DB_TEST_NAME") { "sofreaking_test" } %>
22 |
23 | production:
24 | <<: *default
25 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the Rails application.
5 | Olb::Application.initialize!
6 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Olb::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Do not eager load code on boot.
10 | config.eager_load = false
11 |
12 | # Show full error reports and disable caching.
13 | config.consider_all_requests_local = true
14 | config.action_controller.perform_caching = false
15 |
16 | # Don't care if the mailer can't send.
17 | config.action_mailer.raise_delivery_errors = false
18 |
19 | # Print deprecation notices to the Rails logger.
20 | config.active_support.deprecation = :log
21 |
22 | # Raise an error on page load if there are pending migrations
23 | config.active_record.migration_error = :page_load
24 |
25 | # Debug mode disables concatenation and preprocessing of assets.
26 | # This option may cause significant delays in view rendering with a large
27 | # number of complex assets.
28 | config.assets.debug = true
29 |
30 | config.action_mailer.default_url_options = { host: 'localhost:3000' }
31 |
32 | config.after_initialize do
33 | Bullet.enable = true
34 | Bullet.alert = false
35 | Bullet.bullet_logger = true
36 | Bullet.console = true
37 | Bullet.growl = false
38 | Bullet.rails_logger = true
39 | Bullet.add_footer = false
40 | Bullet.stacktrace_includes = [ 'your_gem', 'your_middleware' ]
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Olb::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # Code is not reloaded between requests.
5 | config.cache_classes = true
6 |
7 | # Eager load code on boot. This eager loads most of Rails and
8 | # your application in memory, allowing both thread web servers
9 | # and those relying on copy on write to perform better.
10 | # Rake tasks automatically ignore this option for performance.
11 | config.eager_load = true
12 |
13 | # Full error reports are disabled and caching is turned on.
14 | config.consider_all_requests_local = false
15 | config.action_controller.perform_caching = true
16 |
17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application
18 | # Add `rack-cache` to your Gemfile before enabling this.
19 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid.
20 | # config.action_dispatch.rack_cache = true
21 |
22 | # Disable Rails's static asset server (Apache or nginx will already do this).
23 | config.serve_static_files = true
24 |
25 | # Compress JavaScripts and CSS.
26 | config.assets.js_compressor = :uglifier
27 | # config.assets.css_compressor = :sass
28 |
29 | # Do not fallback to assets pipeline if a precompiled asset is missed.
30 | config.assets.compile = true
31 |
32 | # Generate digests for assets URLs.
33 | config.assets.digest = true
34 |
35 | # Version of your assets, change this if you want to expire all your assets.
36 | config.assets.version = '1.0'
37 |
38 | # Specifies the header that your server uses for sending files.
39 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
40 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
41 |
42 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
43 | # config.force_ssl = true
44 |
45 | # Set to :debug to see everything in the log.
46 | config.log_level = :info
47 |
48 | # Prepend all log lines with the following tags.
49 | # config.log_tags = [ :subdomain, :uuid ]
50 |
51 | # Use a different logger for distributed setups.
52 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
53 |
54 | # Use a different cache store in production.
55 | # config.cache_store = :mem_cache_store
56 |
57 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
58 | # config.action_controller.asset_host = "http://assets.example.com"
59 |
60 | # Precompile additional assets.
61 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
62 | # config.assets.precompile += %w( search.js )
63 |
64 | # Ignore bad email addresses and do not raise email delivery errors.
65 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
66 | # config.action_mailer.raise_delivery_errors = false
67 |
68 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
69 | # the I18n.default_locale when a translation can not be found).
70 | config.i18n.fallbacks = true
71 |
72 | # Send deprecation notices to registered listeners.
73 | config.active_support.deprecation = :notify
74 |
75 | # Disable automatic flushing of the log to improve performance.
76 | # config.autoflush_log = false
77 |
78 | # Use default logging formatter so that PID and timestamp are not suppressed.
79 | config.log_formatter = ::Logger::Formatter.new
80 |
81 | config.action_mailer.default_url_options = { host: 'www.sofreakingboring.com' }
82 | end
83 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Olb::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Do not eager load code on boot. This avoids loading your whole application
11 | # just for the purpose of running a single test. If you are using a tool that
12 | # preloads Rails for running tests, you may have to set it to true.
13 | config.eager_load = false
14 |
15 | # Configure static asset server for tests with Cache-Control for performance.
16 | config.serve_static_files = true
17 | config.static_cache_control = "public, max-age=3600"
18 |
19 | # Show full error reports and disable caching.
20 | config.consider_all_requests_local = true
21 | config.action_controller.perform_caching = false
22 |
23 | # Raise exceptions instead of rendering exception templates.
24 | config.action_dispatch.show_exceptions = false
25 |
26 | # Disable request forgery protection in test environment.
27 | config.action_controller.allow_forgery_protection = false
28 |
29 | # Tell Action Mailer not to deliver emails to the real world.
30 | # The :test delivery method accumulates sent emails in the
31 | # ActionMailer::Base.deliveries array.
32 | config.action_mailer.delivery_method = :test
33 |
34 | # Print deprecation notices to the stderr.
35 | config.active_support.deprecation = :stderr
36 | end
37 |
--------------------------------------------------------------------------------
/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure sensitive parameters which will be filtered from the log file.
4 | Rails.application.config.filter_parameters += [:password]
5 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, '\1en'
8 | # inflect.singular /^(ox)en/i, '\1'
9 | # inflect.irregular 'person', 'people'
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym 'RESTful'
16 | # end
17 |
--------------------------------------------------------------------------------
/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 | # Mime::Type.register_alias "text/html", :iphone
6 |
--------------------------------------------------------------------------------
/config/initializers/omniauth.rb:
--------------------------------------------------------------------------------
1 | Rails.application.config.middleware.use OmniAuth::Builder do
2 | provider :developer unless Rails.env.production?
3 | provider :github, ENV['GITHUB_KEY'], ENV['GITHUB_SECRET'], scope: 'email,profile'
4 | provider :facebook, ENV['FACEBOOK_KEY'], ENV['FACEBOOK_SECRET']
5 | provider :google_oauth2, ENV['GOOGLE_KEY'], ENV['GOOGLE_SECRET']
6 | end
7 |
--------------------------------------------------------------------------------
/config/initializers/secret_token.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key is used for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 |
6 | # Make sure the secret is at least 30 characters and all random,
7 | # no regular words or you'll be exposed to dictionary attacks.
8 | # You can use `rake secret` to generate a secure secret key.
9 |
10 | # Make sure your secret_key_base is kept private
11 | # if you're sharing your code publicly.
12 |
13 | token = ENV['SECRET_TOKEN']
14 | unless token
15 | token_file = Rails.root.join('.secret')
16 | if File.exist? token_file
17 | token = File.read(token_file).chomp
18 | else
19 | token = SecureRandom.hex(64)
20 | File.write(token_file, token)
21 | end
22 | end
23 |
24 | Olb::Application.config.secret_token = token
25 | Olb::Application.config.secret_key_base = token
26 |
--------------------------------------------------------------------------------
/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Olb::Application.config.session_store :cookie_store, key: '_olb_session'
4 |
--------------------------------------------------------------------------------
/config/initializers/state_machine_hack.rb:
--------------------------------------------------------------------------------
1 | # Rails 4.1.0.rc1 and StateMachine don't play nice
2 |
3 | require 'state_machine/version'
4 |
5 | unless StateMachine::VERSION == '1.2.0'
6 | # If you see this message, please test removing this file
7 | # If it's still required, please bump up the version above
8 | Rails.logger.warn "Please remove me, StateMachine version has changed"
9 | end
10 |
11 | module StateMachine::Integrations::ActiveModel
12 | alias_method :around_validation_protected, :around_validation
13 | def around_validation(*args, &block)
14 | around_validation_protected(*args, &block)
15 | end
16 | end
--------------------------------------------------------------------------------
/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters)
9 | end
10 |
11 | # To enable root element in JSON for ActiveRecord objects.
12 | # ActiveSupport.on_load(:active_record) do
13 | # self.include_root_in_json = true
14 | # end
15 |
--------------------------------------------------------------------------------
/config/locales/devise.en.yml:
--------------------------------------------------------------------------------
1 | # Additional translations at https://github.com/plataformatec/devise/wiki/I18n
2 |
3 | en:
4 | devise:
5 | confirmations:
6 | confirmed: "Your account was successfully confirmed."
7 | send_instructions: "You will receive an email with instructions about how to confirm your account in a few minutes."
8 | send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions about how to confirm your account in a few minutes."
9 | failure:
10 | already_authenticated: "You are already signed in."
11 | inactive: "Your account is not activated yet."
12 | invalid: "Invalid email or password."
13 | locked: "Your account is locked."
14 | last_attempt: "You have one more attempt before your account will be locked."
15 | not_found_in_database: "Invalid email or password."
16 | timeout: "Your session expired. Please sign in again to continue."
17 | unauthenticated: "You need to sign in or sign up before continuing."
18 | unconfirmed: "You have to confirm your account before continuing."
19 | mailer:
20 | confirmation_instructions:
21 | subject: "Confirmation instructions"
22 | reset_password_instructions:
23 | subject: "Reset password instructions"
24 | unlock_instructions:
25 | subject: "Unlock Instructions"
26 | omniauth_callbacks:
27 | failure: "Could not authenticate you from %{kind} because \"%{reason}\"."
28 | success: "Successfully authenticated from %{kind} account."
29 | passwords:
30 | no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided."
31 | send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes."
32 | send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes."
33 | updated: "Your password was changed successfully. You are now signed in."
34 | updated_not_active: "Your password was changed successfully."
35 | registrations:
36 | destroyed: "Bye! Your account was successfully cancelled. We hope to see you again soon."
37 | signed_up: "Welcome! You have signed up successfully."
38 | signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated."
39 | signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked."
40 | signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please open the link to activate your account."
41 | update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and click on the confirm link to finalize confirming your new email address."
42 | updated: "You updated your account successfully."
43 | sessions:
44 | signed_in: "Signed in successfully."
45 | signed_out: "Signed out successfully."
46 | unlocks:
47 | send_instructions: "You will receive an email with instructions about how to unlock your account in a few minutes."
48 | send_paranoid_instructions: "If your account exists, you will receive an email with instructions about how to unlock it in a few minutes."
49 | unlocked: "Your account has been unlocked successfully. Please sign in to continue."
50 | errors:
51 | messages:
52 | already_confirmed: "was already confirmed, please try signing in"
53 | confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one"
54 | expired: "has expired, please request a new one"
55 | not_found: "not found"
56 | not_locked: "was not locked"
57 | not_saved:
58 | one: "1 error prohibited this %{resource} from being saved:"
59 | other: "%{count} errors prohibited this %{resource} from being saved:"
60 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # To learn more, please read the Rails Internationalization guide
20 | # available at http://guides.rubyonrails.org/i18n.html.
21 |
22 | en:
23 | hello: "Hello world"
24 |
--------------------------------------------------------------------------------
/config/puma.rb:
--------------------------------------------------------------------------------
1 | # Puma can serve each request in a thread from an internal thread pool.
2 | # The `threads` method setting takes two numbers: a minimum and maximum.
3 | # Any libraries that use thread pools should be configured to match
4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
5 | # and maximum; this matches the default thread size of Active Record.
6 | #
7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
8 | threads threads_count, threads_count
9 |
10 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
11 | #
12 | port ENV.fetch("PORT") { 3000 }
13 |
14 | # Specifies the `environment` that Puma will run in.
15 | #
16 | environment ENV.fetch("RAILS_ENV") { "development" }
17 |
18 | # Specifies the number of `workers` to boot in clustered mode.
19 | # Workers are forked webserver processes. If using threads and workers together
20 | # the concurrency of the application would be max `threads` * `workers`.
21 | # Workers do not work on JRuby or Windows (both of which do not support
22 | # processes).
23 | #
24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
25 |
26 | # Use the `preload_app!` method when specifying a `workers` number.
27 | # This directive tells Puma to first boot the application and load code
28 | # before forking the application. This takes advantage of Copy On Write
29 | # process behavior so workers use less memory. If you use this option
30 | # you need to make sure to reconnect any threads in the `on_worker_boot`
31 | # block.
32 | #
33 | # preload_app!
34 |
35 | # If you are preloading your application and using Active Record, it's
36 | # recommended that you close any connections to the database before workers
37 | # are forked to prevent connection leakage.
38 | #
39 | # before_fork do
40 | # ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord)
41 | # end
42 |
43 | # The code in the `on_worker_boot` will be called if you are using
44 | # clustered mode by specifying a number of `workers`. After each worker
45 | # process is booted, this block will be run. If you are using the `preload_app!`
46 | # option, you will want to use this block to reconnect to any threads
47 | # or connections that may have been created at application boot, as Ruby
48 | # cannot share connections between processes.
49 | #
50 | # on_worker_boot do
51 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
52 | # end
53 | #
54 |
55 | # Allow puma to be restarted by `rails restart` command.
56 | plugin :tmp_restart
57 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | require 'api/api'
2 |
3 | Olb::Application.routes.draw do
4 |
5 | # API
6 | API::API.logger Rails.logger
7 | mount API::API => '/api'
8 |
9 | devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" }
10 |
11 | root :to => 'home#index'
12 |
13 | get "/404", :to => "errors#not_found"
14 | get "/500", :to => "errors#internal_error"
15 |
16 | get "/work_for_period" => 'home#work_for_period', as: 'work_for_period'
17 |
18 | resource :profile, only: [:show, :edit, :update] do
19 | scope module: :profiles do
20 | resource :avatar, only: [:destroy]
21 | resource :password, only: [:edit, :update]
22 | end
23 | end
24 |
25 | get "/users/:id/avatar" => "users#avatar"
26 |
27 |
28 | resources :projects do
29 | put :remove_attachment
30 | get :show_export
31 | get :export
32 | scope module: :projects do
33 | resources :members
34 | resources :tasks
35 | resources :stats, only: [:index, :show]
36 | resource :timesheet do
37 | get :tasks
38 | end
39 | end
40 | end
41 |
42 | namespace :admin do
43 | resources :projects, only: [:index]
44 | resources :users do
45 | put :remove_avatar
46 | end
47 | resources :settings, only: [:index, :edit]
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/config/schedule.rb:
--------------------------------------------------------------------------------
1 |
2 | every 1.day, :at => '1:00 am' do
3 | runner "Project.snapshot"
4 | end
5 |
--------------------------------------------------------------------------------
/db/migrate/20140426173644_devise_create_users.rb:
--------------------------------------------------------------------------------
1 | class DeviseCreateUsers < ActiveRecord::Migration
2 | def change
3 | create_table(:users) do |t|
4 | ## Database authenticatable
5 | t.string :email, null: false, default: ""
6 | t.string :encrypted_password, null: false, default: ""
7 |
8 | ## Recoverable
9 | t.string :reset_password_token
10 | t.datetime :reset_password_sent_at
11 |
12 | ## Rememberable
13 | t.datetime :remember_created_at
14 |
15 | ## Trackable
16 | t.integer :sign_in_count, default: 0, null: false
17 | t.datetime :current_sign_in_at
18 | t.datetime :last_sign_in_at
19 | t.string :current_sign_in_ip
20 | t.string :last_sign_in_ip
21 |
22 | ## Confirmable
23 | # t.string :confirmation_token
24 | # t.datetime :confirmed_at
25 | # t.datetime :confirmation_sent_at
26 | # t.string :unconfirmed_email # Only if using reconfirmable
27 |
28 | ## Lockable
29 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
30 | # t.string :unlock_token # Only if unlock strategy is :email or :both
31 | # t.datetime :locked_at
32 |
33 |
34 | t.timestamps
35 | end
36 |
37 | add_index :users, :email, unique: true
38 | add_index :users, :reset_password_token, unique: true
39 | # add_index :users, :confirmation_token, unique: true
40 | # add_index :users, :unlock_token, unique: true
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/db/migrate/20140428100928_add_name_and_bio_to_user.rb:
--------------------------------------------------------------------------------
1 | class AddNameAndBioToUser < ActiveRecord::Migration
2 | def change
3 | add_column :users, :name, :string, default: '', null: false
4 | add_column :users, :bio, :string, default: '', null: false
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/db/migrate/20140428102407_add_avatar_to_user.rb:
--------------------------------------------------------------------------------
1 | class AddAvatarToUser < ActiveRecord::Migration
2 | def change
3 | add_attachment :users, :avatar
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20140428131010_add_admin_to_user.rb:
--------------------------------------------------------------------------------
1 | class AddAdminToUser < ActiveRecord::Migration
2 | def change
3 | add_column :users, :admin, :boolean, default: false, null: false
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20140504095518_create_projects.rb:
--------------------------------------------------------------------------------
1 | class CreateProjects < ActiveRecord::Migration
2 | def change
3 | create_table :projects do |t|
4 | t.string :code, null: false, default: ""
5 | t.string :name, null: false, default: ""
6 | t.text :description
7 | t.timestamps
8 | end
9 |
10 | add_attachment :projects, :attachment
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/db/migrate/20140504095531_create_project_members.rb:
--------------------------------------------------------------------------------
1 | class CreateProjectMembers < ActiveRecord::Migration
2 | def change
3 | create_table :project_members do |t|
4 | t.integer :user_id, null: false
5 | t.integer :project_id, null: false
6 | t.integer :role, default: 0, null: false
7 | t.timestamps
8 | end
9 |
10 | add_index(:project_members, :user_id)
11 | add_index(:project_members, :project_id)
12 | add_index(:project_members, [:user_id, :project_id], unique: true)
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/db/migrate/20140506134955_add_authentication_token_to_users.rb:
--------------------------------------------------------------------------------
1 | class AddAuthenticationTokenToUsers < ActiveRecord::Migration
2 | def change
3 | add_column :users, :authentication_token, :string
4 | add_index :users, :authentication_token, unique: true
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/db/migrate/20140507095311_create_tasks.rb:
--------------------------------------------------------------------------------
1 | class CreateTasks < ActiveRecord::Migration
2 | def change
3 | create_table :tasks do |t|
4 | t.string :name, null: false, default: ""
5 | t.text :description
6 | t.integer :original_estimate
7 | t.integer :remaining_estimate
8 | t.integer :project_id, null: false
9 | t.integer :assignee_id
10 | t.timestamps
11 | end
12 |
13 | add_index(:tasks, :assignee_id)
14 | add_index(:tasks, :project_id)
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/db/migrate/20140507105253_create_work_logs.rb:
--------------------------------------------------------------------------------
1 | class CreateWorkLogs < ActiveRecord::Migration
2 | def change
3 | create_table :work_logs do |t|
4 | t.string :description
5 | t.string :day
6 | t.integer :worked
7 | t.integer :task_id
8 | t.timestamps
9 | end
10 |
11 | add_index(:work_logs, :task_id)
12 | end
13 | end
--------------------------------------------------------------------------------
/db/migrate/20140617153407_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb:
--------------------------------------------------------------------------------
1 | # This migration comes from acts_as_taggable_on_engine (originally 1)
2 | class ActsAsTaggableOnMigration < ActiveRecord::Migration
3 | def self.up
4 | create_table :tags do |t|
5 | t.string :name
6 | end
7 |
8 | create_table :taggings do |t|
9 | t.references :tag
10 |
11 | # You should make sure that the column created is
12 | # long enough to store the required class names.
13 | t.references :taggable, polymorphic: true
14 | t.references :tagger, polymorphic: true
15 |
16 | # Limit is created to prevent MySQL error on index
17 | # length for MyISAM table type: http://bit.ly/vgW2Ql
18 | t.string :context, limit: 128
19 |
20 | t.datetime :created_at
21 | end
22 |
23 | add_index :taggings, :tag_id
24 | add_index :taggings, [:taggable_id, :taggable_type, :context]
25 | end
26 |
27 | def self.down
28 | drop_table :taggings
29 | drop_table :tags
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/db/migrate/20140617153408_add_missing_unique_indices.acts_as_taggable_on_engine.rb:
--------------------------------------------------------------------------------
1 | # This migration comes from acts_as_taggable_on_engine (originally 2)
2 | class AddMissingUniqueIndices < ActiveRecord::Migration
3 | def self.up
4 | add_index :tags, :name, unique: true
5 |
6 | remove_index :taggings, :tag_id
7 | remove_index :taggings, [:taggable_id, :taggable_type, :context]
8 | add_index :taggings,
9 | [:tag_id, :taggable_id, :taggable_type, :context, :tagger_id, :tagger_type],
10 | unique: true, name: 'taggings_idx'
11 | end
12 |
13 | def self.down
14 | remove_index :tags, :name
15 |
16 | remove_index :taggings, name: 'taggings_idx'
17 | add_index :taggings, :tag_id
18 | add_index :taggings, [:taggable_id, :taggable_type, :context]
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/db/migrate/20140617153409_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb:
--------------------------------------------------------------------------------
1 | # This migration comes from acts_as_taggable_on_engine (originally 3)
2 | class AddTaggingsCounterCacheToTags < ActiveRecord::Migration
3 | def self.up
4 | add_column :tags, :taggings_count, :integer, default: 0
5 |
6 | ActsAsTaggableOn::Tag.reset_column_information
7 | ActsAsTaggableOn::Tag.find_each do |tag|
8 | ActsAsTaggableOn::Tag.reset_counters(tag.id, :taggings)
9 | end
10 | end
11 |
12 | def self.down
13 | remove_column :tags, :taggings_count
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/db/migrate/20140804195208_add_state_to_project.rb:
--------------------------------------------------------------------------------
1 | class AddStateToProject < ActiveRecord::Migration
2 | def change
3 | add_column :projects, :state, :string
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20140804213245_create_versions.rb:
--------------------------------------------------------------------------------
1 | class CreateVersions < ActiveRecord::Migration
2 | def change
3 | create_table :versions do |t|
4 | t.string :item_type, :null => false
5 | t.integer :item_id, :null => false
6 | t.string :event, :null => false
7 | t.string :whodunnit
8 | t.text :object
9 | t.datetime :created_at
10 | end
11 | add_index :versions, [:item_type, :item_id]
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/db/migrate/20140810120406_create_project_snapshots.rb:
--------------------------------------------------------------------------------
1 | class CreateProjectSnapshots < ActiveRecord::Migration
2 | def change
3 | create_table :project_snapshots do |t|
4 | t.integer :project_id, :null => false
5 | t.integer :task_count
6 | t.integer :original_estimate
7 | t.integer :work_logged
8 | t.integer :remaining_estimate
9 | t.timestamps
10 | end
11 | add_index :project_snapshots, :project_id
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/db/migrate/20140821204235_create_project_openings.rb:
--------------------------------------------------------------------------------
1 | class CreateProjectOpenings < ActiveRecord::Migration
2 | def change
3 | create_table :project_openings do |t|
4 | t.integer :project_id, :null => false
5 | t.integer :user_id, :null => false
6 | t.integer :touched
7 | t.timestamps
8 | end
9 | add_index :project_openings, :project_id
10 | add_index :project_openings, :user_id
11 | add_index :project_openings, [:user_id, :project_id], unique: true
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/db/migrate/20140824133531_change_times_to_seconds.rb:
--------------------------------------------------------------------------------
1 | class ChangeTimesToSeconds < ActiveRecord::Migration
2 | def change
3 | Task.update_all( 'original_estimate = original_estimate * 3600, remaining_estimate = remaining_estimate * 3600' )
4 | WorkLog.update_all( 'worked = worked * 3600')
5 | ProjectSnapshot.update_all( 'original_estimate = original_estimate * 3600, remaining_estimate = remaining_estimate * 3600, work_logged = work_logged * 3600' )
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20140923195808_create_identities.rb:
--------------------------------------------------------------------------------
1 | class CreateIdentities < ActiveRecord::Migration
2 | def change
3 | create_table :identities do |t|
4 | t.string :provider, null: false
5 | t.string :uid, null: false
6 | t.integer :user_id, null: false
7 | t.timestamps
8 | end
9 | add_index :identities, [:provider, :uid], unique: true
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/db/migrate/20141031210039_add_internal_ids_to_tasks.rb:
--------------------------------------------------------------------------------
1 | class AddInternalIdsToTasks < ActiveRecord::Migration
2 | def change
3 | add_column :tasks, :iid, :integer
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20141031223050_change_times_to_minutes.rb:
--------------------------------------------------------------------------------
1 | class ChangeTimesToMinutes < ActiveRecord::Migration
2 | def change
3 | Task.update_all( 'original_estimate = original_estimate / 60, remaining_estimate = remaining_estimate / 60' )
4 | WorkLog.update_all( 'worked = worked / 60')
5 | ProjectSnapshot.update_all( 'original_estimate = original_estimate / 60, remaining_estimate = remaining_estimate / 60, work_logged = work_logged / 60' )
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # This file should contain all the record creation needed to seed the database with its default values.
2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
3 | #
4 | # Examples:
5 | #
6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
7 | # Mayor.create(name: 'Emanuel', city: cities.first)
8 |
--------------------------------------------------------------------------------
/lib/api/api.rb:
--------------------------------------------------------------------------------
1 | Dir["#{Rails.root}/lib/api/*.rb"].each {|file| require file}
2 |
3 | module API
4 | class API < Grape::API
5 | version 'v1', using: :path
6 |
7 | rescue_from ActiveRecord::RecordNotFound do
8 | rack_response({'message' => '404 Not found'}.to_json, 404)
9 | end
10 |
11 | rescue_from :all do |exception|
12 | trace = exception.backtrace
13 |
14 | message = "\n#{exception.class} (#{exception.message}):\n"
15 | message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
16 | message << " " << trace.join("\n ")
17 |
18 | API.logger.add Logger::FATAL, message
19 | rack_response({'message' => '500 Internal Server Error'}, 500)
20 | end
21 |
22 | format :json
23 | content_type :txt, "text/plain"
24 |
25 | helpers APIHelpers
26 |
27 | mount Users
28 | mount Projects
29 | mount ProjectMembers
30 | mount ProjectSnapshots
31 | mount Tasks
32 | mount WorkLogs
33 | mount Timesheets
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/api/entities.rb:
--------------------------------------------------------------------------------
1 | module API
2 | module Entities
3 | class User < Grape::Entity
4 | expose :id, :email, :name, :bio, :created_at
5 | expose :is_admin?, as: :is_admin
6 | expose :avatar_url
7 | end
8 |
9 | class AdminUser < User
10 | expose :current_sign_in_at, :sign_in_count, :current_sign_in_ip
11 | end
12 |
13 | class Project < Grape::Entity
14 | expose :id, :code, :name, :description, :state, :picture_url, :created_at, :original_estimate, :work_logged, :remaining_estimate, :delta
15 | end
16 |
17 | class ProjectMember < Grape::Entity
18 | expose :id, :project_id, :user_id, :username, :role, :created_at
19 | expose :avatar_url
20 | end
21 |
22 | class ProjectSnapshot < Grape::Entity
23 | expose :id, :project_id, :task_count, :original_estimate, :work_logged, :remaining_estimate, :delta, :created_at
24 | end
25 |
26 | class Task < Grape::Entity
27 | format_with :to_s do |tags|
28 | tags.to_s
29 | end
30 |
31 | expose :id, :project_id, :assignee_id, :code, :iid, :name, :description, :original_estimate, :remaining_estimate, :work_logged, :delta, :created_at, :updated_at
32 | expose :tag_list, format_with: :to_s
33 | end
34 |
35 | class TimesheetTask < Grape::Entity
36 | format_with :to_s do |tags|
37 | tags.to_s
38 | end
39 |
40 | expose :id, :code, :name, :description, :monday, :tuesday, :wednesday, :thursday, :friday, :saturday, :sunday, :original_estimate, :remaining_estimate, :work_logged, :delta
41 | expose :tag_list, format_with: :to_s
42 | end
43 |
44 | class WorkLog < Grape::Entity
45 | expose :id, :task_id, :description, :day, :worked, :created_at
46 | end
47 | end
48 | end
--------------------------------------------------------------------------------
/lib/api/project_members.rb:
--------------------------------------------------------------------------------
1 | module API
2 | # ProjectMembers API
3 | class ProjectMembers < Grape::API
4 | before { authenticate! }
5 |
6 | resource :projects do
7 |
8 | # Get a list of project members
9 | #
10 | # Parameters:
11 | # id (required) - The ID of a project
12 | # Example Request:
13 | # GET /members/:id/tasks
14 | get ":id/members" do
15 | authorize! :read_project_member, user_project
16 | present paginate(user_project.project_members.includes(:user)), with: Entities::ProjectMember
17 | end
18 |
19 | # Get a single project member
20 | #
21 | # Parameters:
22 | # id (required) - The ID of a project
23 | # member_id (required) - The ID of a project member
24 | # Example Request:
25 | # GET /projects/:id/members/:member_id
26 | get ":id/members/:member_id" do
27 | authorize! :read_project_member, user_project
28 | @member = user_project.project_members.find(params[:member_id])
29 | present @member, with: Entities::ProjectMember
30 | end
31 |
32 | # Create a new project member
33 | #
34 | # Parameters:
35 | # id (required) - The ID of a project
36 | # user_id (required) - The id of a user
37 | # role (required) - The role of the member
38 | # Example Request:
39 | # POST /projects/:id/members
40 | post ":id/members" do
41 | set_current_user_for_thread do
42 | required_attributes! [:name]
43 | authorize! :create_project_member, user_project
44 | attrs = attributes_for_keys [:user_id, :role]
45 | @member = user_project.project_members.new attrs
46 | if @member.save
47 | present @member, with: Entities::ProjectMember
48 | else
49 | not_found!
50 | end
51 | end
52 | end
53 |
54 | # Update an existing project member
55 | #
56 | # Parameters:
57 | # id (required) - The ID of a project
58 | # member_id (required) - The ID of a project member
59 | # role (optional) - The role of the member
60 | # Example Request:
61 | # PUT /projects/:id/members/:member_id
62 | put ":id/members/:member_id" do
63 | set_current_user_for_thread do
64 | authorize! :update_project_member, user_project
65 | @member = user_project.project_members.find(params[:member_id])
66 |
67 | attrs = attributes_for_keys [:role]
68 |
69 | if @member.update_attributes attrs
70 | present @member, with: Entities::ProjectMember
71 | else
72 | not_found!
73 | end
74 | end
75 | end
76 |
77 |
78 | # Delete a project member
79 | #
80 | # Parameters:
81 | # id (required) - The ID of a project
82 | # member_id (required) - The ID of a project member
83 | # Example Request:
84 | # DELETE /projects/:id/members/:member_id
85 | delete ":id/members/:member_id" do
86 | set_current_user_for_thread do
87 | authorize! :delete_project_member, user_project
88 | member = user_project.project_members.find(params[:member_id])
89 | if member.work_logged == 0
90 | member.destroy
91 | else
92 | render_api_error!("Can't delete member with work logged", 403)
93 | end
94 | end
95 | end
96 |
97 | end
98 | end
99 | end
--------------------------------------------------------------------------------
/lib/api/project_snapshots.rb:
--------------------------------------------------------------------------------
1 | module API
2 | # ProjectSnapshots API
3 | class ProjectSnapshots < Grape::API
4 | before { authenticate! }
5 |
6 | resource :projects do
7 |
8 | # Get a list of project snapshots
9 | #
10 | # Parameters:
11 | # id (required) - The ID of a project
12 | # Example Request:
13 | # GET /projects/:id/snapshots
14 | get ":id/snapshots" do
15 | authorize! :read_project, user_project
16 | present paginate(user_project.project_snapshots), with: Entities::ProjectSnapshot
17 | end
18 |
19 | end
20 | end
21 | end
--------------------------------------------------------------------------------
/lib/api/projects.rb:
--------------------------------------------------------------------------------
1 | module API
2 | # Projects API
3 | class Projects < Grape::API
4 | before { authenticate! }
5 |
6 | resource :projects do
7 |
8 | # Get a projects list for authenticated user
9 | #
10 | # Example Request:
11 | # GET /projects
12 | get do
13 | allAsked =
14 |
15 | if current_user.is_admin? && params[:admin] == 'true'
16 | @projects = paginate Project.all
17 | else
18 | @projects = paginate current_user.projects
19 | end
20 | present @projects, with: Entities::Project
21 | end
22 |
23 | # Get recent projects list for authenticated user
24 | #
25 | # Example Request:
26 | # GET /projects/recents
27 | get "recents" do
28 | recent_openings = paginate current_user.project_openings
29 | @projects = Array.new
30 | recent_openings.each do |recent|
31 | @projects << recent.project
32 | end
33 | present @projects, with: Entities::Project
34 | end
35 |
36 | # Get a single project
37 | #
38 | # Parameters:
39 | # id (required) - The ID of a project
40 | # Example Request:
41 | # GET /projects/:id
42 | get ":id" do
43 | authorize! :read_project, user_project
44 | present user_project, with: Entities::Project, user: current_user
45 | end
46 | end
47 | end
48 | end
--------------------------------------------------------------------------------
/lib/api/timesheets.rb:
--------------------------------------------------------------------------------
1 | module API
2 | # Timesheets API
3 | class Timesheets < Grape::API
4 | before { authenticate! }
5 |
6 | helpers do
7 | def compute_end(start)
8 | endDate = DateTime.strptime(start, "%Y%m%d")
9 | endDate += 6.days
10 | endDate.strftime("%Y%m%d")
11 | end
12 |
13 | def find_member(project_id, user_id)
14 | ProjectMember.where("user_id=? and project_id=?", user_id, project_id).first
15 | end
16 | end
17 |
18 | resource :projects do
19 |
20 | desc "Returns all tasks for a given period."
21 | get ":id/timesheets/:start/tasks" do
22 | authorize! :read_project, user_project
23 | period_start = params[:start]
24 | period_end = compute_end(period_start)
25 | user_id = params[:user_id] || current_user.id
26 | member = find_member(params[:id], user_id)
27 | tasks = member.assigned_tasks
28 | timesheet_tasks = []
29 | tasks.each do |task|
30 | timesheet_task = TimesheetTask.new(task, period_start, period_end)
31 | timesheet_tasks << timesheet_task
32 | end
33 |
34 | present timesheet_tasks, with: Entities::TimesheetTask
35 | end
36 |
37 | end
38 |
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/lib/api/users.rb:
--------------------------------------------------------------------------------
1 | module API
2 | # Users API
3 | class Users < Grape::API
4 | before { authenticate! }
5 |
6 | helpers do
7 | def entity
8 | current_user.is_admin? ? Entities::AdminUser : Entities::User
9 | end
10 | end
11 |
12 | resource :users do
13 | # Get a users list
14 | #
15 | # Example Request:
16 | # GET /users
17 | get do
18 | @users = User.all
19 | @users = @users.search(params[:search]) if params[:search].present?
20 | @users = paginate @users
21 | present @users, with: entity
22 | end
23 |
24 | # Get a single user
25 | #
26 | # Parameters:
27 | # id (required) - The ID of a user
28 | # Example Request:
29 | # GET /users/:id
30 | get ":id" do
31 | @user = User.find(params[:id])
32 | present @user, with: entity
33 | end
34 | end
35 | end
36 | end
--------------------------------------------------------------------------------
/lib/api/work_logs.rb:
--------------------------------------------------------------------------------
1 | module API
2 | # WorkLogs API
3 | class WorkLogs < Grape::API
4 | before { authenticate! }
5 |
6 | resource :projects do
7 |
8 | desc "Returns all work logs for a given task."
9 | get ":id/tasks/:task_id/work_logs" do
10 | authorize! :read_project, user_project
11 | @task = user_project.tasks.find(params[:task_id])
12 | present paginate(@task.work_logs), with: Entities::WorkLog
13 | end
14 |
15 | desc "Returns a work log for a given day."
16 | get ":id/tasks/:task_id/work_logs/:day" do
17 | authorize! :read_project, user_project
18 | @task = user_project.tasks.find(params[:task_id])
19 | present @task.work_logs.find_by(day: params[:day]), with: Entities::WorkLog
20 | end
21 |
22 | desc "create or update a work log for a given day."
23 | put ":id/tasks/:task_id/work_logs/:day" do
24 | authorize! :update_task_work, user_project
25 | @task = user_project.tasks.find(params[:task_id])
26 |
27 | if @task.assignee.id == current_user.id || can?(current_user, :update_members_task_work, user_project)
28 | @log = @task.work_logs.find_by(day: params[:day])
29 | unless @log
30 | @log = WorkLog.new
31 | @log.task_id = @task.id
32 | @log.day = params[:day]
33 | end
34 | @log.worked = params[:worked]
35 |
36 | if @log.save
37 | present @log, with: Entities::WorkLog
38 | else
39 | not_found!
40 | end
41 | else
42 | forbidden!
43 | end
44 | end
45 |
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/lib/assets/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frocher/sofreakingboring/37ccfa946e3764652cc552f644e0d9affbcaa4a3/lib/assets/.keep
--------------------------------------------------------------------------------
/lib/tasks/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frocher/sofreakingboring/37ccfa946e3764652cc552f644e0d9affbcaa4a3/lib/tasks/.keep
--------------------------------------------------------------------------------
/lib/tasks/migrate_iids.rake:
--------------------------------------------------------------------------------
1 | task migrate_iids: :environment do
2 | puts 'Tasks'
3 | Task.where(iid: nil).find_each(batch_size: 100) do |task|
4 | begin
5 | task.set_iid
6 | if task.update_attribute(:iid, task.iid)
7 | print '.'
8 | else
9 | print 'F'
10 | end
11 | rescue
12 | print 'F'
13 | end
14 | end
15 |
16 | puts 'done'
17 | end
--------------------------------------------------------------------------------
/log/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frocher/sofreakingboring/37ccfa946e3764652cc552f644e0d9affbcaa4a3/log/.keep
--------------------------------------------------------------------------------
/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
48 |
49 |
50 |
51 |
52 |
53 |
The change you wanted was rejected.
54 |
Maybe you tried to change something you didn't have access to.
55 |
56 | If you are the application owner check the logs for more information.
57 |
58 |
59 |
--------------------------------------------------------------------------------
/public/attachments/medium/missing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frocher/sofreakingboring/37ccfa946e3764652cc552f644e0d9affbcaa4a3/public/attachments/medium/missing.png
--------------------------------------------------------------------------------
/public/attachments/original/missing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frocher/sofreakingboring/37ccfa946e3764652cc552f644e0d9affbcaa4a3/public/attachments/original/missing.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frocher/sofreakingboring/37ccfa946e3764652cc552f644e0d9affbcaa4a3/public/favicon.ico
--------------------------------------------------------------------------------
/public/fonts/bootstrap/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frocher/sofreakingboring/37ccfa946e3764652cc552f644e0d9affbcaa4a3/public/fonts/bootstrap/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/public/fonts/bootstrap/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frocher/sofreakingboring/37ccfa946e3764652cc552f644e0d9affbcaa4a3/public/fonts/bootstrap/glyphicons-halflings-regular.ttf
--------------------------------------------------------------------------------
/public/fonts/bootstrap/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frocher/sofreakingboring/37ccfa946e3764652cc552f644e0d9affbcaa4a3/public/fonts/bootstrap/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /users/password/new
3 | Disallow: /users/sign_up
4 |
--------------------------------------------------------------------------------
/spec/factories.rb:
--------------------------------------------------------------------------------
1 | FactoryGirl.define do
2 |
3 | factory :user, aliases: [:assignee] do
4 | email { Faker::Internet.email }
5 | sequence(:name) { |n| "#{Faker::Internet.user_name}#{n}" }
6 | password "12345678"
7 | password_confirmation { password }
8 |
9 | trait :admin do
10 | admin true
11 | end
12 |
13 | factory :admin, traits: [:admin]
14 | end
15 |
16 | factory :project do
17 | state "opened"
18 | sequence(:code) { |n| "#{Faker::Lorem.characters(6)}#{n}" }
19 | sequence(:name) { |n| "#{Faker::Lorem.sentence}#{n}" }
20 | end
21 |
22 | factory :project_opening do
23 | project
24 | user
25 | sequence(:touched) { |n| "#{Faker::Number.number(3)}#{n}" }
26 | end
27 |
28 |
29 | factory :task do
30 | project
31 | assignee
32 |
33 | sequence(:name) { |n| "#{Faker::Lorem.characters(8)}#{n}" }
34 | sequence(:original_estimate) { |n| "#{Faker::Number.number(6)}#{n}" }
35 | sequence(:remaining_estimate) { |n| "#{Faker::Number.number(6)}#{n}" }
36 | end
37 |
38 |
39 | factory :work_log do
40 | task
41 |
42 | day '20141010'
43 | sequence(:worked) { |n| "#{Faker::Number.number(6)}#{n}" }
44 | end
45 |
46 | end
--------------------------------------------------------------------------------
/spec/models/project_member_spec.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: project_members
4 | #
5 | # id :integer not null, primary key
6 | # user_id :integer not null
7 | # project_id :integer not null
8 | # role :integer default("0"), not null
9 | # created_at :datetime
10 | # updated_at :datetime
11 | #
12 |
13 | require 'rails_helper'
14 |
15 | describe ProjectMember do
16 | describe 'Associations' do
17 | it { should belong_to(:project) }
18 | it { should belong_to(:user) }
19 | end
20 |
21 | describe 'validations' do
22 | it { should validate_presence_of(:user) }
23 | it { should validate_presence_of(:project) }
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/spec/models/project_opening_spec.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: project_openings
4 | #
5 | # id :integer not null, primary key
6 | # project_id :integer not null
7 | # user_id :integer not null
8 | # touched :integer
9 | # created_at :datetime
10 | # updated_at :datetime
11 | #
12 | require 'rails_helper'
13 |
14 | describe ProjectOpening do
15 | describe 'Associations' do
16 | it { should belong_to(:project) }
17 | it { should belong_to(:user) }
18 | end
19 |
20 |
21 | end
22 |
--------------------------------------------------------------------------------
/spec/models/project_spec.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: projects
4 | #
5 | # id :integer not null, primary key
6 | # code :string(255) default(""), not null
7 | # name :string(255) default(""), not null
8 | # description :text
9 | # created_at :datetime
10 | # updated_at :datetime
11 | # attachment_file_name :string(255)
12 | # attachment_content_type :string(255)
13 | # attachment_file_size :integer
14 | # attachment_updated_at :datetime
15 | # state :string(255)
16 | #
17 | require 'rails_helper'
18 |
19 | describe Project do
20 | describe 'Associations' do
21 | it { should have_many(:project_members).class_name('ProjectMember') }
22 | it { should have_many(:users).class_name('User') }
23 | it { should have_many(:tasks).class_name('Task') }
24 | it { should have_many(:project_snapshots).class_name('ProjectSnapshot') }
25 | it { should have_many(:project_openings).class_name('ProjectOpening') }
26 | end
27 |
28 | describe 'validations' do
29 | it { should validate_presence_of(:code) }
30 | it { should validate_uniqueness_of(:code) }
31 | it { should ensure_length_of(:code).is_at_most(8) }
32 | it { should validate_presence_of(:name) }
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/spec/models/user_spec.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: users
4 | #
5 | # id :integer not null, primary key
6 | # email :string(255) default(""), not null
7 | # encrypted_password :string(255) default(""), not null
8 | # reset_password_token :string(255)
9 | # reset_password_sent_at :datetime
10 | # remember_created_at :datetime
11 | # sign_in_count :integer default("0"), not null
12 | # current_sign_in_at :datetime
13 | # last_sign_in_at :datetime
14 | # current_sign_in_ip :string(255)
15 | # last_sign_in_ip :string(255)
16 | # created_at :datetime
17 | # updated_at :datetime
18 | # name :string(255) default(""), not null
19 | # bio :string(255) default(""), not null
20 | # avatar_file_name :string(255)
21 | # avatar_content_type :string(255)
22 | # avatar_file_size :integer
23 | # avatar_updated_at :datetime
24 | # admin :boolean default("f"), not null
25 | # authentication_token :string(255)
26 | #
27 |
28 | require 'rails_helper'
29 |
30 | describe User do
31 | describe 'Associations' do
32 | it { should have_many(:identities).class_name('Identity') }
33 | it { should have_many(:project_members).class_name('ProjectMember') }
34 | it { should have_many(:project_openings).class_name('ProjectOpening') }
35 | it { should have_many(:projects).class_name('Project') }
36 | it { should have_many(:tasks).class_name('Task') }
37 | end
38 |
39 | describe 'validations' do
40 | it { should validate_presence_of(:name) }
41 | it { should validate_uniqueness_of(:name) }
42 | it { should ensure_length_of(:bio).is_at_most(500) }
43 | end
44 |
45 | describe 'Respond to' do
46 | it { should respond_to(:is_admin?) }
47 | it { should respond_to(:private_token) }
48 | end
49 |
50 | describe 'authentication token' do
51 | it "should have authentication token" do
52 | user = create(:user)
53 | expect(user.authentication_token).not_to be_blank
54 | end
55 | end
56 |
57 | describe 'admin creation' do
58 | it "should be admin" do
59 | user = create(:admin)
60 | expect(user.is_admin?).to be true
61 | end
62 | end
63 |
64 | describe '#work_logged' do
65 | it 'should have no work logged' do
66 | user = create(:user)
67 | expect(user.work_logged).to be 0
68 | end
69 |
70 | it 'should have work' do
71 | work = create(:work_log)
72 | user = work.task.assignee
73 | expect(user.work_logged).not_to be 0
74 | end
75 |
76 | end
77 |
78 | describe '.search' do
79 | it 'should work' do
80 | result = User.search('foo')
81 | expect(result).to_not be_nil
82 | end
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/spec/rails_helper.rb:
--------------------------------------------------------------------------------
1 | # This file is copied to spec/ when you run 'rails generate rspec:install'
2 | ENV["RAILS_ENV"] ||= 'test'
3 | require 'spec_helper'
4 | require File.expand_path("../../config/environment", __FILE__)
5 | require 'rspec/rails'
6 | require 'shoulda/matchers'
7 | require 'capybara/rails'
8 | require 'capybara/rspec'
9 |
10 |
11 | # Add additional requires below this line. Rails is not loaded until this point!
12 |
13 | # Requires supporting ruby files with custom matchers and macros, etc, in
14 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
15 | # run as spec files by default. This means that files in spec/support that end
16 | # in _spec.rb will both be required and run as specs, causing the specs to be
17 | # run twice. It is recommended that you do not name files matching this glob to
18 | # end with _spec.rb. You can configure this pattern with the --pattern
19 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
20 | #
21 | # The following line is provided for convenience purposes. It has the downside
22 | # of increasing the boot-up time by auto-requiring all files in the support
23 | # directory. Alternatively, in the individual `*_spec.rb` files, manually
24 | # require only the support files necessary.
25 | #
26 | # Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
27 |
28 | # Checks for pending migrations before tests are run.
29 | # If you are not using ActiveRecord, you can remove this line.
30 | ActiveRecord::Migration.maintain_test_schema!
31 |
32 | RSpec.configure do |config|
33 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
34 | config.fixture_path = "#{::Rails.root}/spec/fixtures"
35 |
36 | # If you're not using ActiveRecord, or you'd prefer not to run each of your
37 | # examples within a transaction, remove the following line or assign false
38 | # instead of true.
39 | config.use_transactional_fixtures = true
40 |
41 | config.include FactoryGirl::Syntax::Methods
42 |
43 | # RSpec Rails can automatically mix in different behaviours to your tests
44 | # based on their file location, for example enabling you to call `get` and
45 | # `post` in specs under `spec/controllers`.
46 | #
47 | # You can disable this behaviour by removing the line below, and instead
48 | # explicitly tag your specs with their type, e.g.:
49 | #
50 | # RSpec.describe UsersController, :type => :controller do
51 | # # ...
52 | # end
53 | #
54 | # The different available types are documented in the features, such as in
55 | # https://relishapp.com/rspec/rspec-rails/docs
56 | config.infer_spec_type_from_file_location!
57 | end
58 |
--------------------------------------------------------------------------------
/vendor/assets/javascripts/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frocher/sofreakingboring/37ccfa946e3764652cc552f644e0d9affbcaa4a3/vendor/assets/javascripts/.keep
--------------------------------------------------------------------------------
/vendor/assets/javascripts/jquery.flip.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * jQuery 3D Flip v1.0
3 | * https://github.com/nnattawat/flip
4 | *
5 | * Copyright 2014, Nattawat Nonsung
6 | */
7 |
8 | (function( $ ) {
9 | var flip = function(dom, flipedRotate) {
10 | dom.data("fliped", true);
11 | dom.css({
12 | transform: flipedRotate
13 | });
14 | };
15 | var unflip = function(dom) {
16 | dom.data("fliped", false);
17 | dom.css({
18 | transform: "rotatex(0deg)"
19 | });
20 | };
21 | $.fn.flip = function(options) {
22 | this.each(function(){
23 | var $dom = $(this);
24 | var width = $dom.width();
25 | var height = $dom.height();
26 | var margin = $dom.css('margin');
27 | var prespective;
28 |
29 | if (options !== undefined && typeof(options) == "boolean") { // Force flip the DOM
30 | if (options) {
31 | flip($dom, $dom.data("flipedRotate"));
32 | } else {
33 | unflip($dom);
34 | }
35 | } else { //Init flipable DOM
36 | var settings = $.extend({
37 | axis: "y",
38 | trigger: "click"
39 | }, options );
40 |
41 | if (settings.axis.toLowerCase() == "x") {
42 | prespective = height*2;
43 | // save rotating css to DOM for manual flip
44 | $dom.data("flipedRotate", "rotatex(180deg)");
45 | } else {
46 | prespective = width*2;
47 | $dom.data("flipedRotate", "rotatey(180deg)");
48 | }
49 | var flipedRotate = $dom.data("flipedRotate");
50 |
51 | $dom.wrap("
");
52 | $dom.parent().css({
53 | perspective: prespective,
54 | position: "relative",
55 | width: width,
56 | height: height,
57 | margin: margin
58 | });
59 |
60 | $dom.css({
61 | "transform-style": "preserve-3d",
62 | transition: "all 0.5s ease-out"
63 | });
64 |
65 | $dom.find(".front").wrap("
");
66 | $dom.find(".back").wrap("
");
67 |
68 | $dom.find(".front, .back").css({
69 | width: "100%",
70 | height: "100%",
71 | display: 'inline-table'
72 | });
73 |
74 | $dom.find(".front-wrap, .back-wrap").css({
75 | position: "absolute",
76 | "backface-visibility": "hidden",
77 | width: "100%",
78 | height: "100%"
79 | });
80 |
81 | $dom.find(".back-wrap").css({
82 | transform: flipedRotate
83 | });
84 |
85 | if (settings.trigger.toLowerCase() == "click") {
86 | $dom.parent().click(function() {
87 | if ($dom.data("fliped")) {
88 | unflip($dom);
89 | } else {
90 | flip($dom, flipedRotate);
91 | }
92 | });
93 | } else if (settings.trigger.toLowerCase() == "hover") {
94 | $dom.parent().hover(function() {
95 | flip($dom, flipedRotate);
96 | }, function() {
97 | unflip($dom);
98 | });
99 | }
100 | }
101 | });
102 |
103 | return this;
104 | };
105 |
106 | }( jQuery ));
--------------------------------------------------------------------------------
/vendor/assets/javascripts/jquery.flot.categories.min.js:
--------------------------------------------------------------------------------
1 | /* Javascript plotting library for jQuery, version 0.8.3.
2 |
3 | Copyright (c) 2007-2014 IOLA and Ole Laursen.
4 | Licensed under the MIT license.
5 |
6 | */
7 | (function($){var options={xaxis:{categories:null},yaxis:{categories:null}};function processRawData(plot,series,data,datapoints){var xCategories=series.xaxis.options.mode=="categories",yCategories=series.yaxis.options.mode=="categories";if(!(xCategories||yCategories))return;var format=datapoints.format;if(!format){var s=series;format=[];format.push({x:true,number:true,required:true});format.push({y:true,number:true,required:true});if(s.bars.show||s.lines.show&&s.lines.fill){var autoscale=!!(s.bars.show&&s.bars.zero||s.lines.show&&s.lines.zero);format.push({y:true,number:true,required:false,defaultValue:0,autoscale:autoscale});if(s.bars.horizontal){delete format[format.length-1].y;format[format.length-1].x=true}}datapoints.format=format}for(var m=0;mindex)index=categories[v];return index+1}function categoriesTickGenerator(axis){var res=[];for(var label in axis.categories){var v=axis.categories[label];if(v>=axis.min&&v<=axis.max)res.push([v,label])}res.sort(function(a,b){return a[0]-b[0]});return res}function setupCategoriesForAxis(series,axis,datapoints){if(series[axis].options.mode!="categories")return;if(!series[axis].categories){var c={},o=series[axis].options.categories||{};if($.isArray(o)){for(var i=0;i=0;t--){if(i[t]==this){i.splice(t,1);break}}e.removeData(m);if(!i.length){if(r){cancelAnimationFrame(a)}else{clearTimeout(a)}a=null}},add:function(e){if(!n[f]&&this[s]){return false}var i;function a(e,n,a){var r=$(this),s=r.data(m)||{};s.w=n!==t?n:r.width();s.h=a!==t?a:r.height();i.apply(this,arguments)}if($.isFunction(e)){i=e;return a}else{i=e.handler;e.handler=a}}};function h(t){if(r===true){r=t||1}for(var s=i.length-1;s>=0;s--){var l=$(i[s]);if(l[0]==e||l.is(":visible")){var f=l.width(),c=l.height(),d=l.data(m);if(d&&(f!==d.w||c!==d.h)){l.trigger(u,[d.w=f,d.h=c]);r=t||true}}else{d=l.data(m);d.w=0;d.h=0}}if(a!==null){if(r&&(t==null||t-r<1e3)){a=e.requestAnimationFrame(h)}else{a=setTimeout(h,n[o]);r=false}}}if(!e.requestAnimationFrame){e.requestAnimationFrame=function(){return e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.oRequestAnimationFrame||e.msRequestAnimationFrame||function(t,i){return e.setTimeout(function(){t((new Date).getTime())},n[l])}}()}if(!e.cancelAnimationFrame){e.cancelAnimationFrame=function(){return e.webkitCancelRequestAnimationFrame||e.mozCancelRequestAnimationFrame||e.oCancelRequestAnimationFrame||e.msCancelRequestAnimationFrame||clearTimeout}()}})(jQuery,this);(function($){var options={};function init(plot){function onResize(){var placeholder=plot.getPlaceholder();if(placeholder.width()==0||placeholder.height()==0)return;plot.resize();plot.setupGrid();plot.draw()}function bindEvents(plot,eventHolder){plot.getPlaceholder().resize(onResize)}function shutdown(plot,eventHolder){plot.getPlaceholder().unbind("resize",onResize)}plot.hooks.bindEvents.push(bindEvents);plot.hooks.shutdown.push(shutdown)}$.plot.plugins.push({init:init,options:options,name:"resize",version:"1.0"})})(jQuery);
--------------------------------------------------------------------------------
/vendor/assets/javascripts/jquery.flot.stack.min.js:
--------------------------------------------------------------------------------
1 | /* Javascript plotting library for jQuery, version 0.8.3.
2 |
3 | Copyright (c) 2007-2014 IOLA and Ole Laursen.
4 | Licensed under the MIT license.
5 |
6 | */
7 | (function($){var options={series:{stack:null}};function init(plot){function findMatchingSeries(s,allseries){var res=null;for(var i=0;i2&&(horizontal?datapoints.format[2].x:datapoints.format[2].y),withsteps=withlines&&s.lines.steps,fromgap=true,keyOffset=horizontal?1:0,accumulateOffset=horizontal?0:1,i=0,j=0,l,m;while(true){if(i>=points.length)break;l=newpoints.length;if(points[i]==null){for(m=0;m=otherpoints.length){if(!withlines){for(m=0;mqx){if(withlines&&i>0&&points[i-ps]!=null){intery=py+(points[i-ps+accumulateOffset]-py)*(qx-px)/(points[i-ps+keyOffset]-px);newpoints.push(qx);newpoints.push(intery+qy);for(m=2;m0&&otherpoints[j-otherps]!=null)bottom=qy+(otherpoints[j-otherps+accumulateOffset]-qy)*(px-qx)/(otherpoints[j-otherps+keyOffset]-qx);newpoints[l+accumulateOffset]+=bottom;i+=ps}fromgap=false;if(l!=newpoints.length&&withbottom)newpoints[l+2]+=bottom}if(withsteps&&l!=newpoints.length&&l>0&&newpoints[l]!=null&&newpoints[l]!=newpoints[l-ps]&&newpoints[l+1]!=newpoints[l-ps+1]){for(m=0;m