├── .env.example ├── .gitignore ├── .overcommit.yml ├── .rspec ├── .rubocop.yml ├── Capfile ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── app ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── controllers │ ├── api │ │ └── v1 │ │ │ ├── absences_controller.rb │ │ │ ├── admins_controller.rb │ │ │ ├── auth │ │ │ └── sessions_controller.rb │ │ │ ├── dashboard_controller.rb │ │ │ ├── holidays_controller.rb │ │ │ ├── projects_controller.rb │ │ │ ├── reports │ │ │ ├── estimation_reports_controller.rb │ │ │ ├── time_reports_controller.rb │ │ │ └── user_reports_controller.rb │ │ │ ├── slack │ │ │ └── slack_controller.rb │ │ │ ├── teams_controller.rb │ │ │ ├── time_entries_controller.rb │ │ │ └── users_controller.rb │ ├── application_controller.rb │ └── concerns │ │ ├── .keep │ │ └── exception_handler.rb ├── jobs │ └── application_job.rb ├── mailers │ └── application_mailer.rb ├── models │ ├── absence.rb │ ├── admin.rb │ ├── application_record.rb │ ├── concerns │ │ ├── .keep │ │ ├── filterable.rb │ │ └── paginationable.rb │ ├── holiday.rb │ ├── notification.rb │ ├── project.rb │ ├── team.rb │ ├── time_entry.rb │ ├── user.rb │ └── user_notification.rb ├── serializers │ ├── absence_serializer.rb │ ├── admin_serializer.rb │ ├── holiday_serializer.rb │ ├── project_serializer.rb │ ├── team_serializer.rb │ ├── time_entry_serializer.rb │ └── user_serializer.rb ├── services │ ├── absence_days.rb │ ├── add_project.rb │ ├── auth │ │ ├── authenticate_service.rb │ │ ├── authorize_api_request.rb │ │ └── base_service.rb │ ├── base_service.rb │ ├── create_entry.rb │ ├── create_entry_for_day.rb │ ├── create_notification.rb │ ├── dashboard_info.rb │ ├── do_not_understand.rb │ ├── edit_entry.rb │ ├── find_project.rb │ ├── finish_dialog.rb │ ├── remove_entry.rb │ ├── reports │ │ ├── base_service.rb │ │ ├── estimation_report_service.rb │ │ ├── time_report_service.rb │ │ ├── trello_list_service.rb │ │ ├── user_absence_service.rb │ │ ├── user_report_service.rb │ │ └── user_worked_time_service.rb │ ├── service_helper.rb │ ├── set_absence.rb │ ├── show_help.rb │ ├── show_notifications.rb │ ├── show_projects.rb │ ├── show_report.rb │ ├── show_worked_hours.rb │ ├── slack_engine │ │ ├── adapters │ │ │ ├── base_adapter.rb │ │ │ ├── command.rb │ │ │ └── submission.rb │ │ ├── collback_constants.rb │ │ ├── commands │ │ │ └── logtime.rb │ │ ├── processor.rb │ │ └── submissions │ │ │ └── logtime.rb │ ├── specify_project.rb │ └── start_conversation.rb ├── views │ └── layouts │ │ ├── application.html.erb │ │ ├── mailer.html.erb │ │ └── mailer.text.erb └── workers │ └── notifier_worker.rb ├── bin ├── bot ├── bundle ├── rails ├── rake ├── setup ├── spring └── update ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── database.yml.example ├── deploy.rb ├── deploy.rb.example ├── deploy │ ├── production.rb │ └── production.rb.example ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── active_model_serializers.rb │ ├── application_controller_renderer.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── constants.rb │ ├── cookies_serializer.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── new_framework_defaults.rb │ ├── session_store.rb │ ├── slack.rb │ ├── trello.rb │ └── wrap_parameters.rb ├── locales │ └── en.yml ├── messages.example.yml ├── puma.rb ├── routes.rb ├── schedule.rb ├── sidekiq.yml └── spring.rb ├── db ├── migrate │ ├── 20160807153225_create_users.rb │ ├── 20160807154431_add_needs_asking_to_users.rb │ ├── 20160807174332_create_time_entries.rb │ ├── 20160808100052_create_projects.rb │ ├── 20160808110903_add_is_speaking_to_users.rb │ ├── 20160811131313_create_admins.rb │ ├── 20160811131325_add_devise_to_admins.rb │ ├── 20160814120433_add_alias_to_projects.rb │ ├── 20160822065951_add_is_active_to_users.rb │ ├── 20160822073621_add_is_absent_and_reason_to_time_entries.rb │ ├── 20161007142406_create_holidays.rb │ ├── 20161028094405_create_absences.rb │ ├── 20161028114143_remove_is_absent_and_reason_from_time_entries.rb │ ├── 20180306092732_create_teams.rb │ ├── 20180306093524_add_relations_team_and_users_and_projects.rb │ ├── 20180330160731_add_last_message_to_users.rb │ ├── 20180404084359_add_index_to_projects.rb │ ├── 20180620192339_add_trello_labels_to_time_entries.rb │ ├── 20180828152514_add_ticket_to_time_entries.rb │ ├── 20180920133731_remove_team_lead_and_manager_from_team.rb │ ├── 20180926110804_add_role_to_users.rb │ ├── 20181212142838_add_active_column_to_projects.rb │ ├── 20181224163929_create_notifications.rb │ └── 20181225093651_create_user_notifications.rb ├── schema.rb └── seeds.rb ├── docs.md ├── lib ├── assets │ └── .keep ├── capistrano │ └── tasks │ │ ├── puma.cap │ │ ├── restart.rake │ │ └── restart_slack.rake ├── event_handler.rb ├── helper.rb ├── json_web_token.rb ├── message │ ├── conditions.rb │ ├── logger.rb │ └── sender.rb ├── slack_client.rb └── tasks │ ├── .keep │ ├── conversions.rake │ ├── cron.rake │ ├── fixes.rake │ └── slack.rake ├── log └── .keep ├── public ├── 404.html ├── 422.html ├── 500.html ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── gifs │ ├── abs.gif │ ├── logtime_dialog.gif │ ├── logtime_simple.gif │ ├── projects.gif │ └── reports.gif ├── images │ ├── c-3po.jpg │ └── favicon.ico ├── messages │ ├── canteen.txt │ └── help.txt └── robots.txt ├── spec ├── api_doc │ └── v1 │ │ ├── absence.rb │ │ ├── admins.rb │ │ ├── dashboard.rb │ │ ├── descriptions │ │ └── header.md │ │ ├── holidays.rb │ │ ├── projects.rb │ │ ├── reports │ │ └── estimation_reports.rb │ │ ├── teams.rb │ │ ├── time_entries.rb │ │ └── users.rb ├── examples.txt ├── factories │ ├── absence.rb │ ├── admin.rb │ ├── holidays.rb │ ├── projects.rb │ ├── teams.rb │ ├── time_entry.rb │ └── users.rb ├── lib │ └── json_web_token_spec.rb ├── rails_helper.rb ├── requests │ └── api │ │ └── v1 │ │ ├── absences_spec.rb │ │ ├── admins_spec.rb │ │ ├── auth_spec.rb │ │ ├── dashboard_spec.rb │ │ ├── estimation_reports_spec.rb │ │ ├── filters_spec.rb │ │ ├── holidays_spec.rb │ │ ├── projects_spec.rb │ │ ├── sessions_spec.rb │ │ ├── teams_spec.rb │ │ ├── time_entries_spec.rb │ │ ├── time_reports_spec.rb │ │ ├── user_reports_spec.rb │ │ └── users_spec.rb ├── spec_helper.rb └── support │ ├── api_helper_spec.rb │ └── auth_helper.rb └── tmp └── .keep /.env.example: -------------------------------------------------------------------------------- 1 | SECRET_KEY_BASE=secret_key_base 2 | SECRET_KEY=secret_key_base 3 | SLACK_TOKEN=slack_token 4 | TIMEBOT_APP_TOKEN=token 5 | TIMEBOT_LAUNCH_DATE=2016-8-15 6 | TRELLO_DEVELOPER_PUBLIC_KEY=developer_public_key 7 | TRELLO_MEMBER_TOKEN=member_token 8 | SIDEKIQ_USERNAME=name 9 | SIDEKIQ_PASSWORD=password -------------------------------------------------------------------------------- /.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 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-journal 13 | 14 | # Ignore all logfiles and tempfiles. 15 | /log/* 16 | /tmp/* 17 | !/log/.keep 18 | !/tmp/.keep 19 | 20 | # Ignore Byebug command history file. 21 | .byebug_history 22 | .idea/ 23 | .ruby-gemset 24 | .ruby-version 25 | dump.rdb 26 | config/database.yml 27 | public/assets 28 | public/messages/fact.txt 29 | config/messages.yml 30 | .env 31 | .vscode/ 32 | package-lock.json 33 | config/secrets.yml 34 | -------------------------------------------------------------------------------- /.overcommit.yml: -------------------------------------------------------------------------------- 1 | # Use this file to configure the Overcommit hooks you wish to use. This will 2 | # extend the default configuration defined in: 3 | # https://github.com/brigade/overcommit/blob/master/config/default.yml 4 | # 5 | # At the topmost level of this YAML file is a key representing type of hook 6 | # being run (e.g. pre-commit, commit-msg, etc.). Within each type you can 7 | # customize each hook, such as whether to only run it on certain files (via 8 | # `include`), whether to only display output if it fails (via `quiet`), etc. 9 | # 10 | # For a complete list of hooks, see: 11 | # https://github.com/brigade/overcommit/tree/master/lib/overcommit/hook 12 | # 13 | # For a complete list of options that you can use to customize hooks, see: 14 | # https://github.com/brigade/overcommit#configuration 15 | # 16 | # Uncomment the following lines to make the configuration take effect. 17 | 18 | PreCommit: 19 | RuboCop: 20 | enabled: true 21 | on_warn: fail # Treat all warnings as failures 22 | 23 | TrailingWhitespace: 24 | enabled: true 25 | exclude: 26 | - '**/db/structure.sql' # Ignore trailing whitespace in generated files 27 | 28 | PostCheckout: 29 | ALL: # Special hook name that customizes all hooks of this type 30 | quiet: true # Change all post-checkout hooks to only display output on failure 31 | 32 | # IndexTags: 33 | # enabled: true # Generate a tags file with `ctags` each time HEAD changes 34 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.3.1 3 | Exclude: 4 | - 'db/**/*' 5 | - 'bin/*' 6 | - 'config/**/*' 7 | - 'public/**/*' 8 | - 'spec/**/*' 9 | - 'tmp/**/*' 10 | - 'lib/**/*' 11 | - 'Rakefile' 12 | - 'Capfile' 13 | - 'Gemfile' 14 | - 'config.ru' 15 | 16 | Style/FrozenStringLiteralComment: 17 | Enabled: false 18 | 19 | Style/Documentation: 20 | Enabled: false 21 | 22 | Style/ClassAndModuleChildren: 23 | Enabled: false 24 | 25 | Style/UnneededInterpolation: 26 | Enabled: false 27 | 28 | Style/RegexpLiteral: 29 | Enabled: false 30 | 31 | Style/PerlBackrefs: 32 | Enabled: false 33 | 34 | Metrics/LineLength: 35 | Max: 200 36 | 37 | Metrics/ClassLength: 38 | Max: 350 39 | 40 | Metrics/PerceivedComplexity: 41 | Max: 8 42 | 43 | Metrics/ModuleLength: 44 | Max: 350 45 | 46 | Metrics/AbcSize: 47 | Max: 100 48 | 49 | Metrics/MethodLength: 50 | Max: 35 51 | 52 | Metrics/CyclomaticComplexity: 53 | Max: 11 54 | 55 | Style/EvalWithLocation: 56 | Enabled: false 57 | 58 | Rails: 59 | Enabled: true 60 | 61 | Rails/InverseOf: 62 | Enabled: false 63 | -------------------------------------------------------------------------------- /Capfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Load DSL and set up stages 3 | require 'capistrano/setup' 4 | 5 | # Include default deployment tasks 6 | require 'capistrano/deploy' 7 | require "capistrano/scm/git" 8 | install_plugin Capistrano::SCM::Git 9 | require "capistrano/rails" 10 | require "capistrano/rvm" 11 | require "capistrano/puma" 12 | install_plugin Capistrano::Puma 13 | require 'capistrano/sidekiq' 14 | 15 | # Include tasks from other gems included in your Gemfile 16 | # 17 | # For documentation on these, see for example: 18 | # 19 | # https://github.com/capistrano/rvm 20 | # https://github.com/capistrano/rbenv 21 | # https://github.com/capistrano/chruby 22 | # https://github.com/capistrano/bundler 23 | # https://github.com/capistrano/rails 24 | # https://github.com/capistrano/passenger 25 | # 26 | # require 'capistrano/rvm' 27 | # require 'capistrano/rbenv' 28 | # require 'capistrano/chruby' 29 | # require 'capistrano/bundler' 30 | # require 'capistrano/rails/assets' 31 | # require 'capistrano/rails/migrations' 32 | # require 'capistrano/passenger' 33 | 34 | # Load custom tasks from `lib/capistrano/tasks` if you have any defined 35 | Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r } 36 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | source 'https://rubygems.org' 3 | 4 | gem 'rails', '~> 5.0.0' 5 | gem 'pg' 6 | gem 'puma', '~> 3.0' 7 | 8 | 9 | gem 'slack-ruby-client' 10 | gem 'eventmachine' 11 | gem 'faye-websocket' 12 | gem 'whenever', require: false 13 | gem 'inherited_resources', github: 'activeadmin/inherited_resources' 14 | gem 'dotenv-rails' 15 | gem 'ruby-trello' 16 | gem 'active_model_serializers' 17 | gem 'rack-cors', require: 'rack/cors' 18 | gem 'kaminari' 19 | 20 | # Auth 21 | gem 'jwt' 22 | gem 'bcrypt' 23 | 24 | # Background processing 25 | gem 'sidekiq' 26 | gem 'redis' 27 | gem 'tzinfo' 28 | 29 | group :development, :test do 30 | gem 'byebug', platform: :mri 31 | gem 'database_cleaner' 32 | gem 'capistrano', '~> 3.6' 33 | gem 'capistrano-bundler', '>= 1.1.0' 34 | gem 'capistrano-rails', '~> 1.2' 35 | gem 'capistrano-rvm' 36 | gem 'capistrano3-puma' 37 | gem 'capistrano-sidekiq' 38 | gem 'pry' 39 | gem 'rspec-rails' 40 | gem 'faker', '~> 1.9', '>= 1.9.1' 41 | gem 'factory_bot_rails' 42 | gem 'dox', require: false 43 | gem 'rubocop', require: false 44 | gem 'overcommit' 45 | end 46 | 47 | group :development do 48 | gem 'web-console' 49 | gem 'listen', '~> 3.0.5' 50 | gem 'spring' 51 | gem 'spring-watcher-listen', '~> 2.0.0' 52 | end 53 | 54 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 55 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Add your own tasks in files placed in lib/tasks ending in .rake, 3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 4 | 5 | require_relative 'config/application' 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationCable 4 | class Channel < ActionCable::Channel::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationCable 4 | class Connection < ActionCable::Connection::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/controllers/api/v1/absences_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | module V1 5 | class AbsencesController < ApplicationController 6 | before_action :set_absence, only: %i[show update destroy] 7 | 8 | def index 9 | absences = Absence.includes(user: [:team]).filter(filtering_params) 10 | render json: absences, include: ['user'] 11 | end 12 | 13 | def show 14 | render json: @absence 15 | end 16 | 17 | def create 18 | absences = Absence.create(multiple_absences_params) 19 | if absences.any? { |abs| abs.errors.any? } 20 | render json: absences.map(&:errors), status: :unprocessable_entity 21 | else 22 | render json: absences, include: ['user'], status: :created 23 | end 24 | end 25 | 26 | def update 27 | if @absence.update(absence_params) 28 | render json: @absence, include: ['user'], status: :ok 29 | else 30 | render json: @absence.errors, status: :unprocessable_entity 31 | end 32 | end 33 | 34 | def destroy 35 | @absence.destroy 36 | end 37 | 38 | def delete_multiple 39 | Absence.where(id: params[:absence_ids]).destroy_all 40 | end 41 | 42 | private 43 | 44 | def set_absence 45 | @absence = Absence.find(params[:id]) 46 | end 47 | 48 | def multiple_absences_params 49 | params[:absences].map do |param| 50 | param.require(:absence).permit(:reason, :comment, :user_id, :date) 51 | end 52 | end 53 | 54 | def absence_params 55 | params.require(:absence).permit(:reason, :comment, :user_id, :date) 56 | end 57 | 58 | def filtering_params 59 | params.permit(:by_user, :by_reason, :date_from, :date_to, :by_active_users) 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /app/controllers/api/v1/admins_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | module V1 5 | class AdminsController < ApplicationController 6 | before_action :set_admin, only: %i[show update destroy] 7 | 8 | def index 9 | render json: Admin.all 10 | end 11 | 12 | def show 13 | render json: @admin 14 | end 15 | 16 | def create 17 | admin = Admin.new(admin_params) 18 | if admin.save 19 | render json: admin, status: :created 20 | else 21 | render json: admin.errors, status: :unprocessable_entity 22 | end 23 | end 24 | 25 | def update 26 | if @admin.update(admin_params) 27 | render json: @admin, status: :ok 28 | else 29 | render json: @admin.errors, status: :unprocessable_entity 30 | end 31 | end 32 | 33 | def destroy 34 | @admin.destroy 35 | end 36 | 37 | def delete_multiple 38 | Admin.where(id: params[:admin_ids]).destroy_all 39 | end 40 | 41 | private 42 | 43 | def set_admin 44 | @admin = Admin.find(params[:id]) 45 | end 46 | 47 | def admin_params 48 | params.require(:admin).permit(:email, :password) 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /app/controllers/api/v1/auth/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | module V1 5 | module Auth 6 | class SessionsController < ApplicationController 7 | skip_before_action :authenticate_user! 8 | 9 | def create 10 | render ::Auth::AuthenticateService.call(auth_params) 11 | end 12 | 13 | private 14 | 15 | def auth_params 16 | params.permit(:email, :password) 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/controllers/api/v1/dashboard_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | module V1 5 | class DashboardController < ApplicationController 6 | def index 7 | render json: DashboardInfo.call(filters: filtering_params) 8 | end 9 | 10 | private 11 | 12 | def dashboard_params 13 | params.permit(:start_date, :end_date) 14 | end 15 | 16 | def filtering_params 17 | params.permit(:date_from, :date_to) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/controllers/api/v1/holidays_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | module V1 5 | class HolidaysController < ApplicationController 6 | before_action :set_holiday, only: %i[show update destroy] 7 | 8 | def index 9 | render json: Holiday.filter(filtering_params) 10 | end 11 | 12 | def show 13 | render json: @holiday 14 | end 15 | 16 | def create 17 | holiday = Holiday.new(holiday_params) 18 | if holiday.save 19 | render json: holiday, status: :created 20 | else 21 | render json: holiday.errors, status: :unprocessable_entity 22 | end 23 | end 24 | 25 | def update 26 | if @holiday.update(holiday_params) 27 | render json: @holiday, status: :ok 28 | else 29 | render json: @holiday.errors, status: :unprocessable_entity 30 | end 31 | end 32 | 33 | def destroy 34 | @holiday.destroy 35 | end 36 | 37 | def delete_multiple 38 | Holiday.where(id: params[:holiday_ids]).destroy_all 39 | end 40 | 41 | private 42 | 43 | def set_holiday 44 | @holiday = Holiday.find(params[:id]) 45 | end 46 | 47 | def holiday_params 48 | params.require(:holiday).permit(:name, :date) 49 | end 50 | 51 | def filtering_params 52 | params.permit(:by_name, :date_from, :date_to) 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /app/controllers/api/v1/projects_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | module V1 5 | class ProjectsController < ApplicationController 6 | before_action :set_project, only: %i[show update destroy] 7 | 8 | def index 9 | projects = Project.includes(:team).filter(filtering_params).paginate(params) 10 | render json: projects, include: ['team'], meta: { total_count: projects.total_count } 11 | end 12 | 13 | def search 14 | render json: Project.search(filtering_params) 15 | end 16 | 17 | def show 18 | render json: @project 19 | end 20 | 21 | def create 22 | project = Project.new(project_params) 23 | if project.save 24 | render json: project, status: :ok 25 | else 26 | render json: project.errors, status: :unprocessable_entity 27 | end 28 | end 29 | 30 | def update 31 | if @project.update(project_params) 32 | render json: @project, status: :ok 33 | else 34 | render json: @project.errors, status: :unprocessable_entity 35 | end 36 | end 37 | 38 | def destroy 39 | @project.destroy 40 | head :ok 41 | end 42 | 43 | def delete_multiple 44 | Project.where(id: params[:project_ids]).destroy_all 45 | end 46 | 47 | private 48 | 49 | def project_params 50 | params.require(:project).permit(:name, :alias, :team_id, :active) 51 | end 52 | 53 | def set_project 54 | @project = Project.find(params[:id]) 55 | end 56 | 57 | def filtering_params 58 | params.permit(:by_name, :by_alias, :active) 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /app/controllers/api/v1/reports/estimation_reports_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | module V1 5 | module Reports 6 | class EstimationReportsController < ApplicationController 7 | def index 8 | render ::Reports::EstimationReportService.call(filters: filtering_params, pagination: params) 9 | end 10 | 11 | private 12 | 13 | def filtering_params 14 | params.permit(:date_from, :with_ticket, :date_to, by_projects: [], by_users: []) 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/controllers/api/v1/reports/time_reports_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | module V1 5 | module Reports 6 | class TimeReportsController < ApplicationController 7 | def index 8 | render ::Reports::TimeReportService.call(filters: filtering_params, pagination: params) 9 | end 10 | 11 | private 12 | 13 | def filtering_params 14 | params.permit(:date_from, :date_to, by_projects: []) 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/controllers/api/v1/reports/user_reports_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | module V1 5 | module Reports 6 | class UserReportsController < ApplicationController 7 | def index 8 | render ::Reports::UserReportService.call(filters: filtering_params) 9 | end 10 | 11 | def worked_time 12 | render ::Reports::UserWorkedTimeService.call(filters: user_worked_time_filters) 13 | end 14 | 15 | def absence 16 | render ::Reports::UserAbsenceService.call(filters: user_absence_filters) 17 | end 18 | 19 | private 20 | 21 | def filtering_params 22 | params.permit(:date_from, :date_to) 23 | end 24 | 25 | def user_worked_time_filters 26 | params.permit(:date_from, :date_to, :with_ticket, by_users: [], by_projects: []) 27 | end 28 | 29 | def user_absence_filters 30 | params.permit(:date_from, :date_to) 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/controllers/api/v1/slack/slack_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | module V1 5 | module Slack 6 | class SlackController < ApplicationController 7 | skip_before_action :authenticate_user! 8 | 9 | def command 10 | SlackEngine::Processor.perform(params, adapter: :command) 11 | end 12 | 13 | def submission 14 | render SlackEngine::Processor.perform(params, adapter: :submission) 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/controllers/api/v1/teams_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | module V1 5 | class TeamsController < ApplicationController 6 | before_action :set_team, only: %i[show update destroy] 7 | 8 | def index 9 | render json: Team.filter(filtering_params) 10 | end 11 | 12 | def show 13 | render json: @team 14 | end 15 | 16 | def create 17 | team = Team.new(team_params) 18 | if team.save 19 | render json: team, status: :created 20 | else 21 | render json: team.errors, status: :unprocessable_entity 22 | end 23 | end 24 | 25 | def update 26 | if @team.update(team_params) 27 | render json: @team, status: :ok 28 | else 29 | render json: @team.errors, status: :unprocessable_entity 30 | end 31 | end 32 | 33 | def destroy 34 | @team.destroy 35 | end 36 | 37 | def delete_multiple 38 | Team.where(id: params[:team_ids]).destroy_all 39 | end 40 | 41 | private 42 | 43 | def set_team 44 | @team = Team.find(params[:id]) 45 | end 46 | 47 | def team_params 48 | params.require(:team).permit(:name, :description) 49 | end 50 | 51 | def filtering_params 52 | params.permit(:by_project, :by_user) 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /app/controllers/api/v1/time_entries_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | module V1 5 | class TimeEntriesController < ApplicationController 6 | before_action :set_time_entry, only: %i[show update destroy] 7 | 8 | def index 9 | time_entries = TimeEntry.includes(:project, user: [:team]).filter(filtering_params).paginate(params) 10 | render json: time_entries, include: %w[project user], meta: { total_count: time_entries.total_count } 11 | end 12 | 13 | def show 14 | render json: @time_entry 15 | end 16 | 17 | def create 18 | time_entry = TimeEntry.new(time_entry_params) 19 | if time_entry.save 20 | render json: time_entry, include: %w[project user], status: :created 21 | else 22 | render json: time_entry.errors, status: :unprocessable_entity 23 | end 24 | end 25 | 26 | def update 27 | if @time_entry.update(time_entry_params) 28 | render json: @time_entry, include: %w[project user], status: :ok 29 | else 30 | render json: @time_entry.errors, status: :unprocessable_entity 31 | end 32 | end 33 | 34 | def destroy 35 | @time_entry.destroy 36 | head :ok 37 | end 38 | 39 | def delete_multiple 40 | TimeEntry.where(id: params[:time_entry_ids]).destroy_all 41 | end 42 | 43 | private 44 | 45 | def time_entry_params 46 | params.require(:time_entry).permit(:user_id, :time, :date, :details, :project_id) 47 | end 48 | 49 | def set_time_entry 50 | @time_entry = TimeEntry.find(params[:id]) 51 | end 52 | 53 | def filtering_params 54 | params.permit(:date_from, :with_ticket, :date_to, by_projects: [], by_users: []) 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /app/controllers/api/v1/users_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Api 4 | module V1 5 | class UsersController < ApplicationController 6 | before_action :set_user, only: %i[show update destroy] 7 | 8 | def index 9 | users = User.includes(:team).filter(filtering_params).paginate(params) 10 | render json: users, include: ['team'], meta: { total_count: users.total_count } 11 | end 12 | 13 | def search 14 | render json: User.active.search(filtering_params) 15 | end 16 | 17 | def show 18 | render json: @user 19 | end 20 | 21 | def sync_users 22 | SlackClient.new.sync_users 23 | end 24 | 25 | def create 26 | user = User.new(user_params) 27 | if user.save 28 | render json: user, status: :created 29 | else 30 | render json: user.errors, status: :unprocessable_entity 31 | end 32 | end 33 | 34 | def update 35 | if @user.update(user_params) 36 | render json: @user, status: :ok 37 | else 38 | render json: @user.errors, status: :unprocessable_entity 39 | end 40 | end 41 | 42 | def destroy 43 | @user.destroy 44 | end 45 | 46 | def delete_multiple 47 | User.where(id: params[:user_ids]).destroy_all 48 | end 49 | 50 | private 51 | 52 | def set_user 53 | @user = User.find(params[:id]) 54 | end 55 | 56 | def user_params 57 | params.require(:user).permit(:name, :description, :is_active, :team_id, :last_message, :role, :uid) 58 | end 59 | 60 | def filtering_params 61 | params.permit(:by_name, :active_status, :by_role) 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationController < ActionController::Base 4 | include ExceptionHandler 5 | 6 | protect_from_forgery with: :exception 7 | skip_before_action :verify_authenticity_token 8 | before_action :authenticate_user!, unless: proc { Rails.env.development? } 9 | 10 | attr_reader :current_user 11 | 12 | private 13 | 14 | def authenticate_user! 15 | @current_user = ::Auth::AuthorizeApiRequest.call(request.headers) 16 | raise ExceptionHandler::UnauthorizedRequestError, 'Unauthorized' if current_user.blank? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codica2/timebot/ee7396d297abbd83161da60bd268ba4a2d648f66/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/concerns/exception_handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ExceptionHandler 4 | extend ActiveSupport::Concern 5 | 6 | class UnauthorizedRequestError < StandardError; end 7 | included do 8 | rescue_from ExceptionHandler::UnauthorizedRequestError, with: :unauthorized_request 9 | end 10 | 11 | private 12 | 13 | def unauthorized_request(message) 14 | render json: { message: message }, status: :unauthorized 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationJob < ActiveJob::Base 4 | end 5 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationMailer < ActionMailer::Base 4 | default from: 'from@example.com' 5 | layout 'mailer' 6 | end 7 | -------------------------------------------------------------------------------- /app/models/absence.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Absence < ApplicationRecord 4 | include Paginationable 5 | include Filterable 6 | 7 | belongs_to :user 8 | 9 | enum reason: %i[vacation illness other] 10 | 11 | validates :reason, :date, presence: true 12 | validate :presence_of_comment 13 | 14 | scope :date_from, ->(date) { where('date >= ?', date) } 15 | scope :date_to, ->(date) { where('date <= ?', date) } 16 | scope :by_user, ->(user_id) { where(user_id: user_id) } 17 | scope :by_reason, ->(reason) { where(reason: reason) } 18 | scope :by_active_users, ->(status) { joins(:user).where(users: { is_active: status }) } 19 | 20 | def presence_of_comment 21 | errors.add(:comment, 'must not be nil') if other? && comment.nil? 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/models/admin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bcrypt' 4 | 5 | class Admin < ApplicationRecord 6 | include BCrypt 7 | 8 | validates :email, presence: true 9 | 10 | def password 11 | @password ||= Password.new(encrypted_password) 12 | end 13 | 14 | def password=(new_password) 15 | @password = Password.create(new_password) 16 | self.encrypted_password = @password 17 | end 18 | 19 | def valid_password?(password_to_check) 20 | password == password_to_check 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationRecord < ActiveRecord::Base 4 | self.abstract_class = true 5 | end 6 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codica2/timebot/ee7396d297abbd83161da60bd268ba4a2d648f66/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/concerns/filterable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Filterable 4 | extend ActiveSupport::Concern 5 | class_methods do 6 | def filter(filtering_params) 7 | results = where(nil) 8 | filtering_params.each do |key, value| 9 | value.reject!(&:blank?) if value.is_a? Array 10 | results = results.public_send(key, value) if value.present? 11 | end 12 | results 13 | end 14 | 15 | def search(filtering_params) 16 | { data: select(:id, :name).filter(filtering_params).order(name: :asc) } 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/models/concerns/paginationable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Paginationable 4 | extend ActiveSupport::Concern 5 | included do 6 | scope :paginate, ->(params) { order(id: :desc).page(params[:page]).per(params[:per_page] || PER_PAGE) } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/models/holiday.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Holiday < ApplicationRecord 4 | include Filterable 5 | 6 | validates :name, :date, presence: true 7 | validates :date, uniqueness: true 8 | 9 | scope :by_name, ->(term) { where('lower(name) LIKE ?', "%#{term.downcase}%") } 10 | scope :date_from, ->(date) { where('date >= ?', date) } 11 | scope :date_to, ->(date) { where('date <= ?', date) } 12 | 13 | def self.holiday?(date = Time.zone.today) 14 | Holiday.all.map { |holy| holy.date.strftime('%y-%m-%d') }.include? date.strftime('%y-%m-%d') 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/models/notification.rb: -------------------------------------------------------------------------------- 1 | class Notification < ApplicationRecord 2 | belongs_to :creator, class_name: 'User' 3 | has_many :user_notifications, dependent: :destroy 4 | has_many :users, through: :user_notifications 5 | end 6 | -------------------------------------------------------------------------------- /app/models/project.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Project < ApplicationRecord 4 | MINIMUM_PROJECT_NAME_LENGTH = 4 5 | 6 | include Paginationable 7 | include Filterable 8 | 9 | has_many :time_entries, dependent: :nullify 10 | belongs_to :team, optional: true 11 | 12 | validates :name, :alias, uniqueness: { case_sensitive: false } 13 | 14 | before_validation :generate_alias 15 | 16 | scope :by_name, ->(term) { where('lower(name) LIKE ?', "#{term.downcase}%") } 17 | scope :by_alias, ->(term) { where('lower(alias) LIKE ?', "#{term.downcase}%") } 18 | scope :order_by_entries_number, -> { left_outer_joins(:time_entries).group(:id).order('COUNT(time_entries.id) DESC') } 19 | scope :active, ->(status = true) { where(active: status) } 20 | 21 | def to_s 22 | string = "#{id}. *#{name}*" 23 | string += "; Alias: *#{self.alias}*" if self.alias.present? 24 | string 25 | end 26 | 27 | private 28 | 29 | def generate_alias 30 | return if self.alias.present? 31 | 32 | self.alias = name.parameterize 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/models/team.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Team < ApplicationRecord 4 | include Filterable 5 | 6 | has_many :projects, dependent: :nullify 7 | has_many :users, dependent: :nullify 8 | 9 | validates :name, presence: true 10 | 11 | scope :by_project, ->(project_id) { Team.joins(:projects).where('projects.id = ?', project_id) } 12 | scope :by_user, ->(user_id) { Team.joins(:users).where('users.id = ?', user_id) } 13 | end 14 | -------------------------------------------------------------------------------- /app/models/time_entry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class TimeEntry < ApplicationRecord 4 | include Paginationable 5 | include Filterable 6 | 7 | belongs_to :user 8 | belongs_to :project 9 | has_one :team, through: :project 10 | 11 | before_save :save_labels, :save_ticket_url, :calc_minutes, :format_time 12 | 13 | validates :date, :time, presence: true 14 | 15 | validate :future_date_validation 16 | 17 | attr_accessor :status 18 | 19 | scope :in_interval, ->(start_date, end_date) { where(['date BETWEEN ? AND ?', start_date, end_date]) } 20 | scope :with_ticket, ->(ticket_url) { where('lower(details) LIKE ?', "%#{ticket_url.downcase}%") } 21 | scope :by_users, ->(users_id) { where(user_id: users_id) } 22 | scope :by_projects, ->(projects_id) { where(project_id: projects_id) } 23 | scope :date_from, ->(date) { where('date >= ?', date) } 24 | scope :date_to, ->(date) { where('date <= ?', date) } 25 | 26 | def description 27 | "*#{id}: #{project.name}* - #{time} - #{details}" 28 | end 29 | 30 | def ticket_url 31 | return if details.blank? 32 | 33 | URI.extract(details).first 34 | end 35 | 36 | def trello_ticket_id 37 | url = ticket_url 38 | return if url.blank? 39 | 40 | regexp = /\/[a-zA-Z0-9]{8}\// 41 | id = url.scan(regexp).first 42 | id.delete('/') if id.present? 43 | end 44 | 45 | def total_time 46 | search_param = ticket_url || details 47 | (TimeEntry.with_ticket(search_param).pluck(:minutes).sum / 60.0).round(1) 48 | end 49 | 50 | def estimated_time 51 | trello_labels.grep(/^\d+$/).first if trello_labels.present? 52 | end 53 | 54 | private 55 | 56 | def save_labels 57 | self.trello_labels = trello_card_labels 58 | end 59 | 60 | def save_ticket_url 61 | url = ticket_url 62 | self.ticket = url if url.present? 63 | end 64 | 65 | def trello_card_labels 66 | return if trello_ticket_id.blank? 67 | 68 | begin 69 | card = Trello::Card.find(trello_ticket_id) 70 | rescue StandardError => _e 71 | return nil 72 | end 73 | card.labels.map(&:name) 74 | end 75 | 76 | def calc_minutes 77 | match_data = time.match(/^(\d?\d):([0-5]\d)$/) 78 | self.minutes = match_data[1].to_i * 60 + match_data[2].to_i 79 | end 80 | 81 | def format_time 82 | self.time = time.to_time.strftime('%H:%M') # rubocop:disable Rails/Date 83 | format('%2d:%02d', minutes / 60, minutes % 60) 84 | end 85 | 86 | def future_date_validation 87 | errors.add(:date, 'You can\'t log time for future') if date > Time.current 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class User < ApplicationRecord 4 | include Paginationable 5 | include Filterable 6 | 7 | has_many :time_entries, dependent: :destroy 8 | has_many :absences, dependent: :destroy 9 | has_many :user_notifications, dependent: :destroy 10 | has_many :notifications, through: :user_notifications 11 | belongs_to :team, optional: true 12 | 13 | validates :uid, uniqueness: true 14 | validates :role, :name, :uid, presence: true 15 | 16 | scope :by_role, ->(role) { where(role: User.roles[role]) } 17 | scope :active, -> { where(is_active: true) } 18 | scope :by_name, ->(term) { where('lower(name) LIKE ?', "%#{term.downcase}%") } 19 | scope :active_status, ->(status) { where(is_active: status) if %w[true false].include? status } 20 | 21 | enum role: %i[pm front_end back_end QA ops marketing design not_set] 22 | 23 | def total_time_for_range(start_date, end_date, project = nil) 24 | total = time_entries.where(['date BETWEEN ? AND ?', start_date, end_date]) 25 | total = total.where(project_id: project.id) if project.present? 26 | total = total.sum(:minutes) 27 | hours = total / 60 28 | minutes = total % 60 29 | "#{hours} hours #{minutes} minutes" 30 | end 31 | 32 | def add_absence(reason, date, comment = nil) 33 | absence = absences.find_by(date: date) || absences.build(date: date) 34 | absence.reason = reason 35 | absence.comment = comment 36 | absence.save 37 | end 38 | 39 | def absent?(date) 40 | absences.find_by(date: date).present? 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/models/user_notification.rb: -------------------------------------------------------------------------------- 1 | class UserNotification < ApplicationRecord 2 | belongs_to :user 3 | belongs_to :notification 4 | end 5 | -------------------------------------------------------------------------------- /app/serializers/absence_serializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AbsenceSerializer < ActiveModel::Serializer 4 | attributes :id, :date, :comment, :reason 5 | belongs_to :user 6 | end 7 | -------------------------------------------------------------------------------- /app/serializers/admin_serializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AdminSerializer < ActiveModel::Serializer 4 | attributes :id, :email 5 | end 6 | -------------------------------------------------------------------------------- /app/serializers/holiday_serializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class HolidaySerializer < ActiveModel::Serializer 4 | attributes :id, :name, :date 5 | end 6 | -------------------------------------------------------------------------------- /app/serializers/project_serializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ProjectSerializer < ActiveModel::Serializer 4 | attributes :id, :name, :alias, :active 5 | belongs_to :team 6 | end 7 | -------------------------------------------------------------------------------- /app/serializers/team_serializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class TeamSerializer < ActiveModel::Serializer 4 | attributes :id, :name, :description 5 | has_many :projects 6 | has_many :users 7 | end 8 | -------------------------------------------------------------------------------- /app/serializers/time_entry_serializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class TimeEntrySerializer < ActiveModel::Serializer 4 | attributes :id, :date, :time, :details, :trello_labels, :estimated_time 5 | belongs_to :user 6 | belongs_to :project 7 | end 8 | -------------------------------------------------------------------------------- /app/serializers/user_serializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UserSerializer < ActiveModel::Serializer 4 | attributes :id, :name, :uid, :created_at, :is_active, :role 5 | belongs_to :team 6 | 7 | def created_at 8 | { 9 | date: object.created_at.strftime('%d %b, %Y'), 10 | time: object.created_at.strftime('%H:%M') 11 | } 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/services/absence_days.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AbsenceDays < BaseService 4 | include ServiceHelper 5 | 6 | attr_reader :user, :text 7 | 8 | def initialize(user, text) 9 | @user = user 10 | @text = text 11 | super() 12 | end 13 | 14 | def call 15 | dates = set_dates 16 | 17 | list = (dates[0]..dates[1]).to_a.map do |day| 18 | absence = user.absences.find_by(date: day) 19 | absence.present? ? [day, "#{"*#{absence.id}: #{absence.reason} #{absence.comment}*"}"] : [] 20 | end.reject(&:empty?) 21 | 22 | strings = list.map do |day, entry| 23 | "`#{day.strftime('%d.%m.%y`')}: #{entry}" 24 | end 25 | 26 | absences = user.absences.where(date: [dates[0]..dates[1]]) 27 | vacation_days_total = (dates[1].year * 12 + dates[1].month - (dates[0].year * 12 + dates[0].month)) * 15 / 12 28 | illness_days_total = 5 29 | 30 | s = "```Period: #{dates[0].strftime('%b %d, %Y')} - #{dates[1].strftime('%b %d, %Y')}\n" 31 | s += "Vacation days taken: #{absences.vacation.count} of #{vacation_days_total}\n" 32 | s += "Illness days taken: #{absences.illness.count} of #{illness_days_total}\n```\n" 33 | s += "_Notice: you had joined the team only on #{user.created_at.strftime('%b %d, %Y')}_" if user.created_at > dates[1] 34 | strings << s 35 | sender.send_message(user, strings.join("\n")) 36 | end 37 | 38 | def set_dates 39 | last_year = text.downcase[/last year/] || text[/ly/] 40 | this_year = Time.zone.today.year 41 | hire_date = user.created_at.to_date 42 | anniversary = Date.new(this_year, hire_date.month, hire_date.day) 43 | less_ann_date = Date.new(this_year - 1, hire_date.month, hire_date.day) 44 | 45 | if last_year.nil? 46 | Time.zone.today < anniversary ? [less_ann_date, Time.zone.today] : [anniversary, Time.zone.today] 47 | else 48 | Time.zone.today < anniversary ? [Date.new(this_year - 2, hire_date.month, hire_date.day), less_ann_date] : [less_ann_date, Date.new(this_year, hire_date.month, hire_date.day)] 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /app/services/add_project.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddProject < BaseService 4 | include ServiceHelper 5 | 6 | attr_reader :user, :text 7 | 8 | def initialize(user, text) 9 | @user = user 10 | @text = text 11 | super() 12 | end 13 | 14 | def call 15 | project_name = text.match(Message::Conditions::ADD_PROJECT_REGEXP)[1] 16 | project = find_project_by_name(project_name) 17 | 18 | if project 19 | sender.send_message(user, "Project with name #{project.name} already exists.") 20 | return 21 | end 22 | 23 | if project_name.length < Project::MINIMUM_PROJECT_NAME_LENGTH 24 | text = "Project name is too short - must be at least #{Project::MINIMUM_PROJECT_NAME_LENGTH} characters." 25 | sender.send_message(user, text) 26 | return 27 | end 28 | 29 | project = Project.create!(name: project_name) 30 | sender.send_message(user, "Project with name #{project.name} is created.") 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/services/auth/authenticate_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Auth 4 | class AuthenticateService < BaseService 5 | def initialize(params = {}) 6 | @email = params[:email] 7 | @password = params[:password] 8 | end 9 | 10 | def call 11 | process_response 12 | end 13 | 14 | private 15 | 16 | attr_reader :password, :email 17 | 18 | def process_response 19 | if token.present? 20 | { json: { token: token[:token], exp: token[:exp], user: @user.as_json(only: %i[id email]) } } 21 | else 22 | { json: { message: 'Invalid email or password' }, status: :unauthorized } 23 | end 24 | end 25 | 26 | def token 27 | @user ||= Admin.find_by(email: email) 28 | @user.present? && @user.valid_password?(password) && JsonWebToken.encode(user_id: @user.id) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/services/auth/authorize_api_request.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Auth 4 | class AuthorizeApiRequest < BaseService 5 | def initialize(headers = {}) 6 | @auth_header = headers['Authorization'] 7 | end 8 | 9 | def call 10 | user 11 | end 12 | 13 | private 14 | 15 | attr_reader :auth_header 16 | 17 | def token_from_headers 18 | @token ||= auth_header.scan(/Bearer (.*)$/)&.flatten&.first if auth_header.present? 19 | end 20 | 21 | def decoded_payload 22 | @payload ||= JsonWebToken.decode(token_from_headers) if token_from_headers.present? 23 | end 24 | 25 | def user 26 | @user ||= Admin.find_by(id: decoded_payload['user_id']) if decoded_payload.present? 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/services/auth/base_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Auth 4 | class BaseService 5 | def self.call(*args) 6 | new(*args).call 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/services/base_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class BaseService 4 | attr_reader :sender 5 | 6 | def self.call(*args) 7 | new(*args).call 8 | end 9 | 10 | def initialize 11 | @sender = Message::Sender.new 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/services/create_entry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateEntry < BaseService 4 | include ServiceHelper 5 | 6 | attr_reader :user, :text, :messages, :project_number 7 | 8 | def initialize(user, text, messages, project_number = nil) 9 | @user = user 10 | @text = text 11 | @messages = messages 12 | @project_number = project_number 13 | super() 14 | end 15 | 16 | def call 17 | match_data = text.match(Message::Conditions::ENTER_TIME_REGEXP) 18 | 19 | create_entry(match_data[1], match_data[2], match_data[3]) 20 | end 21 | 22 | private 23 | 24 | def create_entry(project_name, time, details) 25 | projects = find_project_by_name_like(project_name) 26 | precise_match = find_project_by_name(project_name) 27 | 28 | handle_multiple_projects.call(projects, precise_match) 29 | handle_no_projects.call(projects) 30 | add_entry_to_db(projects, precise_match, time, details) 31 | end 32 | 33 | def handle_multiple_projects 34 | proc do |projects, precise_match| 35 | if projects.count > 1 && project_number.nil? && precise_match.nil? 36 | outgoing_message = 'Specify the number of project : ' 37 | projects.each_with_index { |obj, i| outgoing_message += "\n#{i + 1} - #{obj.name}" } 38 | sender.send_message(user, outgoing_message) 39 | user.update!(last_message: text) 40 | return 41 | end 42 | end 43 | end 44 | 45 | def handle_no_projects 46 | proc do |projects| 47 | if projects.count.zero? 48 | sender.send_message(user, 'No such project.') 49 | ShowProjects.call(user) 50 | return 51 | end 52 | end 53 | end 54 | 55 | def add_entry_to_db(projects, precise_match, time, details) 56 | project = projects.count > 1 && project_number.present? ? projects[project_number - 1] : precise_match || projects.first 57 | user.time_entries.create!(project_id: project.id, time: time, details: details, date: Time.zone.today) 58 | 59 | notify_user(project, details, time) 60 | end 61 | 62 | def notify_user(project, details, time) 63 | message = "Set timesheet for #{Time.zone.today.strftime('%b %-d, %Y')} for #{project.name}: #{time}." 64 | message += "\nDetails: #{details || 'none'}." if details 65 | 66 | sender.send_message(user, message) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /app/services/create_entry_for_day.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateEntryForDay < BaseService 4 | include ServiceHelper 5 | 6 | attr_reader :user, :text, :project_number 7 | 8 | def initialize(user, text, project_number = nil) 9 | @user = user 10 | @text = text 11 | @project_number = project_number 12 | super() 13 | end 14 | 15 | def call 16 | match_data = text.match Message::Conditions::ENTER_TIME_FOR_DAY_REGEXP 17 | 18 | date_string = match_data[1] 19 | project_name = match_data[2] 20 | time = match_data[3] 21 | details = match_data[4] 22 | 23 | projects = find_project_by_name_like(project_name) 24 | precise_match = find_project_by_name(project_name) 25 | 26 | handle_multiple_projects.call(projects, precise_match) 27 | handle_no_projects.call(projects) 28 | 29 | project = projects.count > 1 && project_number.present? ? projects[project_number - 1] : precise_match || projects.first 30 | 31 | date = parse_date(date_string) 32 | check_date.call(date) 33 | 34 | add_entry_to_db(project, details, time, date) 35 | end 36 | 37 | def handle_multiple_projects 38 | proc do |projects, precise_match| 39 | if projects.count > 1 && project_number.nil? && precise_match.nil? 40 | outgoing_message = 'Specify the number of project : ' 41 | projects.each_with_index { |obj, i| outgoing_message += "\n#{i + 1} - #{obj.name}" } 42 | sender.send_message(user, outgoing_message) 43 | user.update!(last_message: text) 44 | return 45 | end 46 | end 47 | end 48 | 49 | def handle_no_projects 50 | proc do |projects| 51 | if projects.count.zero? 52 | sender.send_message(user, 'No such project.') 53 | ShowProjects.call(user) 54 | return 55 | end 56 | end 57 | end 58 | 59 | def add_entry_to_db(project, details, time, date) 60 | user.time_entries.create!(project_id: project.id, time: time, details: details, date: date) 61 | 62 | message = "Set timesheet for #{date.strftime('%b %-d, %Y')} for #{project.name}: #{time}." 63 | message += "\nDetails: #{details || 'none'}." if details 64 | 65 | sender.send_message(user, message) 66 | end 67 | 68 | def check_date 69 | proc do |date| 70 | if date > Time.zone.today 71 | sender.send_message(user, 'Please enter a valid date.') 72 | return 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /app/services/create_notification.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateNotification < BaseService 4 | include ServiceHelper 5 | 6 | attr_reader :user, :text 7 | 8 | def initialize(user, text) 9 | @user = user 10 | @text = text.match(Message::Conditions::NOTIFICATION_REGEXP) 11 | super() 12 | end 13 | 14 | def call 15 | return invalid_notify_at unless notify_at_is_valid? 16 | 17 | notification = Notification.create(message: message, notify_at: notify_at, creator: user) 18 | notification.users << users 19 | 20 | NotifierWorker.perform_at(notify_at - 10.minutes, notification.id) 21 | notification_created 22 | end 23 | 24 | private 25 | 26 | def notify_at_is_valid? 27 | notify_at > Time.current 28 | end 29 | 30 | def invalid_notify_at 31 | sender.send_message(user, 'Invalid date.') 32 | end 33 | 34 | def uids 35 | [user.uid, *text[1].strip.scan(/\w+/)].uniq 36 | end 37 | 38 | def notify_at 39 | date = parse_date(text[2]) 40 | hours, minutes = text[3].split(':') 41 | 42 | tz = TZInfo::Timezone.get('Europe/Kiev') 43 | tz.local_to_utc(Time.zone.local(date.year, date.month, date.day, hours, minutes)) 44 | end 45 | 46 | def message 47 | text[4] 48 | end 49 | 50 | def users 51 | User.where(uid: uids) 52 | end 53 | 54 | def recipients_for_slack 55 | uids.map { |uid| "<@#{uid}>" }.join(' ') 56 | end 57 | 58 | def notification_created 59 | tz = TZInfo::Timezone.get('Europe/Kiev') 60 | m = ":bell: You have a new notification from <@#{user.uid}>\n" 61 | m += ":timex: #{tz.utc_to_local(notify_at).strftime('%d.%m.%Y %H:%M')}\n" 62 | m += "\n" 63 | m += ":busts_in_silhouette: #{recipients_for_slack}\n" 64 | m += "\n" 65 | m += "*#{message}*" 66 | users.each do |user| 67 | sender.send_message(user, m) 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /app/services/dashboard_info.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class DashboardInfo < BaseService 4 | include ServiceHelper 5 | 6 | def initialize(filters) 7 | @filters = { 8 | date_from: filters[:filters][:date_from] || Time.zone.today.beginning_of_week.to_s, 9 | date_to: filters[:filters][:date_to] || Time.zone.today.end_of_week.to_s 10 | } 11 | @time_entries = TimeEntry.includes(:project, :user).filter(@filters) 12 | end 13 | 14 | def call 15 | data = {} 16 | data.merge!(date) 17 | data.merge!(hours) 18 | data.merge!(holidays) 19 | data.merge!(absent) 20 | data.merge!(pie_chart) 21 | data.merge!(bar_chart) 22 | end 23 | 24 | private 25 | 26 | attr_reader :filters 27 | 28 | def date 29 | { start_of_week: filters[:date_from], end_of_week: filters[:date_to] } 30 | end 31 | 32 | def holidays 33 | { holidays: Holiday.filter(filters).pluck(:name, :date) } 34 | end 35 | 36 | def absent 37 | absent = Absence.includes(:user).filter(filters).group_by(&:user) 38 | result = absent.map do |user, absences| 39 | { 40 | user: user.name, 41 | children: absences.map do |abs| 42 | { date: abs.date, reason: abs.reason, comment: abs.comment } 43 | end 44 | } 45 | end 46 | { absent: result } 47 | end 48 | 49 | def hours 50 | { hours_to_work: hours_to_work, hours_worked: hours_worked } 51 | end 52 | 53 | def hours_worked 54 | @time_entries.map { |p| p.minutes / 60.0 }.sum.round(2) 55 | end 56 | 57 | def hours_to_work 58 | users = @time_entries.pluck(:user_id).uniq 59 | working_days = (filters[:date_from].to_date..filters[:date_to].to_date).select do |day| 60 | !day.saturday? && !day.sunday? 61 | end 62 | (working_days.count - holidays['holidays']&.count.to_i) * 8 * (users.count - absent.count) 63 | end 64 | 65 | def pie_chart 66 | roles = hours_by_roles 67 | projects = hours_by_projects.compact 68 | { 69 | users_chart: { 70 | title: 'Users', 71 | data: roles, 72 | colors: projects_colors(roles) 73 | }, 74 | projects_chart: { 75 | title: 'Projects', 76 | innerSize: '75%', 77 | colors: projects_colors(projects), 78 | data: projects 79 | } 80 | } 81 | end 82 | 83 | def bar_chart 84 | users = User.active.order(:name) 85 | data = @time_entries.group_by(&:project).map do |project_entries| 86 | { 87 | name: project_entries.first.name, 88 | data: users.map do |user| 89 | minutes = project_entries.last.select { |t| t.user_id == user.id } 90 | .pluck(:minutes).sum 91 | minutes.zero? ? 0 : (minutes / 60.0).round(2) 92 | end 93 | } 94 | end 95 | { 96 | series: data, 97 | xAxisData: users.pluck(:name), 98 | colors: projects_colors(data) 99 | } 100 | end 101 | 102 | def hours_by_roles 103 | @time_entries.group_by { |t| t.user.role&.humanize || 'Other' } 104 | .map do |role_entries| 105 | total = role_entries.last.map { |rt| rt.minutes / 60.0 }.sum.round(2) 106 | { 107 | name: role_entries.first, 108 | y: total, 109 | z: total 110 | } 111 | end 112 | end 113 | 114 | def hours_by_projects 115 | @time_entries.group_by(&:project).map do |project_entries| 116 | total = project_entries.last.map { |p| p.minutes / 60.0 }.sum.round(2) 117 | { 118 | name: project_entries.first.name, 119 | y: total, 120 | z: total 121 | } 122 | end 123 | end 124 | 125 | def projects_colors(projects) 126 | projects.pluck(:name).map { |name| colorize(name) } 127 | end 128 | 129 | def colorize(string) 130 | "##{Digest::MD5.hexdigest(string).first(6)}" 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /app/services/do_not_understand.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class DoNotUnderstand < BaseService 4 | attr_reader :user, :messages 5 | 6 | def initialize(user, messages) 7 | @user = user 8 | @messages = messages 9 | super() 10 | end 11 | 12 | def call 13 | message = messages['understand'].sample 14 | sender.send_message(user, message['text'], message['options']) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/services/edit_entry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class EditEntry < BaseService 4 | include ServiceHelper 5 | 6 | attr_reader :user, :text 7 | 8 | def initialize(user, text) 9 | @user = user 10 | @text = text 11 | super() 12 | end 13 | 14 | def call 15 | match_data = text.match(Message::Conditions::EDIT_ENTRY_REGEXP) 16 | 17 | edit_entry(match_data[2], match_data[3], match_data[4], match_data[5], match_data[1]) 18 | end 19 | 20 | private 21 | 22 | def edit_entry(id, project_name, time, details, unparsed_new_date) 23 | time_entry = TimeEntry.find(id) 24 | if time_entry.user != user 25 | sender.send_message(user, "This time entry isn't yours.") 26 | return 27 | end 28 | 29 | project = find_project_by_name(project_name) 30 | 31 | new_date = time_entry.date 32 | if unparsed_new_date.present? 33 | date_match = unparsed_new_date.match(/(\d+)\.(\d+)\.?(\d+)?/) 34 | new_date = Date.new((date_match[3] || Time.zone.today.year).to_i, date_match[2].to_i, date_match[1].to_i) 35 | end 36 | 37 | time_entry.update(time: time, 38 | project_id: project.id, 39 | details: details, 40 | date: new_date) 41 | 42 | sender.send_message(user, 'The time entry was successfully updated.') 43 | rescue ActiveRecord::RecordNotFound 44 | sender.send_message(user, "The time entry with ID *#{id}* was not found.") 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /app/services/find_project.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class FindProject < BaseService 4 | include ServiceHelper 5 | 6 | attr_reader :user, :text 7 | 8 | def initialize(user, text) 9 | @user = user 10 | @text = text 11 | super() 12 | end 13 | 14 | def call 15 | project_name = text.match(Message::Conditions::FIND_PROJECT_REGEXP)[1] 16 | projects = scan_projects_by_name(project_name) 17 | 18 | if projects.blank? 19 | sender.send_message(user, "Projects with name `#{project_name}` not found.") 20 | return 21 | end 22 | 23 | answer = '```' 24 | answer += "Search results: \n" 25 | projects.map do |project| 26 | answer += project.name.ljust(20).to_s 27 | answer += "Alias: #{project.alias if project.alias.presence}" 28 | answer += "\n" 29 | end 30 | answer += '```' 31 | 32 | sender.send_message(user, answer) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/services/finish_dialog.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class FinishDialog < BaseService 4 | attr_reader :user, :messages 5 | 6 | def initialize(user, messages) 7 | @user = user 8 | @messages = messages 9 | super() 10 | end 11 | 12 | def call 13 | message = messages['thanks'].sample 14 | user.update(is_speaking: false) 15 | sender.send_message(user, message['text'], message['options']) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/services/remove_entry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RemoveEntry < BaseService 4 | include Message::Conditions 5 | attr_reader :user, :time_entry_id 6 | 7 | def initialize(user, text) 8 | @user = user 9 | @time_entry_id = text.match(REMOVE_ENTRY_REGEXP)[0][/\d+/] 10 | super() 11 | end 12 | 13 | def call 14 | last_entry = TimeEntry.where(user_id: user.id).last 15 | 16 | if id_given? && TimeEntry.find(time_entry_id).user != user 17 | sender.send_message(user, "This time entry isn't yours.") 18 | return 19 | end 20 | 21 | id_given? ? TimeEntry.find(time_entry_id).destroy : last_entry.destroy 22 | 23 | message = id_given? ? "Entry with ID #{time_entry_id} successfully removed." : "Your last entry (#{last_entry.description}) was successfully removed." 24 | sender.send_message(user, message) 25 | end 26 | 27 | def id_given? 28 | time_entry_id.present? 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/services/reports/base_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Reports 4 | class BaseService 5 | def self.call(*args) 6 | new(*args).call 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/services/reports/estimation_report_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Reports 4 | class EstimationReportService < BaseService 5 | def initialize(options) 6 | @filters = options[:filters] || {} 7 | @pagination = options[:pagination] || {} 8 | end 9 | 10 | def call 11 | { json: data } 12 | end 13 | 14 | private 15 | 16 | attr_reader :filters, :pagination 17 | 18 | def data 19 | { data: entities, meta: meta } 20 | end 21 | 22 | def entities 23 | entities_grouped_by_ticket.reduce([]) do |acum, (ticket, entries)| 24 | acum << { 25 | projects: entries.map { |t| { id: t.project.id, name: t.project.name } }.uniq, 26 | details: ticket, 27 | created_at: entries.last.date.strftime('%d %b, %Y'), 28 | trello_labels: formatted_labels(entries.last.trello_labels)[:labels], 29 | estimate: formatted_labels(entries.last.trello_labels)[:estimate], 30 | status: trello_list[entries.last.trello_ticket_id].try(:[], 'name'), 31 | total_time: (entries.pluck(:minutes).sum / 60.0).round(1), 32 | collaborators: entries.map { |t| { id: t.user.id, name: t.user.name } }.uniq 33 | } 34 | end 35 | end 36 | 37 | def entities_grouped_by_ticket 38 | time_entries.paginate(pagination) 39 | .group_by { |t| t.ticket || t.details&.downcase } 40 | .each { |_k, v| v.sort! { |a, b| a.created_at <=> b.created_at } } 41 | end 42 | 43 | def meta 44 | { total_count: time_entries.count } 45 | end 46 | 47 | def time_entries 48 | @time_entries ||= TimeEntry.includes(:project, :user).filter(filters) 49 | end 50 | 51 | def trello_ticket_ids 52 | @trello_ticket_ids ||= time_entries.map(&:trello_ticket_id).reject(&:blank?) 53 | end 54 | 55 | def trello_list 56 | @trello_list ||= ::Reports::TrelloListService.call(trello_ticket_ids) 57 | end 58 | 59 | def filtering_params 60 | params.permit(:date_from, :with_ticket, :date_to, by_projects: [], by_users: []) 61 | end 62 | 63 | def formatted_labels(trello_labels) 64 | return {} if trello_labels.blank? 65 | 66 | labels = if trello_labels.is_a? Array 67 | trello_labels.flatten.compact.uniq 68 | else 69 | trello_labels.split(',') 70 | end 71 | regexp = /^\d+$/ 72 | labels = { 73 | estimate: labels.select { |l| l.match(regexp) }, 74 | labels: labels.reject { |l| l.match(regexp) } 75 | } 76 | labels.each { |k, v| labels[k] = v.to_s.gsub(/[{}"\[\]\\]/, '') } 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /app/services/reports/time_report_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Reports 4 | class TimeReportService < BaseService 5 | def initialize(options) 6 | @filters = options[:filters] || {} 7 | @filters[:date_from] ||= Time.zone.today.beginning_of_week 8 | @filters[:date_to] ||= Time.zone.today 9 | @pagination = options[:pagination] || {} 10 | end 11 | 12 | def call 13 | { json: data } 14 | end 15 | 16 | private 17 | 18 | attr_reader :filters, :pagination 19 | 20 | def data 21 | { data: time_entries, meta: meta } 22 | end 23 | 24 | def time_entries 25 | entries = assign_trello_list_to(time_entry_collection) 26 | entries.as_json( 27 | only: %i[id date time details trello_labels estimated_time], 28 | methods: %i[status], 29 | include: %i[user project] 30 | ) 31 | end 32 | 33 | def meta 34 | { total_count: time_entry_collection.count } 35 | end 36 | 37 | def time_entry_collection 38 | @time_entry_collection ||= TimeEntry.includes(:user, :project).filter(filters) 39 | end 40 | 41 | def trello_ticket_ids 42 | @trello_ticket_ids ||= time_entry_collection.map(&:trello_ticket_id).reject(&:blank?) 43 | end 44 | 45 | def trello_list 46 | @trello_list ||= ::Reports::TrelloListService.call(trello_ticket_ids) 47 | end 48 | 49 | def assign_trello_list_to(collection) 50 | collection.each { |te| te.status = trello_list[te.trello_ticket_id].try(:[], 'name') } 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /app/services/reports/trello_list_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Reports 4 | class TrelloListService < BaseService 5 | ONE_CALL_LIMIT = 100 6 | 7 | attr_reader :ids 8 | 9 | def initialize(ids) 10 | @ids = ids 11 | end 12 | 13 | def call 14 | result = {} 15 | ids.uniq.each_slice(ONE_CALL_LIMIT) do |batch| 16 | url = URI.parse("#{api_url}&urls=#{ticket_urls_for(batch)}") 17 | res = Net::HTTP.get(url) 18 | res = JSON.parse(res) 19 | res = res.map { |r| r['200'] } 20 | res = batch.zip(res).to_h 21 | result.merge!(res) 22 | end 23 | result 24 | end 25 | 26 | private 27 | 28 | def api_url 29 | c = ::Trello.configuration 30 | "https://api.trello.com/1/batch/?key=#{c.developer_public_key}&token=#{c.member_token}" 31 | end 32 | 33 | def ticket_urls_for(keys) 34 | keys.map { |key| "/cards/#{key}/list?fields=name" }.join(',') 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/services/reports/user_absence_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Reports 4 | class UserAbsenceService < BaseService 5 | def initialize(filters) 6 | @filters = filters[:filters] || {} 7 | @filters[:date_from] ||= Time.zone.today.beginning_of_week 8 | @filters[:date_to] ||= Time.zone.today.end_of_week 9 | end 10 | 11 | def call 12 | { json: data } 13 | end 14 | 15 | private 16 | 17 | attr_reader :filters 18 | 19 | def data 20 | { data: users } 21 | end 22 | 23 | def users 24 | Absence.select(sql_for_select) 25 | .joins(:user) 26 | .filter(filters) 27 | .group('users.id') 28 | end 29 | 30 | def sql_for_select 31 | <<~SQL 32 | users.id, 33 | users.name, 34 | COUNT(CASE absences.reason WHEN 0 THEN 1 END) AS vacation, 35 | COUNT(CASE absences.reason WHEN 1 THEN 1 END) AS illness, 36 | COUNT(CASE absences.reason WHEN 2 THEN 1 END) AS other 37 | SQL 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/services/reports/user_report_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Reports 4 | class UserReportService < BaseService 5 | def initialize(filters) 6 | @filters = filters[:filters] || {} 7 | @start_date = @filters[:date_from]&.to_date || Time.zone.today.beginning_of_week 8 | @end_date = @filters[:date_to]&.to_date || Time.zone.today.end_of_week 9 | end 10 | 11 | def call 12 | { json: data } 13 | end 14 | 15 | private 16 | 17 | attr_reader :filters 18 | 19 | def data 20 | { data: user_hours } 21 | end 22 | 23 | def user_hours 24 | users = User.includes(:absences).active.map do |user| 25 | worked = hours_worked(user) 26 | to_work = hours_to_work(user) 27 | { 28 | name: user.name, 29 | hours_worked: worked, 30 | hours_to_work: to_work, 31 | difference: worked - to_work 32 | } 33 | end 34 | 35 | users.sort { |a, b| b[:hours_worked] <=> a[:hours_worked] } 36 | end 37 | 38 | def hours_worked(user) 39 | entries = user.time_entries.in_interval(@start_date, @end_date) 40 | (entries.map(&:minutes).select(&:present?).inject(&:+).to_f / 60.0).round(2) 41 | end 42 | 43 | def hours_to_work(user) 44 | working_days = @end_date >= Time.zone.today ? (@start_date..Time.zone.today) : (@start_date..@end_date) 45 | working_days = working_days.select { |day| !day.saturday? && !day.sunday? } 46 | holidays = Holiday.pluck(:date) 47 | absence = user.absences.where.not(reason: 'other').pluck(:date) 48 | (working_days - holidays - absence).count * 8 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /app/services/reports/user_worked_time_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Reports 4 | class UserWorkedTimeService < BaseService 5 | def initialize(filters) 6 | @filters = filters[:filters] || {} 7 | end 8 | 9 | def call 10 | { json: data } 11 | end 12 | 13 | private 14 | 15 | attr_reader :filters 16 | 17 | def data 18 | { data: users } 19 | end 20 | 21 | def users 22 | TimeEntry.includes(:project, :user).filter(filters).group_by(&:user).map do |user, user_entries| 23 | { 24 | id: user.id, 25 | name: user.name, 26 | total_time_spent: (user_entries.pluck(:minutes).inject(:+).to_f / 60.0).round(2), 27 | projects: projects(user_entries) 28 | } 29 | end 30 | end 31 | 32 | def projects(user_entries) 33 | user_entries.group_by(&:project).map do |project, project_entries| 34 | { 35 | id: project.id, 36 | name: project.name, 37 | time_spent: (project_entries.pluck(:minutes).inject(:+).to_f / 60.0).round(2) 38 | } 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/services/service_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ServiceHelper 4 | def find_project_by_name(project_name) 5 | Project.where(['lower(name) = ? OR lower(alias) = ?', project_name.downcase, project_name.downcase]).first 6 | end 7 | 8 | def find_project_by_name_like(project_name) 9 | Project.where('lower(name) like ? OR lower(alias) like ?', "%#{project_name.downcase}%", "%#{project_name.downcase}%") 10 | end 11 | 12 | def scan_projects_by_name(project_name) 13 | search_value = "%#{project_name.downcase}%" 14 | Project.where('lower(name) LIKE ? OR lower(alias) LIKE ?', search_value, search_value) 15 | end 16 | 17 | def suitable_start_date(start_date) 18 | launch_date = ENV['TIMEBOT_LAUNCH_DATE'] ? Date.parse(ENV['TIMEBOT_LAUNCH_DATE']) : nil 19 | launch_date && launch_date > start_date ? launch_date : start_date 20 | end 21 | 22 | def parse_date(date_string) 23 | match_data = date_string.match(/^(\d?\d)\.(\d?\d)(?:\.(\d?\d?\d\d))?$/) 24 | day = match_data[1].to_i 25 | month = match_data[2].to_i 26 | year = match_data[3] ? match_data[3].to_i : Time.zone.today.year 27 | year += 2000 if year < 100 28 | Date.new(year, month, day) 29 | end 30 | 31 | def parse_time(time) 32 | match_data = time.match(/^(\d?\d):([0-5]\d)$/) 33 | match_data[1].to_i * 60 + match_data[2].to_i 34 | end 35 | 36 | def format_time(minutes) 37 | format('%2d:%02d', minutes / 60, minutes % 60) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /app/services/set_absence.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SetAbsence < BaseService 4 | attr_reader :user, :text 5 | 6 | def initialize(user, text) 7 | @user = user 8 | @text = text 9 | super() 10 | end 11 | 12 | def call 13 | match_data = text.match Message::Conditions::SET_ABSENCE_REGEXP 14 | reason = match_data[1].downcase 15 | start_date = end_date = parse_date(match_data[2]) 16 | end_date = parse_date(match_data[3]) if match_data[3] 17 | comment = match_data[4] 18 | 19 | if Absence.reasons.key?(reason) && start_date <= end_date 20 | (start_date..end_date).reject(&:day_off_holiday?).each do |date| 21 | unless user.add_absence(reason, date, comment) 22 | sender.send_message(user, 'Absence validation failed. Check `help` for the examples.') 23 | return nil 24 | end 25 | end 26 | text = "Set #{reason} from #{start_date.strftime('%b %e, %Y')} to #{end_date.strftime('%b %e, %Y')}." 27 | text = "Set #{reason} for #{start_date.strftime('%b %e, %Y')}." if start_date == end_date 28 | sender.send_message(user, text) 29 | else 30 | sender.send_message(user, 'Invalid reason or invalid date(s).') 31 | end 32 | end 33 | 34 | Date.class_eval('def day_off_holiday?; saturday? || sunday? || Holiday.holiday?(self) end') 35 | end 36 | -------------------------------------------------------------------------------- /app/services/show_help.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ShowHelp < BaseService 4 | attr_reader :user 5 | 6 | def initialize(user) 7 | @user = user 8 | super() 9 | end 10 | 11 | def call 12 | message = File.open(Rails.root.join('public', 'messages', 'help.txt').to_s, 'r').read 13 | sender.send_message(user, message) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/services/show_notifications.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ShowNotifications < BaseService 4 | include ServiceHelper 5 | 6 | attr_reader :user 7 | 8 | def initialize(user) 9 | @user = user 10 | super() 11 | end 12 | 13 | def call 14 | notifications = user.notifications.where('notify_at >= ?', Time.current) 15 | tz = TZInfo::Timezone.get('Europe/Kiev') 16 | text = if notifications.empty? 17 | 'No notifications added yet.' 18 | else 19 | notifications.map do |notification| 20 | recipients_for_slack = notification.users.pluck(:uid).map { |uid| "<@#{uid}>" }.join(' ') 21 | 22 | m = ":bell: You have notification from <@#{notification.creator.uid}>\n" 23 | m += ":timex: #{tz.utc_to_local(notification.notify_at).strftime('%d.%m.%Y %H:%M')}\n" 24 | m += "\n" 25 | m += ":busts_in_silhouette: #{recipients_for_slack}\n" 26 | m += "\n" 27 | m + "*#{notification.message}*" 28 | end.join("\n:heavy_minus_sign::heavy_minus_sign::heavy_minus_sign:\n") 29 | end 30 | sender.send_message(user, text) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/services/show_projects.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ShowProjects < BaseService 4 | attr_reader :user 5 | 6 | def initialize(user) 7 | @user = user 8 | super() 9 | end 10 | 11 | def call 12 | projects = Project.order(:id) 13 | text = if projects.empty? 14 | 'No projects added yet.' 15 | else 16 | projects.map { |project| "#{project.name.ljust(20)} Alias: #{project.alias}" }.join("\n") 17 | end 18 | sender.send_message(user, "```#{text}```") 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/services/show_report.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ShowReport < BaseService 4 | include ServiceHelper 5 | 6 | attr_reader :user, :text 7 | 8 | def initialize(user, text) 9 | @user = user 10 | @text = text.match(Message::Conditions::MESSAGE_IN_REPORT) 11 | super() 12 | end 13 | 14 | def call 15 | time = text[:alias][1..-1].sub('l', 'last ').sub('d', 'day').sub('w', 'week').sub('m', 'month') if text[:alias] 16 | time = text[:time].downcase unless text[:alias] 17 | project_name = text[:project].try(:downcase) 18 | 19 | if project_name.present? 20 | project = find_project_by_name(project_name) 21 | if project.present? 22 | handle_show_by(time, project) 23 | else 24 | sender.send_message(user, 'No such project.') 25 | handle_message_show_projects 26 | end 27 | else 28 | handle_show_by time 29 | end 30 | end 31 | 32 | private 33 | 34 | def handle_show_by(time, project = nil) 35 | case time 36 | when 'day' 37 | handle_report(Time.zone.today, Time.zone.today, project) 38 | when 'last day' 39 | handle_report(Time.zone.today - 1, Time.zone.today - 1, project) 40 | when 'week' 41 | handle_report(Time.zone.now.beginning_of_week.to_date, Time.zone.today, project) 42 | when 'last week' 43 | handle_report(1.week.ago.beginning_of_week.to_date, 1.week.ago.end_of_week.to_date, project) 44 | when 'month' 45 | handle_report(Time.zone.now.beginning_of_month.to_date, Time.zone.today, project) 46 | when 'last month' 47 | handle_report(1.month.ago.beginning_of_month.to_date, 1.month.ago.end_of_month.to_date, project) 48 | end 49 | end 50 | 51 | def handle_report(start_date, end_date, project) 52 | date = suitable_start_date(start_date) 53 | 54 | list = (date..end_date).to_a.map do |day| 55 | entries = user.time_entries.where(date: day) 56 | entries = entries.where(project_id: project.id) if project.present? 57 | entries unless entries.empty? 58 | entries.empty? ? [day, nil, []] : [day, entries.map(&:minutes).sum, entries.map(&:description).join("\n")] 59 | end 60 | 61 | strings = list.map do |day, minutes, entries| 62 | "`#{day.strftime('%d.%m.%y` (%A)')}: #{":timex: *#{format_time(minutes)}*" if minutes} #{entries.empty? ? 'No entries' : "\n#{entries}"}" 63 | end 64 | 65 | strings << "*Total*: #{user.total_time_for_range(start_date, end_date, project)}." 66 | sender.send_message(user, strings.join("\n")) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /app/services/show_worked_hours.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ShowWorkedHours < BaseService 4 | include ServiceHelper 5 | include Message::Conditions 6 | 7 | attr_accessor :user, :start_date, :end_date 8 | 9 | def initialize(user, date_period) 10 | @user = user 11 | @start_date, @end_date = init_dates(date_period) 12 | super() 13 | end 14 | 15 | def call 16 | begin 17 | @start_date = get_date(@start_date) 18 | @end_date = get_date(@end_date) 19 | rescue StandardError => _e 20 | sender.send_message(user, "Invalid date period: #{start_date} - #{end_date}") 21 | return 22 | end 23 | 24 | if end_date - start_date > 700 25 | sender.send_message(user, 'Too wide date period: ' + (@end_date - @start_date).to_i.to_s + 'days') 26 | return 27 | end 28 | message = '```' 29 | message += "Period : #{start_date} - #{end_date}\n" 30 | message += "Hours worked : #{hours_worked}\n" 31 | message += "Estimated hours worked : #{estimated_hours_worked}\n" 32 | message += "Difference : #{difference}\n" 33 | message += '```' 34 | 35 | sender.send_message(user, message) 36 | end 37 | 38 | private 39 | 40 | def init_dates(date_period) 41 | today = Time.zone.today 42 | if date_period[0].match(WORKED_HOURS_MONTH) 43 | ["16.#{(today.day > 15 ? today : today - 1.month).strftime('%m.%Y')}", 44 | "15.#{(today.day > 15 ? today + 1.month : today).strftime('%m.%Y')}"] 45 | elsif date_period[0].match(WORKED_HOURS_PREV_MONTH) 46 | ["16.#{(today.day > 15 ? today - 1.month : today - 2.months).strftime('%m.%Y')}", 47 | "15.#{(today.day > 15 ? today : today - 1.month).strftime('%m.%Y')}"] 48 | else 49 | [date_period[1], date_period[2]] 50 | end 51 | end 52 | 53 | def get_date(date) 54 | date.split('.').count > 2 && date.split('.')[2].size == 2 ? date.sub(/(\d{2})$/) { "20#{$1}" }.to_date : "#{date}.#{Time.zone.now.year}".to_date 55 | end 56 | 57 | def hours_worked 58 | entries = user.time_entries.in_interval(start_date, end_date) 59 | total_time(entries) 60 | end 61 | 62 | def estimated_hours_worked 63 | working_days = if end_date == start_date 64 | [start_date] 65 | elsif end_date > Time.zone.today 66 | (start_date..Time.zone.today) 67 | else 68 | (start_date..end_date) 69 | end 70 | working_days = working_days.select { |day| !day.saturday? && !day.sunday? } 71 | holidays = Holiday.pluck(:date) 72 | absence = user.absences.pluck(:date) 73 | (working_days - holidays - absence).count * 8 74 | end 75 | 76 | def difference 77 | hours = estimated_hours_worked 78 | (hours_worked - hours).round(2) if hours 79 | end 80 | 81 | def total_time(time_entries) 82 | (time_entries.map(&:minutes).select(&:present?).inject(&:+).to_f / 60.0).round(2) 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /app/services/slack_engine/adapters/base_adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SlackEngine 4 | module Adapters 5 | class BaseAdapter 6 | def self.call(*args) 7 | new(*args).call 8 | end 9 | 10 | private 11 | 12 | attr_reader :command, :params 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/services/slack_engine/adapters/command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SlackEngine 4 | module Adapters 5 | class Command < BaseAdapter 6 | def initialize(params) 7 | @params = params 8 | @command = params[:command] 9 | end 10 | 11 | def call 12 | SlackEngine::Commands.const_get(command.parameterize.classify).new(params) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/services/slack_engine/adapters/submission.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SlackEngine 4 | module Adapters 5 | class Submission < BaseAdapter 6 | def initialize(params) 7 | @params = JSON.parse(params[:payload]) 8 | @command = @params['callback_id'] 9 | end 10 | 11 | def call 12 | SlackEngine::Submissions.const_get(command.classify).new(params) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/services/slack_engine/collback_constants.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SlackEngine 4 | module CollbackConstants 5 | LOGTIME = 'logtime' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/services/slack_engine/commands/logtime.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SlackEngine 4 | module Commands 5 | class Logtime 6 | def initialize(params) 7 | @trigger_id = params[:trigger_id] 8 | @client = Slack::Web::Client.new(token: ENV.fetch('TIMEBOT_APP_TOKEN')) 9 | end 10 | 11 | def perform 12 | client.dialog_open(dialog: dialog, trigger_id: trigger_id) 13 | end 14 | 15 | private 16 | 17 | attr_reader :client, :trigger_id 18 | 19 | def dialog 20 | { 21 | callback_id: SlackEngine::CollbackConstants::LOGTIME, 22 | title: 'Log time', 23 | submit_label: 'Add', 24 | elements: elements 25 | } 26 | end 27 | 28 | def elements 29 | [ 30 | { 31 | label: 'Select project', type: 'select', name: 'project', 32 | options: Project.order_by_entries_number.map { |p| { label: p.name, value: p.id } } 33 | }, 34 | { 35 | label: 'Week', type: 'select', name: 'week', value: 0, 36 | options: [{ label: 'Current', value: 0 }, { label: 'Previous', value: 1 }] 37 | }, 38 | { 39 | label: 'Select date', type: 'select', name: 'date', value: Time.current.strftime('%Y-%m-%d'), 40 | options: date_range 41 | }, 42 | { type: 'text', label: 'Time spent (00:00)', name: 'time' }, 43 | { label: 'What have you done?', name: 'details', type: 'textarea' } 44 | ] 45 | end 46 | 47 | def date_range 48 | (Time.current.beginning_of_week.to_date..Time.current.end_of_week.to_date) 49 | .to_a 50 | .map { |t| { label: t.strftime('%A'), value: t.strftime('%Y-%m-%d') } } 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /app/services/slack_engine/processor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SlackEngine 4 | class Processor 5 | def initialize(params, adapter:) 6 | @params = params 7 | @adapter = ::SlackEngine::Adapters.const_get(adapter.to_s.classify).call(params) 8 | end 9 | 10 | def self.perform(*args) 11 | new(*args).call 12 | end 13 | 14 | def call 15 | adapter.perform 16 | end 17 | 18 | private 19 | 20 | attr_reader :adapter, :params 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/services/slack_engine/submissions/logtime.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SlackEngine 4 | module Submissions 5 | class Logtime 6 | def initialize(params) 7 | @params = params.with_indifferent_access 8 | @client = Slack::Web::Client.new 9 | end 10 | 11 | def perform 12 | time_entry = TimeEntry.create(time_params) 13 | if time_entry.errors.any? 14 | { json: { errors: errors(time_entry) } } 15 | else 16 | @client.chat_postMessage(channel: params[:user][:id], text: success_message(time_entry), as_user: true) && { json: {} } 17 | end 18 | end 19 | 20 | private 21 | 22 | attr_reader :params, :user 23 | 24 | def time_params 25 | date = params.dig(:submission, :date).to_date 26 | week_number = params.dig(:submission, :week).to_i 27 | params[:submission][:user_id] = User.find_by(uid: params[:user][:id])&.id 28 | params[:submission][:project_id] = params[:submission][:project] 29 | time_params = params[:submission].slice(:time, :details, :user_id, :project_id) 30 | time_params[:date] = date - week_number.week 31 | time_params 32 | end 33 | 34 | def errors(time_entry) 35 | time_entry.errors.messages.map { |name, message| { name: name.to_s, error: message.first } } 36 | end 37 | 38 | def success_message(time_entry) 39 | date = time_entry.date.strftime('%b %-d, %Y') 40 | project_name = time_entry.project.name 41 | time = time_entry.time 42 | 43 | message = "*Set timesheet for:* #{date} for *#{project_name}:* #{time}." 44 | message += "\n*Details:* #{time_entry.details || 'none'}." if time_entry.details 45 | message 46 | end 47 | 48 | def error_message(time_entry) 49 | time_entry.errors.full_messages.map { |message| "*#{message}*" }.join("\n") 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /app/services/specify_project.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SpecifyProject < BaseService 4 | include ServiceHelper 5 | 6 | attr_reader :user, :text, :messages 7 | 8 | def initialize(user, text, messages) 9 | @user = user 10 | @text = text 11 | @messages = messages 12 | super() 13 | end 14 | 15 | def call 16 | match_data = text.match(Message::Conditions::SPECIFY_PROJECT)[0].to_i 17 | 18 | if match_data < 1 || (last_msg = user.last_message).nil? 19 | DoNotUnderstand.call(user, messages) 20 | elsif last_msg.match(Message::Conditions::ENTER_TIME_REGEXP) 21 | CreateEntry.call(user, last_msg, messages, match_data) 22 | elsif last_msg.match(Message::Conditions::ENTER_TIME_FOR_DAY_REGEXP) 23 | CreateEntryForDay.call(user, last_msg, match_data) 24 | else 25 | DoNotUnderstand.call(user, messages) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/services/start_conversation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class StartConversation < BaseService 4 | attr_reader :user, :messages 5 | 6 | def initialize(user, messages) 7 | @user = user 8 | @messages = messages 9 | super() 10 | end 11 | 12 | def call 13 | message = messages['today'].sample 14 | sender.send_message(user, message['text'], message['options']) 15 | user.update(is_speaking: true) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Timebot 5 | <%= csrf_meta_tags %> 6 | 7 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> 8 | <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> 9 | 10 | 11 | 12 | <%= yield %> 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /app/workers/notifier_worker.rb: -------------------------------------------------------------------------------- 1 | class NotifierWorker 2 | include Sidekiq::Worker 3 | 4 | sidekiq_options queue: 'notifier', retry: 2 5 | 6 | def perform(id) 7 | notification = Notification.find(id) 8 | sender = Message::Sender.new 9 | recipients_for_slack = notification.users.pluck(:uid).map { |uid| "<@#{uid}>" }.join(' ') 10 | 11 | tz = TZInfo::Timezone.get('Europe/Kiev') 12 | m = ":bell: Hey don't forget about notification from <@#{notification.creator.uid}>\n" 13 | m += ":timex: #{tz.utc_to_local(notification.notify_at).strftime('%d.%m.%Y %H:%M')}\n" 14 | m += "\n" 15 | m += ":busts_in_silhouette: #{recipients_for_slack}\n" 16 | m += "\n" 17 | m += "*#{notification.message}*" 18 | 19 | notification.user_notifications.where(delivered: false).each do |user_notification| 20 | sender.send_message(user_notification.user, m) 21 | user_notification.update(delivered: true) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /bin/bot: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | command = ARGV[0] 5 | 6 | available_commands = %w(start stop) 7 | 8 | if ARGV.size != 1 || !available_commands.include?(command) 9 | raise 'Invalid command' 10 | end 11 | 12 | def start 13 | stream = %w(log/production.log a) 14 | 15 | puma_pid = spawn('bundle', 'exec', 'puma', '-e', 'production', out: stream, err: stream) 16 | bot_pid = spawn({ 'RAILS_ENV' => 'production' }, 'bundle', 'exec', 'rake', 'slack:start_bot', out: stream, err: stream) 17 | 18 | Process.detach(puma_pid) 19 | Process.detach(bot_pid) 20 | 21 | File.write('tmp/pids/puma.pid', puma_pid) 22 | File.write('tmp/pids/bot.pid', bot_pid) 23 | end 24 | 25 | def stop 26 | kill_process('puma') 27 | kill_process('bot') 28 | end 29 | 30 | def kill_process(type) 31 | raise ArgumentError, 'wrong process type' unless %w(puma bot).include?(type) 32 | begin 33 | pid = File.read("tmp/pids/#{type}.pid").to_i 34 | Process.kill('KILL', pid) 35 | rescue => e 36 | puts e.inspect 37 | end 38 | end 39 | 40 | send(command) 41 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 4 | load Gem.bin_path('bundler', 'bundle') 5 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | begin 4 | load File.expand_path('../spring', __FILE__) 5 | rescue LoadError => e 6 | raise unless e.message.include?('spring') 7 | end 8 | APP_PATH = File.expand_path('../config/application', __dir__) 9 | require_relative '../config/boot' 10 | require 'rails/commands' 11 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | begin 4 | load File.expand_path('../spring', __FILE__) 5 | rescue LoadError => e 6 | raise unless e.message.include?('spring') 7 | end 8 | require_relative '../config/boot' 9 | require 'rake' 10 | Rake.application.run 11 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | require 'pathname' 4 | require 'fileutils' 5 | include FileUtils 6 | 7 | # path to your application root. 8 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 9 | 10 | def system!(*args) 11 | system(*args) || abort("\n== Command #{args} failed ==") 12 | end 13 | 14 | chdir APP_ROOT do 15 | # This script is a starting point to setup your application. 16 | # Add necessary setup steps to this file. 17 | 18 | puts '== Installing dependencies ==' 19 | system! 'gem install bundler --conservative' 20 | system('bundle check') || system!('bundle install') 21 | 22 | # puts "\n== Copying sample files ==" 23 | # unless File.exist?('config/database.yml') 24 | # cp 'config/database.yml.sample', 'config/database.yml' 25 | # end 26 | 27 | puts "\n== Preparing database ==" 28 | system! 'bin/rails db:setup' 29 | 30 | puts "\n== Removing old logs and tempfiles ==" 31 | system! 'bin/rails log:clear tmp:clear' 32 | 33 | puts "\n== Restarting application server ==" 34 | system! 'bin/rails restart' 35 | end 36 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # This file loads spring without using Bundler, in order to be fast. 5 | # It gets overwritten when you run the `spring binstub` command. 6 | 7 | unless defined?(Spring) 8 | require 'rubygems' 9 | require 'bundler' 10 | 11 | if (match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m)) 12 | Gem.paths = { 'GEM_PATH' => [Bundler.bundle_path.to_s, *Gem.path].uniq.join(Gem.path_separator) } 13 | gem 'spring', match[1] 14 | require 'spring/binstub' 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | require 'pathname' 4 | require 'fileutils' 5 | include FileUtils 6 | 7 | # path to your application root. 8 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 9 | 10 | def system!(*args) 11 | system(*args) || abort("\n== Command #{args} failed ==") 12 | end 13 | 14 | chdir APP_ROOT do 15 | # This script is a way to update your development environment automatically. 16 | # Add necessary update steps to this file. 17 | 18 | puts '== Installing dependencies ==' 19 | system! 'gem install bundler --conservative' 20 | system('bundle check') || system!('bundle install') 21 | 22 | puts "\n== Updating database ==" 23 | system! 'bin/rails db:migrate' 24 | 25 | puts "\n== Removing old logs and tempfiles ==" 26 | system! 'bin/rails log:clear tmp:clear' 27 | 28 | puts "\n== Restarting application server ==" 29 | system! 'bin/rails restart' 30 | end 31 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # This file is used by Rack-based servers to start the application. 3 | 4 | require_relative 'config/environment' 5 | 6 | run Rails.application 7 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 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 Timebot 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 | config.middleware.insert_before 0, Rack::Cors do 16 | allow do 17 | origins '*' 18 | resource '*', headers: :any, methods: %i[get post put delete options] 19 | end 20 | end 21 | 22 | config.eager_load_paths << "#{config.root}/lib" 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: async 6 | 7 | production: 8 | adapter: redis 9 | url: redis://localhost:6379/1 10 | -------------------------------------------------------------------------------- /config/database.yml.example: -------------------------------------------------------------------------------- 1 | defaults: &defaults 2 | adapter: postgresql 3 | username: postgres 4 | pool: 5 5 | timeout: 5000 6 | 7 | development: 8 | <<: *defaults 9 | database: timebot_development 10 | 11 | test: 12 | <<: *defaults 13 | database: timebot_test 14 | 15 | production: 16 | <<: *defaults 17 | database: timebot_production 18 | -------------------------------------------------------------------------------- /config/deploy.rb: -------------------------------------------------------------------------------- 1 | # config valid only for current version of Capistrano 2 | # lock '3.6.1' 3 | 4 | set :application, 'timebot' 5 | set :repo_url, 'git@gitlab.codica.com:codica2/timebot.git' 6 | 7 | # Default branch is :master 8 | # ask :branch, `git rev-parse --abbrev-ref HEAD`.chomp 9 | 10 | set :branch, 'master' 11 | # Default deploy_to directory is /var/www/my_app_name 12 | set :deploy_to, '/home/dev/www/timebot' 13 | 14 | # Default value for :scm is :git 15 | # set :scm, :git 16 | 17 | # Default value for :format is :airbrussh. 18 | # set :format, :airbrussh 19 | 20 | # You can configure the Airbrussh format using :format_options. 21 | # These are the defaults. 22 | # set :format_options, command_output: true, log_file: 'log/capistrano.log', color: :auto, truncate: :auto 23 | 24 | # Default value for :pty is false 25 | # set :pty, true 26 | set :sidekiq_config, "#{current_path}/config/sidekiq.yml" 27 | set :sidekiq_env, 'production' 28 | 29 | # Default value for :linked_files is [] 30 | append :linked_files, 'config/secrets.yml', 'config/database.yml', 'config/messages.yml', 'config/puma.rb', '.ruby-version', '.ruby-gemset', '.env' 31 | 32 | # Default value for linked_dirs is [] 33 | append :linked_dirs, 'log', 'tmp/pids', 'tmp/sockets', 'tmp/cache', 'public/assets' 34 | 35 | # Default value for default_env is {} 36 | # set :default_env, { path: "/opt/ruby/bin:$PATH" } 37 | 38 | # Default value for keep_releases is 5 39 | # set :keep_releases, 5 40 | 41 | namespace :deploy do 42 | before :finished, 'slack:slack_stop' 43 | after :finished, 'deploy:restart' 44 | after :finished, 'slack:slack_start' 45 | end 46 | -------------------------------------------------------------------------------- /config/deploy.rb.example: -------------------------------------------------------------------------------- 1 | # config valid only for current version of Capistrano 2 | lock '3.6.1' 3 | 4 | set :application, 'timebot' 5 | set :repo_url, 'repo_url' 6 | 7 | # Default branch is :master 8 | # ask :branch, `git rev-parse --abbrev-ref HEAD`.chomp 9 | 10 | # Default deploy_to directory is /var/www/my_app_name 11 | set :deploy_to, '/home/username/www/timebot' 12 | 13 | # Default value for :scm is :git 14 | # set :scm, :git 15 | 16 | # Default value for :format is :airbrussh. 17 | # set :format, :airbrussh 18 | 19 | # You can configure the Airbrussh format using :format_options. 20 | # These are the defaults. 21 | # set :format_options, command_output: true, log_file: 'log/capistrano.log', color: :auto, truncate: :auto 22 | 23 | # Default value for :pty is false 24 | # set :pty, true 25 | 26 | # Default value for :linked_files is [] 27 | append :linked_files, 'config/database.yml', 'config/messages.yml', '.ruby-version', '.ruby-gemset', '.env' 28 | 29 | # Default value for linked_dirs is [] 30 | append :linked_dirs, 'log', 'tmp/pids', 'tmp/cache', 'public/assets' 31 | 32 | # Default value for default_env is {} 33 | # set :default_env, { path: "/opt/ruby/bin:$PATH" } 34 | 35 | # Default value for keep_releases is 5 36 | # set :keep_releases, 5 37 | 38 | namespace :deploy do 39 | after :publishing, :restart 40 | end 41 | -------------------------------------------------------------------------------- /config/deploy/production.rb: -------------------------------------------------------------------------------- 1 | # server-based syntax 2 | # ====================== 3 | # Defines a single server with a list of roles and multiple properties. 4 | # You can define all roles on a single server, or split them: 5 | 6 | server '172.104.151.14', user: 'dev', roles: %w{app db web}, port: 22 7 | # server 'example.com', user: 'deploy', roles: %w{app web}, other_property: :other_value 8 | # server 'db.example.com', user: 'deploy', roles: %w{db} 9 | set :rvm_ruby_version, '2.3.1@timebot' 10 | 11 | 12 | # role-based syntax 13 | # ================== 14 | 15 | # Defines a role with one or multiple servers. The primary server in each 16 | # group is considered to be the first unless any hosts have the primary 17 | # property set. Specify the username and a domain or IP for the server. 18 | # Don't use `:all`, it's a meta role. 19 | 20 | # role :app, %w{deploy@example.com}, my_property: :my_value 21 | # role :web, %w{user1@primary.com user2@additional.com}, other_property: :other_value 22 | # role :db, %w{deploy@example.com} 23 | 24 | 25 | 26 | # Configuration 27 | # ============= 28 | # You can set any configuration variable like in config/deploy.rb 29 | # These variables are then only loaded and set in this stage. 30 | # For available Capistrano configuration variables see the documentation page. 31 | # http://capistranorb.com/documentation/getting-started/configuration/ 32 | # Feel free to add new variables to customise your setup. 33 | 34 | 35 | 36 | # Custom SSH Options 37 | # ================== 38 | # You may pass any option but keep in mind that net/ssh understands a 39 | # limited set of options, consult the Net::SSH documentation. 40 | # http://net-ssh.github.io/net-ssh/classes/Net/SSH.html#method-c-start 41 | # 42 | # Global options 43 | # -------------- 44 | # set :ssh_options, { 45 | # keys: %w(/home/rlisowski/.ssh/id_rsa), 46 | # forward_agent: false, 47 | # auth_methods: %w(password) 48 | # } 49 | # 50 | # The server-based syntax can be used to override options: 51 | # ------------------------------------ 52 | # server 'example.com', 53 | # user: 'user_name', 54 | # roles: %w{web app}, 55 | # ssh_options: { 56 | # user: 'user_name', # overrides user setting above 57 | # keys: %w(/home/user_name/.ssh/id_rsa), 58 | # forward_agent: false, 59 | # auth_methods: %w(publickey password) 60 | # # password: 'please use keys' 61 | # } 62 | -------------------------------------------------------------------------------- /config/deploy/production.rb.example: -------------------------------------------------------------------------------- 1 | # server-based syntax 2 | # ====================== 3 | # Defines a single server with a list of roles and multiple properties. 4 | # You can define all roles on a single server, or split them: 5 | 6 | server 'servername', user: 'username', roles: %w{app}, port: 22 7 | # server 'example.com', user: 'deploy', roles: %w{app web}, other_property: :other_value 8 | # server 'db.example.com', user: 'deploy', roles: %w{db} 9 | 10 | 11 | 12 | # role-based syntax 13 | # ================== 14 | 15 | # Defines a role with one or multiple servers. The primary server in each 16 | # group is considered to be the first unless any hosts have the primary 17 | # property set. Specify the username and a domain or IP for the server. 18 | # Don't use `:all`, it's a meta role. 19 | 20 | # role :app, %w{deploy@example.com}, my_property: :my_value 21 | # role :web, %w{user1@primary.com user2@additional.com}, other_property: :other_value 22 | # role :db, %w{deploy@example.com} 23 | 24 | 25 | 26 | # Configuration 27 | # ============= 28 | # You can set any configuration variable like in config/deploy.rb 29 | # These variables are then only loaded and set in this stage. 30 | # For available Capistrano configuration variables see the documentation page. 31 | # http://capistranorb.com/documentation/getting-started/configuration/ 32 | # Feel free to add new variables to customise your setup. 33 | 34 | 35 | 36 | # Custom SSH Options 37 | # ================== 38 | # You may pass any option but keep in mind that net/ssh understands a 39 | # limited set of options, consult the Net::SSH documentation. 40 | # http://net-ssh.github.io/net-ssh/classes/Net/SSH.html#method-c-start 41 | # 42 | # Global options 43 | # -------------- 44 | # set :ssh_options, { 45 | # keys: %w(/home/rlisowski/.ssh/id_rsa), 46 | # forward_agent: false, 47 | # auth_methods: %w(password) 48 | # } 49 | # 50 | # The server-based syntax can be used to override options: 51 | # ------------------------------------ 52 | # server 'example.com', 53 | # user: 'user_name', 54 | # roles: %w{web app}, 55 | # ssh_options: { 56 | # user: 'user_name', # overrides user setting above 57 | # keys: %w(/home/user_name/.ssh/id_rsa), 58 | # forward_agent: false, 59 | # auth_methods: %w(publickey password) 60 | # # password: 'please use keys' 61 | # } 62 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.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. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | if Rails.root.join('tmp/caching-dev.txt').exist? 17 | config.action_controller.perform_caching = true 18 | 19 | config.cache_store = :memory_store 20 | config.public_file_server.headers = { 21 | 'Cache-Control' => 'public, max-age=172800' 22 | } 23 | else 24 | config.action_controller.perform_caching = false 25 | 26 | config.cache_store = :null_store 27 | end 28 | 29 | # Don't care if the mailer can't send. 30 | config.action_mailer.raise_delivery_errors = false 31 | 32 | config.action_mailer.perform_caching = false 33 | 34 | # Print deprecation notices to the Rails logger. 35 | config.active_support.deprecation = :log 36 | 37 | # Raise an error on page load if there are pending migrations. 38 | config.active_record.migration_error = :page_load 39 | 40 | # Debug mode disables concatenation and preprocessing of assets. 41 | # This option may cause significant delays in view rendering with a large 42 | # number of complex assets. 43 | config.assets.debug = true 44 | 45 | # Suppress logger output for asset requests. 46 | config.assets.quiet = true 47 | 48 | # Raises error for missing translations 49 | # config.action_view.raise_on_missing_translations = true 50 | 51 | # Use an evented file watcher to asynchronously detect changes in source code, 52 | # routes, locales, etc. This feature depends on the listen gem. 53 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 54 | end 55 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.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 threaded 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 | # Disable serving static files from the `/public` folder by default since 18 | # Apache or NGINX already handles this. 19 | config.public_file_server.enabled = true 20 | 21 | # Compress JavaScripts and CSS. 22 | config.assets.js_compressor = :uglifier 23 | # config.assets.css_compressor = :sass 24 | 25 | # Do not fallback to assets pipeline if a precompiled asset is missed. 26 | config.assets.compile = false 27 | 28 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 29 | 30 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 31 | # config.action_controller.asset_host = 'http://assets.example.com' 32 | 33 | # Specifies the header that your server uses for sending files. 34 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 35 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 36 | 37 | # Mount Action Cable outside main process or domain 38 | # config.action_cable.mount_path = nil 39 | # config.action_cable.url = 'wss://example.com/cable' 40 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 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 | # Use the lowest log level to ensure availability of diagnostic information 46 | # when problems arise. 47 | config.log_level = :debug 48 | 49 | # Prepend all log lines with the following tags. 50 | config.log_tags = [ :request_id ] 51 | 52 | # Use a different cache store in production. 53 | # config.cache_store = :mem_cache_store 54 | 55 | # Use a real queuing backend for Active Job (and separate queues per environment) 56 | # config.active_job.queue_adapter = :resque 57 | # config.active_job.queue_name_prefix = "timebot_#{Rails.env}" 58 | config.action_mailer.perform_caching = false 59 | config.action_mailer.delivery_method = :sendmail 60 | config.action_mailer.default_url_options = { host: 'http://timebot.codica.com' } 61 | 62 | # Ignore bad email addresses and do not raise email delivery errors. 63 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 64 | config.action_mailer.raise_delivery_errors = true 65 | 66 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 67 | # the I18n.default_locale when a translation cannot be found). 68 | config.i18n.fallbacks = true 69 | 70 | # Send deprecation notices to registered listeners. 71 | config.active_support.deprecation = :notify 72 | 73 | # Use default logging formatter so that PID and timestamp are not suppressed. 74 | config.log_formatter = ::Logger::Formatter.new 75 | 76 | # Use a different logger for distributed setups. 77 | # require 'syslog/logger' 78 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 79 | 80 | if ENV["RAILS_LOG_TO_STDOUT"].present? 81 | logger = ActiveSupport::Logger.new(STDOUT) 82 | logger.formatter = config.log_formatter 83 | config.logger = ActiveSupport::TaggedLogging.new(logger) 84 | end 85 | 86 | # Do not dump schema after migrations. 87 | config.active_record.dump_schema_after_migration = false 88 | end 89 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.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 public file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 18 | 'Cache-Control' => 'public, max-age=3600' 19 | } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | config.action_mailer.perform_caching = false 31 | 32 | # Tell Action Mailer not to deliver emails to the real world. 33 | # The :test delivery method accumulates sent emails in the 34 | # ActionMailer::Base.deliveries array. 35 | config.action_mailer.delivery_method = :test 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /config/initializers/active_model_serializers.rb: -------------------------------------------------------------------------------- 1 | ActiveModelSerializers.config.jsonapi_pagination_links_enabled = false 2 | ActiveModelSerializers.config.adapter = :json_api 3 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ApplicationController.renderer.defaults.merge!( 4 | # http_host: 'example.org', 5 | # https: false 6 | # ) -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | -------------------------------------------------------------------------------- /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/constants.rb: -------------------------------------------------------------------------------- 1 | PER_PAGE = 30 2 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/initializers/new_framework_defaults.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains migration options to ease your Rails 5.0 upgrade. 4 | # 5 | # Read the Rails 5.0 release notes for more info on each option. 6 | 7 | # Enable per-form CSRF tokens. Previous versions had false. 8 | Rails.application.config.action_controller.per_form_csrf_tokens = true 9 | 10 | # Enable origin-checking CSRF mitigation. Previous versions had false. 11 | Rails.application.config.action_controller.forgery_protection_origin_check = true 12 | 13 | # Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`. 14 | # Previous versions had false. 15 | ActiveSupport.to_time_preserves_timezone = true 16 | 17 | # Require `belongs_to` associations by default. Previous versions had false. 18 | Rails.application.config.active_record.belongs_to_required_by_default = true 19 | 20 | # Do not halt callback chains when a callback returns false. Previous versions had true. 21 | ActiveSupport.halt_callback_chains_on_return_false = false 22 | 23 | # Configure SSL options to enable HSTS with subdomains. Previous versions had false. 24 | Rails.application.config.ssl_options = { hsts: { subdomains: true } } 25 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_timebot_session' 4 | -------------------------------------------------------------------------------- /config/initializers/slack.rb: -------------------------------------------------------------------------------- 1 | Slack.configure do |config| 2 | config.token = ENV['SLACK_TOKEN'] 3 | end 4 | -------------------------------------------------------------------------------- /config/initializers/trello.rb: -------------------------------------------------------------------------------- 1 | require 'trello' 2 | 3 | Trello.configure do |config| 4 | config.developer_public_key = ENV['TRELLO_DEVELOPER_PUBLIC_KEY'] 5 | config.member_token = ENV['TRELLO_MEMBER_TOKEN'] 6 | end 7 | -------------------------------------------------------------------------------- /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] 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/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 | active_admin: 24 | filters: 25 | predicates: 26 | gteq_date: greater or equal than 27 | lteq_date: less or equal than -------------------------------------------------------------------------------- /config/messages.example.yml: -------------------------------------------------------------------------------- 1 | understand: 2 | - 3 | text: "I don't understand you." 4 | options: 5 | :attachments: [ 6 | { 7 | title: '', 8 | image_url: 'http://bestanimations.com/Sci-Fi/StarWars/R2D2/r2d2-c3po-animated-gif-3.gif' 9 | } 10 | ] 11 | - 12 | text: 'Slack API' 13 | options: 14 | :attachments: [ 15 | { 16 | "fallback": "Required plain-text summary of the attachment.", 17 | "color": "#36a64f", 18 | "pretext": "Optional text that appears above the attachment block", 19 | "author_name": "Bobby Tables", 20 | "author_link": "http://flickr.com/bobby/", 21 | "author_icon": "http://flickr.com/icons/bobby.jpg", 22 | "title": "Slack API Documentation", 23 | "title_link": "https://api.slack.com/", 24 | "text": "Optional text that appears within the attachment", 25 | "fields": [ 26 | { 27 | "title": "Priority", 28 | "value": "High", 29 | "short": false 30 | } 31 | ], 32 | "image_url": "http://my-website.com/path/to/image.jpg", 33 | "thumb_url": "http://example.com/path/to/thumb.png", 34 | "footer": "Slack API", 35 | "footer_icon": "https://platform.slack-edge.com/img/default_application_icon.png", 36 | "ts": 123456789 37 | } 38 | ] 39 | thanks: 40 | - 41 | text: "Thanks bro! Have a nice day! :smiley: " 42 | options: 43 | :attachments: [] 44 | 45 | log: 46 | - 47 | text: 'The entry was successfully created.' 48 | options: 49 | :attachments: [] 50 | today: 51 | - 52 | text: 'Hey mate, what did you do today?' 53 | options: 54 | :attachments: [] -------------------------------------------------------------------------------- /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 }.to_i 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 | # The code in the `on_worker_boot` will be called if you are using 36 | # clustered mode by specifying a number of `workers`. After each worker 37 | # process is booted this block will be run, if you are using `preload_app!` 38 | # option you will want to use this block to reconnect to any threads 39 | # or connections that may have been created at application boot, Ruby 40 | # cannot share connections between processes. 41 | # 42 | # on_worker_boot do 43 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord) 44 | # end 45 | 46 | # Allow puma to be restarted by `rails restart` command. 47 | plugin :tmp_restart 48 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | require "sidekiq/web" 3 | Sidekiq::Web.use Rack::Auth::Basic do |username, password| 4 | ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(username), ::Digest::SHA256.hexdigest(ENV["SIDEKIQ_USERNAME"])) & 5 | ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(password), ::Digest::SHA256.hexdigest(ENV["SIDEKIQ_PASSWORD"])) 6 | end if Rails.env.production? 7 | mount Sidekiq::Web, at: "/sidekiq" 8 | 9 | namespace :api do 10 | namespace :v1 do 11 | namespace :auth do 12 | resources :sessions, only: [:create] 13 | end 14 | resources :projects, except: [:new, :edit] do 15 | collection do 16 | get :search 17 | delete :delete_multiple 18 | end 19 | end 20 | resources :users, except: [:new, :edit] do 21 | collection do 22 | get :search 23 | post :sync_users 24 | delete :delete_multiple 25 | end 26 | end 27 | resources :time_entries, except: [:new, :edit] do 28 | delete :delete_multiple, on: :collection 29 | end 30 | resources :teams, except: [:new, :edit] do 31 | delete :delete_multiple, on: :collection 32 | end 33 | resources :holidays, except: [:new, :edit] do 34 | delete :delete_multiple, on: :collection 35 | end 36 | resources :absences, except: [:new, :edit] do 37 | delete :delete_multiple, on: :collection 38 | end 39 | resources :admins, except: [:new, :edit] do 40 | delete :delete_multiple, on: :collection 41 | end 42 | 43 | scope :dashboard do 44 | get '/' => 'dashboard#index' 45 | end 46 | 47 | scope :reports, module: :reports do 48 | resources :estimation_reports, only: [:index] 49 | resources :time_reports, only: [:index] 50 | resources :user_reports, only: [:index] do 51 | collection do 52 | get :worked_time 53 | get :absence 54 | end 55 | end 56 | end 57 | 58 | scope :slack, module: :slack do 59 | post '/submission' => 'slack#submission' 60 | post '/command' => 'slack#command' 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /config/schedule.rb: -------------------------------------------------------------------------------- 1 | # Use this file to easily define all of your cron jobs. 2 | # 3 | # It's helpful, but not entirely necessary to understand cron before proceeding. 4 | # http://en.wikipedia.org/wiki/Cron 5 | 6 | # Example: 7 | # 8 | # set :output, "/path/to/my/cron_log.log" 9 | # 10 | # every 2.hours do 11 | # command "/usr/bin/some_great_command" 12 | # runner "MyModel.some_method" 13 | # rake "some:great:rake:task" 14 | # end 15 | # 16 | # every 4.days do 17 | # runner "AnotherModel.prune_old_records" 18 | # end 19 | 20 | # Learn more: http://github.com/javan/whenever 21 | 22 | every 1.day, at: '23:35 pm' do 23 | rake 'slack:start_conversation', environment: :development 24 | end 25 | 26 | every 1.day, at: '12:00 pm' do 27 | rake 'slack:greet' 28 | end 29 | -------------------------------------------------------------------------------- /config/sidekiq.yml: -------------------------------------------------------------------------------- 1 | :concurrency: 5 2 | :queues: 3 | - [notifier, 3] -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ).each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /db/migrate/20160807153225_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :users do |t| 4 | t.string :name 5 | t.string :uid 6 | 7 | t.timestamps 8 | end 9 | 10 | add_index :users, :uid 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20160807154431_add_needs_asking_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddNeedsAskingToUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :users, :conversation_stage, :integer, default: 0 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160807174332_create_time_entries.rb: -------------------------------------------------------------------------------- 1 | class CreateTimeEntries < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :time_entries do |t| 4 | t.integer :user_id 5 | t.date :date 6 | t.string :time 7 | t.integer :minutes 8 | t.string :details 9 | 10 | t.timestamps 11 | end 12 | 13 | add_foreign_key :time_entries, :users 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20160808100052_create_projects.rb: -------------------------------------------------------------------------------- 1 | class CreateProjects < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :projects do |t| 4 | t.string :name 5 | 6 | t.timestamps 7 | end 8 | 9 | add_column :time_entries, :project_id, :integer 10 | add_foreign_key :time_entries, :projects 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20160808110903_add_is_speaking_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddIsSpeakingToUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :users, :is_speaking, :boolean, default: false 4 | remove_column :users, :conversation_stage 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20160811131313_create_admins.rb: -------------------------------------------------------------------------------- 1 | class CreateAdmins < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :admins do |t| 4 | 5 | t.timestamps 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20160811131325_add_devise_to_admins.rb: -------------------------------------------------------------------------------- 1 | class AddDeviseToAdmins < ActiveRecord::Migration[5.0] 2 | def self.up 3 | change_table :admins 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.inet :current_sign_in_ip 20 | t.inet :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 | # Uncomment below if timestamps were not included in your original model. 35 | # t.timestamps null: false 36 | end 37 | 38 | add_index :admins, :email, unique: true 39 | add_index :admins, :reset_password_token, unique: true 40 | # add_index :admins, :confirmation_token, unique: true 41 | # add_index :admins, :unlock_token, unique: true 42 | end 43 | 44 | def self.down 45 | # By default, we don't want to make any assumption about how to roll back a migration when your 46 | # model already existed. Please edit below which fields you would like to remove in this migration. 47 | raise ActiveRecord::IrreversibleMigration 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /db/migrate/20160814120433_add_alias_to_projects.rb: -------------------------------------------------------------------------------- 1 | class AddAliasToProjects < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :projects, :alias, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160822065951_add_is_active_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddIsActiveToUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :users, :is_active, :boolean, default: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160822073621_add_is_absent_and_reason_to_time_entries.rb: -------------------------------------------------------------------------------- 1 | class AddIsAbsentAndReasonToTimeEntries < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :time_entries, :is_absent, :boolean, default: false 4 | add_column :time_entries, :reason, :integer 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20161007142406_create_holidays.rb: -------------------------------------------------------------------------------- 1 | class CreateHolidays < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :holidays do |t| 4 | t.string :name 5 | t.date :date 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20161028094405_create_absences.rb: -------------------------------------------------------------------------------- 1 | class CreateAbsences < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :absences do |t| 4 | t.integer :user_id 5 | t.date :date 6 | t.integer :reason 7 | t.string :comment 8 | 9 | t.timestamps 10 | end 11 | 12 | add_foreign_key :absences, :users 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20161028114143_remove_is_absent_and_reason_from_time_entries.rb: -------------------------------------------------------------------------------- 1 | class RemoveIsAbsentAndReasonFromTimeEntries < ActiveRecord::Migration[5.0] 2 | def change 3 | remove_column :time_entries, :is_absent 4 | remove_column :time_entries, :reason 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20180306092732_create_teams.rb: -------------------------------------------------------------------------------- 1 | class CreateTeams < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :teams do |t| 4 | t.string :name 5 | t.string :description 6 | t.integer :team_lead_id 7 | t.integer :project_manager_id 8 | 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20180306093524_add_relations_team_and_users_and_projects.rb: -------------------------------------------------------------------------------- 1 | class AddRelationsTeamAndUsersAndProjects < ActiveRecord::Migration[5.0] 2 | def up 3 | add_column :projects, :team_id, :integer 4 | add_column :users, :team_id, :integer 5 | end 6 | 7 | def down 8 | remove_column :projects, :team_id, :integer 9 | remove_column :users, :team_id, :integer 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20180330160731_add_last_message_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddLastMessageToUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :users, :last_message, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180404084359_add_index_to_projects.rb: -------------------------------------------------------------------------------- 1 | class AddIndexToProjects < ActiveRecord::Migration[5.0] 2 | def change 3 | add_index :projects, :name, unique: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180620192339_add_trello_labels_to_time_entries.rb: -------------------------------------------------------------------------------- 1 | class AddTrelloLabelsToTimeEntries < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :time_entries, :trello_labels, :text, array: true, default: [] 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180828152514_add_ticket_to_time_entries.rb: -------------------------------------------------------------------------------- 1 | class AddTicketToTimeEntries < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :time_entries, :ticket, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180920133731_remove_team_lead_and_manager_from_team.rb: -------------------------------------------------------------------------------- 1 | class RemoveTeamLeadAndManagerFromTeam < ActiveRecord::Migration[5.0] 2 | def change 3 | remove_column :teams, :team_lead_id, :interger 4 | remove_column :teams, :project_manager_id, :interger 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20180926110804_add_role_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddRoleToUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :users, :role, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20181212142838_add_active_column_to_projects.rb: -------------------------------------------------------------------------------- 1 | class AddActiveColumnToProjects < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :projects, :active, :boolean, default: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20181224163929_create_notifications.rb: -------------------------------------------------------------------------------- 1 | class CreateNotifications < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :notifications do |t| 4 | t.belongs_to :creator 5 | t.string :message 6 | t.datetime :notify_at 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20181225093651_create_user_notifications.rb: -------------------------------------------------------------------------------- 1 | class CreateUserNotifications < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :user_notifications do |t| 4 | t.belongs_to :user, index: true 5 | t.belongs_to :notification, index: true 6 | t.boolean :delivered, default: false 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /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 rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) 7 | # Character.create(name: 'Luke', movie: movies.first) 8 | 9 | Project.create!(name: 'Fame and Partners') 10 | Project.create!(name: 'CAREP') 11 | Project.create!(name: 'Analist') 12 | Project.create!(name: 'Fintech') 13 | 14 | Admin.create!(email: 'admin@example.com', password: 'password', password_confirmation: 'password') -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codica2/timebot/ee7396d297abbd83161da60bd268ba4a2d648f66/lib/assets/.keep -------------------------------------------------------------------------------- /lib/capistrano/tasks/puma.cap: -------------------------------------------------------------------------------- 1 | namespace :puma do 2 | desc 'Create Directories for Puma Pids and Socket' 3 | task :make_dirs do 4 | on roles(:app) do 5 | execute "mkdir #{shared_path}/tmp/sockets -p" 6 | execute "mkdir #{shared_path}/tmp/pids -p" 7 | end 8 | end 9 | 10 | before :start, :make_dirs 11 | end 12 | -------------------------------------------------------------------------------- /lib/capistrano/tasks/restart.rake: -------------------------------------------------------------------------------- 1 | 2 | namespace :deploy do 3 | 4 | desc 'Restart application' 5 | task :restart do 6 | on roles(:app), in: :sequence, wait: 5 do 7 | invoke 'puma:restart' 8 | end 9 | end 10 | 11 | desc 'Restart application' 12 | task :stop do 13 | on roles(:app), in: :sequence, wait: 5 do 14 | invoke 'puma:stop' 15 | end 16 | end 17 | 18 | # desc 'Initial Deploy' 19 | # task :initial do 20 | # on roles(:app) do 21 | # before 'deploy:restart', 'puma:start' 22 | # invoke 'deploy' 23 | # end 24 | # end 25 | end 26 | 27 | 28 | -------------------------------------------------------------------------------- /lib/capistrano/tasks/restart_slack.rake: -------------------------------------------------------------------------------- 1 | namespace :slack do 2 | 3 | desc 'Start Slack client' 4 | task :slack_start do 5 | on roles(:app) do 6 | within release_path do 7 | execute :bundle, "exec rake slack:start_bot RAILS_ENV=production 2>&1 >> log/production.log &" 8 | end 9 | end 10 | end 11 | 12 | desc "Stop Slack client" 13 | task :slack_stop do 14 | on roles([:app]) do 15 | within release_path do 16 | with rails_env: fetch(:rails_env) do 17 | execute :bundle, 'exec rake slack:stop_bot RAILS_ENV=production' 18 | end 19 | end 20 | end 21 | end 22 | 23 | desc "Restart Slack client" 24 | task :slack_restart do 25 | on roles(:all) do 26 | if test("[ -f #{deploy_to}/current/tmp/pids/bot.pid ]") 27 | run_locally do 28 | invoke 'slack:slack_stop' 29 | invoke 'slack:slack_start' 30 | end 31 | else 32 | run_locally do 33 | invoke 'slack:slack_start' 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/event_handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | class EventHandler 3 | include Message::Conditions 4 | include Message::Logger 5 | 6 | attr_reader :client, :data, :sender, :messages, :public_channels 7 | 8 | def initialize(client, data, messages, public_channels) 9 | @client = client 10 | @data = data 11 | @public_channels = public_channels 12 | @sender = Message::Sender.new 13 | @messages = messages 14 | end 15 | 16 | def handle_message 17 | return if message_is_not_processable 18 | user = User.find_by(uid: data.user) 19 | log_incoming_message(user, data.text) 20 | if message_is_enter_time 21 | CreateEntry.call(user, data.text, messages) 22 | elsif message_is_specify_project 23 | SpecifyProject.call(user, data.text, messages) 24 | elsif message_is_request_for_help 25 | ShowHelp.call(user) 26 | elsif message_is_remove_entry 27 | RemoveEntry.call(user, data.text) 28 | elsif message_is_show_reports 29 | ShowReport.call(user, data.text) 30 | elsif message_is_enter_time_for_day 31 | CreateEntryForDay.call(user, data.text) 32 | elsif message_is_request_for_project 33 | ShowProjects.call(user) 34 | elsif message_is_edit_entry 35 | EditEntry.call(user, data.text) 36 | elsif message_is_worked_hours 37 | match_data = data.text.match(WORKED_HOURS_MONTH) || data.text.match(WORKED_HOURS_PREV_MONTH) || data.text.match(WORKED_HOURS) 38 | ShowWorkedHours.call(user, match_data) 39 | elsif message_is_add_project 40 | AddProject.call(user, data.text) 41 | elsif message_is_over(user) 42 | FinishDialog.call(user, messages) 43 | elsif message_is_set_absence 44 | SetAbsence.call(user, data.text) 45 | elsif message_is_find_project 46 | FindProject.call(user, data.text) 47 | elsif message_is_absence_days 48 | AbsenceDays.call(user, data.text) 49 | elsif message_is_new_notification 50 | CreateNotification.call(user, data.text) 51 | elsif message_is_show_notifications 52 | ShowNotifications.call(user) 53 | else 54 | DoNotUnderstand.call(user, messages) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Helper 3 | def suitable_start_date(start_date) 4 | launch_date = ENV['TIMEBOT_LAUNCH_DATE'] ? Date.parse(ENV['TIMEBOT_LAUNCH_DATE']) : nil 5 | launch_date && launch_date > start_date ? launch_date : start_date 6 | end 7 | 8 | def parse_date(date_string) 9 | match_data = date_string.match(/^(\d?\d)\.(\d?\d)(?:\.(\d?\d?\d\d))?$/) 10 | day = match_data[1].to_i 11 | month = match_data[2].to_i 12 | year = match_data[3] ? match_data[3].to_i : Time.zone.today.year 13 | year = year + 2000 if year < 100 14 | Date.new(year, month, day) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/json_web_token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class JsonWebToken 4 | ALGORYTHM = 'HS256' 5 | 6 | class << self 7 | def encode(payload, exp = 2.weeks) 8 | payload[:exp] = Time.current.to_i + exp.to_i 9 | { token: JWT.encode(payload, auth_secret, ALGORYTHM), exp: exp.to_i } 10 | end 11 | 12 | def decode(token) 13 | JWT.decode(token, auth_secret, ALGORYTHM).first 14 | rescue JWT::ExpiredSignature, JWT::DecodeError, JWT::VerificationError => _e 15 | raise ExceptionHandler::UnauthorizedRequestError, 'Unauthorized' 16 | end 17 | 18 | private 19 | 20 | def auth_secret 21 | ENV.fetch('SECRET_KEY') 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/message/conditions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Message 3 | module Conditions 4 | ENTER_TIME_FOR_DAY_REGEXP = /^(?:update\s+?)?(\d?\d\.\d?\d(?:\.(?:\d\d)?\d\d)?)\s+\*?([^:]+?-?)[\*\s-]+?(\d?\d:[0-5]\d)[-\s]+([^\s](?:.|\s)*[^\s])\s*$/ 5 | ENTER_TIME_REGEXP = /^\*?(?!update\s)(?!edit\s)(?!add\s)(?!set)([^\.]+?-?)[\s\*-]+?(\d?\d:[0-5]\d)[\s-]+?([^\s-](?:.|\s)*[^\s])\s*$/ 6 | ADD_PROJECT_REGEXP = /^ *add project (\w.*?) *$/ 7 | SET_ABSENCE_REGEXP = /^ *set (.+?) (\d?\d\.\d?\d(?:\.(?:\d\d)?\d\d)?)(?: - (\d?\d\.\d?\d(?:\.(?:\d\d)?\d\d)?)?)?(.+)?$/ 8 | MESSAGE_IN_REPORT = /^ *(?:(?:show (?