├── .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 (?