├── .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 | ![Project Dashboard](https://cloud.githubusercontent.com/assets/7987747/4908740/147eacae-646d-11e4-9971-9ebe095588fe.png) 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