├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .gitlab-ci.yml ├── .rubocop.yml ├── .travis.yml ├── Capfile ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── Makefile ├── Procfile ├── README.md ├── Rakefile ├── app ├── assets │ ├── config │ │ └── manifest.js │ ├── images │ │ └── .keep │ ├── javascripts │ │ ├── application.js │ │ ├── cable.js │ │ ├── channels │ │ │ └── .keep │ │ └── web │ │ │ └── admin │ │ │ └── menus │ │ │ └── edit.js │ └── stylesheets │ │ └── application.css.sass ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── controllers │ ├── api │ │ ├── admin │ │ │ └── menus_controller.rb │ │ ├── application_controller.rb │ │ ├── dishes_controller.rb │ │ ├── user │ │ │ ├── application_controller.rb │ │ │ └── menus_controller.rb │ │ ├── user_menu_votes_controller.rb │ │ ├── user_menus_controller.rb │ │ └── users_controller.rb │ ├── application_controller.rb │ ├── concerns │ │ └── .keep │ └── web │ │ ├── admin │ │ ├── application_controller.rb │ │ ├── daily_reports_controller.rb │ │ ├── dishes_controller.rb │ │ ├── menus_controller.rb │ │ ├── monthly_reports_controller.rb │ │ ├── users_controller.rb │ │ └── welcome_controller.rb │ │ ├── application_controller.rb │ │ ├── remind_passwords_controller.rb │ │ ├── sessions_controller.rb │ │ ├── user │ │ ├── application_controller.rb │ │ └── passwords_controller.rb │ │ └── welcome_controller.rb ├── decorators │ ├── dish_decorator.rb │ ├── dishes_decorator.rb │ ├── user_decorator.rb │ └── users_decorator.rb ├── helpers │ ├── application_helper.rb │ ├── auth_helper.rb │ └── web │ │ └── admin │ │ ├── dishes_helper.rb │ │ ├── menus_helper.rb │ │ └── monthly_reports_helper.rb ├── jobs │ └── application_job.rb ├── mailers │ ├── application_mailer.rb │ └── user_mailer.rb ├── models │ ├── application_record.rb │ ├── concerns │ │ ├── .keep │ │ └── voteable_concern.rb │ ├── dish.rb │ ├── menu.rb │ ├── menu_dish.rb │ ├── menu_observer.rb │ ├── user.rb │ ├── user_menu.rb │ ├── user_menu_dish.rb │ ├── vote.rb │ ├── vote_down.rb │ └── vote_up.rb ├── policies │ ├── admin_policy.rb │ ├── application_policy.rb │ ├── cook_policy.rb │ └── user_policy.rb ├── repositories │ ├── dish_repository.rb │ ├── menu_dish_repository.rb │ ├── menu_repository.rb │ ├── user_menu_repository.rb │ └── user_repository.rb ├── serializers │ ├── dish_serializer.rb │ ├── menu_dish_serializer.rb │ ├── menu_serializer.rb │ ├── user_menu_serializer.rb │ ├── user_menu_vote_type_serializer.rb │ └── user_serializer.rb ├── services │ ├── dishes_service.rb │ ├── menus_service.rb │ ├── monthly_report_service.rb │ └── votes_service.rb ├── types │ ├── menu_publish_type.rb │ ├── user_menu_update_type.rb │ ├── user_menu_vote_type.rb │ ├── user_password_edit_type.rb │ ├── user_remind_password_type.rb │ └── user_sign_in_type.rb ├── uploaders │ └── dish_image_uploader.rb ├── validators │ ├── email_validator.rb │ ├── menu_publish_readiness_validator.rb │ └── restream_email_validator.rb ├── views │ ├── kaminari │ │ ├── _first_page.html.haml │ │ ├── _gap.html.haml │ │ ├── _last_page.html.haml │ │ ├── _next_page.html.haml │ │ ├── _page.html.haml │ │ ├── _paginator.html.haml │ │ └── _prev_page.html.haml │ ├── layouts │ │ ├── admin.html.haml │ │ ├── mailer.html.haml │ │ ├── mailer.text.haml │ │ └── session.html.haml │ ├── mailers │ │ └── user_mailer │ │ │ ├── change_password.ru.html.haml │ │ │ └── notify_menu_changed.ru.html.haml │ └── web │ │ ├── admin │ │ ├── daily_reports │ │ │ └── index.html.haml │ │ ├── dishes │ │ │ ├── _form.html.haml │ │ │ ├── edit.html.haml │ │ │ ├── index.html.haml │ │ │ └── new.html.haml │ │ ├── menus │ │ │ ├── _closest_days_menus.html.haml │ │ │ ├── _form.html.haml │ │ │ ├── _menu_dish_fields.html.haml │ │ │ ├── _validation_modal.html.haml │ │ │ └── edit.html.haml │ │ ├── monthly_reports │ │ │ └── index.html.haml │ │ └── users │ │ │ ├── _form.html.haml │ │ │ ├── edit.html.haml │ │ │ ├── index.html.haml │ │ │ └── new.html.haml │ │ ├── remind_passwords │ │ └── edit.html.haml │ │ ├── sessions │ │ └── new.html.haml │ │ ├── shared │ │ └── _nav.html.haml │ │ └── user │ │ └── passwords │ │ └── edit.html.haml └── workers │ ├── user_menu_freeze_worker.rb │ ├── user_menus_create_worker.rb │ └── user_menus_notify_worker.rb ├── bin ├── bundle ├── rails ├── rake ├── setup ├── spring └── update ├── clock.rb ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── database.yml ├── deploy.rb ├── deploy │ ├── production.rb │ └── vagrant.rb ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── application_controller_renderer.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── config.rb │ ├── cookies_serializer.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── new_framework_defaults.rb │ ├── observers.rb │ ├── session_store.rb │ ├── sidekiq.rb │ ├── simple_form.rb │ ├── simple_form_bootstrap.rb │ └── wrap_parameters.rb ├── locales │ ├── activerecord.en.yml │ ├── activerecord.ru.yml │ ├── en.yml │ ├── enumerize.en.yml │ ├── enumerize.ru.yml │ ├── ru.yml │ ├── simple_form.en.yml │ ├── simple_form.ru.yml │ └── views.ru.yml ├── puma.rb ├── routes.rb ├── secrets.yml ├── settings.yml ├── settings │ ├── development.yml │ ├── production.yml │ └── test.yml ├── sidekiq.yml └── spring.rb ├── db ├── migrate │ ├── 20170204092318_create_users.rb │ ├── 20170204092406_create_dishes.rb │ ├── 20170204094633_create_menus.rb │ ├── 20170204095020_create_menus_users.rb │ ├── 20170204095411_create_dishes_menus_users.rb │ ├── 20170204100217_create_dishes_menus.rb │ ├── 20170204111159_rename_tables.rb │ ├── 20170204142618_add_ready_default_to_menus.rb │ ├── 20170204142829_add_default_default_to_menu_dishes.rb │ ├── 20170211084848_add_neem_default_to_users.rb │ ├── 20170211085009_add_neem_default_to_user_menu_dishes.rb │ ├── 20170211102339_add_description_to_user_menus.rb │ ├── 20170211143820_remove_neem_from_user_menu_dishes.rb │ ├── 20170211145604_add_neem_to_user_menu.rb │ ├── 20170317115018_add_password_to_users.rb │ ├── 20170318184540_add_role_to_users.rb │ ├── 20170328131232_add_unique_index_to_user_menu_dishes.rb │ ├── 20170328151632_add_aasm_state_to_users.rb │ ├── 20170329202145_add_editable_to_user_menus.rb │ ├── 20170404071643_add_image_to_dishes.rb │ ├── 20170404135527_add_assm_state_to_menu.rb │ ├── 20170404215854_remove_ready_from_menu.rb │ ├── 20180215191335_create_votes.rb │ ├── 20180216060838_add_menu_dish_counters.rb │ ├── 20180219133606_add_dish_counters.rb │ └── 20180402120933_add_deleted_at_to_user.rb └── schema.rb ├── docker-compose.yml ├── frontend ├── .babelrc ├── .gitignore ├── README.md ├── build │ ├── build.js │ ├── check-versions.js │ ├── dev-client.js │ ├── dev-server.js │ ├── utils.js │ ├── vue-loader.conf.js │ ├── webpack.base.conf.js │ ├── webpack.dev.conf.js │ └── webpack.prod.conf.js ├── config │ ├── dev.env.js │ ├── index.js │ └── prod.env.js ├── index.html ├── src │ ├── App.vue │ ├── api │ │ ├── apiClient.js │ │ └── users.js │ ├── assets │ │ ├── images │ │ │ ├── logo.svg │ │ │ └── obeder.svg │ │ └── styles │ │ │ ├── variables.css │ │ │ └── voter-theme.css │ ├── components │ │ ├── Header.vue │ │ ├── ImageModal.vue │ │ ├── Menu.vue │ │ ├── Menu │ │ │ ├── DailyMenu.vue │ │ │ ├── MenuDish.vue │ │ │ └── MenuVoter.vue │ │ └── Switcher.vue │ ├── contants │ │ └── dishTypes.js │ ├── main.js │ ├── presenters │ │ └── MenuPresenter.js │ └── router │ │ └── index.js └── static │ └── .gitkeep ├── kube ├── db-deployment.yml ├── db-service.yml ├── db-volume-claim.yml ├── db-volume.yml ├── namespace.yml ├── redis-deployment.yml ├── redis-service.yml ├── scheduler-deployment.yml ├── sidekiq-deployment.yml ├── web-config.yml ├── web-deployment.yml ├── web-ingress.yml ├── web-service.yml ├── web-volume-claim.yml └── web-volume.yml ├── lib ├── assets │ └── .keep ├── flash_helper.rb ├── tasks │ ├── .keep │ └── users.rake └── templates │ └── haml │ └── scaffold │ └── _form.html.haml ├── log └── .keep ├── package.json ├── public ├── 404.html ├── 422.html ├── 500.html ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── assets │ └── .sprockets-manifest-e7137b478274418cb22c220cde5c97d7.json ├── favicon.ico └── robots.txt ├── secrets.sample.env ├── test ├── controllers │ ├── api │ │ ├── admin │ │ │ └── menus_controller_test.rb │ │ ├── dishes_controller_test.rb │ │ ├── user │ │ │ └── menus_controller_test.rb │ │ ├── user_menu_votes_controller_test.rb │ │ ├── user_menus_controller_test.rb │ │ └── users_controller_test.rb │ └── web │ │ ├── admin │ │ ├── dishes_controller_test.rb │ │ ├── menus_controller_test.rb │ │ ├── monthly_reports_controller_test.rb │ │ └── users_controller_test.rb │ │ ├── remind_passwords_controller_test.rb │ │ ├── sessions_controller_test.rb │ │ ├── user │ │ └── passwords_controller_test.rb │ │ └── welcome_controller_test.rb ├── factories │ ├── dishes.rb │ ├── files │ │ ├── .keep │ │ └── dish.jpg │ ├── menu_dishes.rb │ ├── menus.rb │ ├── sequences.rb │ ├── user_menu_dishes.rb │ ├── user_menus.rb │ └── users.rb ├── mailers │ ├── previews │ │ └── user_mailer_preview.rb │ └── user_mailer_test.rb ├── test_helper.rb └── workers │ ├── user_menu_freeze_worker_test.rb │ └── user_menus_create_worker_test.rb ├── tmp └── .keep ├── vendor └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ └── .keep ├── web.env └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // http://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | sourceType: 'module' 8 | }, 9 | env: { 10 | browser: true, 11 | }, 12 | extends: 'airbnb-base', 13 | // required to lint *.vue files 14 | plugins: [ 15 | 'html' 16 | ], 17 | // check if imports actually resolve 18 | 'settings': { 19 | 'import/resolver': { 20 | 'webpack': { 21 | 'config': 'frontend/build/webpack.base.conf.js' 22 | } 23 | } 24 | }, 25 | // add your custom rules here 26 | 'rules': { 27 | // don't require .vue extension when importing 28 | 'import/extensions': ['error', 'always', { 29 | 'js': 'never', 30 | 'vue': 'never' 31 | }], 32 | // allow debugger during development 33 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle 2 | /log/* 3 | /tmp/* 4 | !/log/.keep 5 | !/tmp/.keep 6 | .idea 7 | .byebug_history 8 | public/static/ 9 | public/uploads/ 10 | test/factories/files/uploads/ 11 | app/views/web/welcome/index.html 12 | npm-debug.log 13 | node_modules 14 | .vscode 15 | secrets.env 16 | 17 | config/settings.local.yml 18 | config/settings/*.local.yml 19 | config/environments/*.local.yml 20 | web-secrets.yml 21 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | before_script: 2 | - docker info 3 | build: 4 | script: 5 | - docker -v 6 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.4 3 | # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop 4 | # to ignore them, so only the ones explicitly set in this file are enabled. 5 | DisabledByDefault: true 6 | Exclude: 7 | - '**/vendor/**/*' 8 | - 'db/**' 9 | - 'bin/**' 10 | 11 | # Prefer &&/|| over and/or. 12 | Style/AndOr: 13 | Enabled: true 14 | 15 | # Do not use braces for hash literals when they are the last argument of a 16 | # method call. 17 | Style/BracesAroundHashParameters: 18 | Enabled: true 19 | 20 | # Align `when` with `case`. 21 | Style/CaseIndentation: 22 | Enabled: true 23 | 24 | # Align comments with method definitions. 25 | Style/CommentIndentation: 26 | Enabled: true 27 | 28 | # No extra empty lines. 29 | Style/EmptyLines: 30 | Enabled: true 31 | 32 | # In a regular class definition, no empty lines around the body. 33 | Style/EmptyLinesAroundClassBody: 34 | Enabled: true 35 | 36 | # In a regular method definition, no empty lines around the body. 37 | Style/EmptyLinesAroundMethodBody: 38 | Enabled: true 39 | 40 | # In a regular module definition, no empty lines around the body. 41 | Style/EmptyLinesAroundModuleBody: 42 | Enabled: true 43 | 44 | # Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }. 45 | Style/HashSyntax: 46 | Enabled: true 47 | 48 | # Method definitions after `private` or `protected` isolated calls need one 49 | # extra level of indentation. 50 | Style/IndentationConsistency: 51 | Enabled: true 52 | 53 | # Two spaces, no tabs (for indentation). 54 | Style/IndentationWidth: 55 | Enabled: true 56 | 57 | Style/SpaceAfterColon: 58 | Enabled: true 59 | 60 | Style/SpaceAfterComma: 61 | Enabled: true 62 | 63 | Style/SpaceAroundEqualsInParameterDefault: 64 | Enabled: true 65 | 66 | Style/SpaceAroundKeyword: 67 | Enabled: true 68 | 69 | Style/SpaceAroundOperators: 70 | Enabled: true 71 | 72 | Style/SpaceBeforeFirstArg: 73 | Enabled: true 74 | 75 | # Defining a method with parameters needs parentheses. 76 | Style/MethodDefParentheses: 77 | Enabled: true 78 | 79 | # Use `foo {}` not `foo{}`. 80 | Style/SpaceBeforeBlockBraces: 81 | Enabled: true 82 | 83 | # Use `foo { bar }` not `foo {bar}`. 84 | Style/SpaceInsideBlockBraces: 85 | Enabled: true 86 | 87 | # Use `{ a: 1 }` not `{a:1}`. 88 | Style/SpaceInsideHashLiteralBraces: 89 | Enabled: true 90 | 91 | Style/SpaceInsideParens: 92 | Enabled: true 93 | 94 | # Check quotes usage according to lint rule below. 95 | Style/StringLiterals: 96 | Enabled: true 97 | EnforcedStyle: single_quotes 98 | 99 | # Detect hard tabs, no hard tabs. 100 | Style/Tab: 101 | Enabled: true 102 | 103 | # Blank lines should not have any spaces. 104 | Style/TrailingBlankLines: 105 | Enabled: true 106 | 107 | # No trailing whitespace. 108 | Style/TrailingWhitespace: 109 | Enabled: true 110 | 111 | # Use quotes for string literals when they are enough. 112 | Style/UnneededPercentQ: 113 | Enabled: true 114 | 115 | # Align `end` with the matching keyword or starting expression except for 116 | # assignments, where it should be aligned with the LHS. 117 | Lint/EndAlignment: 118 | Enabled: true 119 | EnforcedStyleAlignWith: variable 120 | 121 | # Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg. 122 | Lint/RequireParentheses: 123 | Enabled: true 124 | 125 | Bundler/DuplicatedGem: 126 | Enabled: true 127 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | cache: 3 | directories: 4 | - node_modules 5 | - /home/travis/.rvm/gems 6 | env: 7 | global: 8 | - NOKOGIRI_USE_SYSTEM_LIBRARIES=true 9 | services: 10 | - redis-server 11 | before_install: 12 | - nvm install 7 13 | - gem install bundler 14 | - npm install -g yarn 15 | rvm: 16 | - 2.4.3 17 | install: 18 | - bundle 19 | - rake db:create db:migrate db:seed 20 | - yarn 21 | - yarn build 22 | script: 23 | - rake test 24 | -------------------------------------------------------------------------------- /Capfile: -------------------------------------------------------------------------------- 1 | require 'capistrano/setup' 2 | require 'capistrano/deploy' 3 | require 'capistrano/scm/git' 4 | require 'capistrano/bundler' 5 | require 'capistrano/rvm' 6 | require 'capistrano/rails/assets' 7 | require 'capistrano/rails/migrations' 8 | 9 | install_plugin Capistrano::SCM::Git 10 | 11 | Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r } 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.4 2 | 3 | RUN curl -sL https://deb.nodesource.com/setup_6.x | bash - 4 | 5 | RUN apt-get update -qq && apt-get install -y \ 6 | build-essential \ 7 | curl \ 8 | imagemagick \ 9 | libpq-dev \ 10 | nodejs \ 11 | postgresql \ 12 | redis-tools \ 13 | vim \ 14 | && apt-get clean \ 15 | && rm -rf /var/lib/apt/lists/* 16 | 17 | RUN npm install -g yarn 18 | 19 | RUN apt-get install locales 20 | RUN echo 'ru_RU.UTF-8 UTF-8' >> /etc/locale.gen 21 | RUN locale-gen ru_RU.UTF-8 22 | RUN dpkg-reconfigure -fnoninteractive locales 23 | ENV LC_ALL=ru_RU.utf8 24 | ENV LANGUAGE=ru_RU.utf8 25 | RUN update-locale LC_ALL="ru_RU.utf8" LANG="ru_RU.utf8" LANGUAGE="ru_RU" 26 | 27 | ENV EDITOR=vim 28 | 29 | ARG APP_DIR=/app 30 | 31 | RUN mkdir -p $APP_DIR 32 | WORKDIR $APP_DIR 33 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '~> 5.0.2' 4 | gem 'pg', '~> 0.18' 5 | gem 'puma', '~> 3.0' 6 | 7 | gem 'uglifier' 8 | gem 'autoprefixer-rails' 9 | gem 'therubyracer', platforms: :ruby 10 | gem 'turbolinks', '~> 5' 11 | gem 'haml-rails' 12 | gem 'jquery-rails' 13 | gem 'bootstrap-sass', '~> 3.3.6' 14 | gem 'twitter-bootstrap-rails' 15 | gem 'active_link_to' 16 | gem 'config' 17 | 18 | gem 'aasm' 19 | gem 'acts_as_paranoid' 20 | gem 'bcrypt' 21 | gem 'authority' 22 | gem 'ransack' 23 | gem 'kaminari' 24 | gem 'bootstrap-kaminari-views' 25 | gem 'active_model_serializers', '~> 0.10.0' 26 | gem 'enumerize' 27 | gem 'simple_form' 28 | gem 'cocoon' 29 | gem 'draper', '3.0.0.pre1' 30 | gem 'active_type' 31 | gem 'pundit' 32 | gem 'clockwork' 33 | gem 'sidekiq' 34 | gem 'redis' 35 | gem 'rails-observers', git: 'https://github.com/rails/rails-observers' 36 | gem 'carrierwave', '~> 1.0' 37 | gem 'mini_magick' 38 | gem 'rubyzip', '~> 1.1.0' 39 | gem 'axlsx', '2.1.0.pre' 40 | gem 'russian' 41 | 42 | group :development, :test do 43 | gem 'byebug', platform: :mri 44 | gem 'factory_girl_rails', require: false 45 | gem 'bullet' 46 | gem 'capistrano', '~> 3.8' 47 | gem 'capistrano-rails' 48 | gem 'capistrano-rvm' 49 | end 50 | 51 | group :development do 52 | gem 'listen', '~> 3.0.5' 53 | gem 'spring' 54 | gem 'spring-watcher-listen', '~> 2.0.0' 55 | gem 'rubocop', require: false 56 | end 57 | 58 | group :test do 59 | gem 'wrong' 60 | gem 'mocha' 61 | end 62 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: docker-setup 2 | docker-setup: 3 | cp -i secrets.sample.env secrets.env 4 | docker-compose build 5 | docker-compose run --rm web bash -c " \ 6 | set -ex; \ 7 | make deps; \ 8 | RAILS_ENV=test bundle exec rake db:drop db:create db:migrate db:seed; \ 9 | RAILS_ENV=development bundle exec rake db:create; \ 10 | make build-assets" 11 | 12 | .PHONY: deps 13 | deps: 14 | bundle install 15 | yarn install 16 | 17 | .PHONY: build-assets 18 | build-assets: 19 | yarn run build 20 | bundle exec rails assets:precompile 21 | 22 | .PHONY: docker-bash 23 | docker-bash: 24 | docker-compose run --rm web bash 25 | 26 | .PHONY: docker-run 27 | docker-run: 28 | docker-compose run --rm --service-ports web bash -c " \ 29 | bundle install; \ 30 | bundle exec sidekiq -d -L log/sidekiq.log; \ 31 | clockwork clock.rb & \ 32 | /bin/bash" 33 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: yarn run build && bundle exec rake db:migrate && bundle exec puma -C config/puma.rb 2 | console: bundle exec bin/rails console 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | [![Build Status](https://travis-ci.org/Restream/obeder.svg?branch=develop)](https://travis-ci.org/Restream/obeder) 4 | 5 | ## Setup 6 | 7 | * make docker-setup 8 | 9 | ## EZ deploy 10 | 11 | Download and install the Heroku CLI. (brew install heroku or download pkg) 12 | * heroku login 13 | * heroku git:remote -a obeder 14 | 15 | Deploy: 16 | * heroku buildpacks:add heroku/nodejs 17 | * heroku buildpacks:add heroku/ruby 18 | * heroku config:set NPM_CONFIG_PRODUCTION=false 19 | * heroku addons:create heroku-postgresql:hobby-dev 20 | * heroku addons:create sendgrid:starter 21 | * git push heroku master 22 | 23 | ## Frontend 24 | 25 | * npm run dev - watching changes and recompiling bundle (also runs development server on 8080 port) 26 | 27 | ## Sidekiq & Clockwork 28 | 29 | * docker-compose run --rm web sidekiq 30 | * docker-compose run --rm web clockwork clock.rb 31 | 32 | ## Закинуть базу в кубер 33 | 34 | - на проде (ssh deployer@obeder-1.staging.ul.restr.im) 35 | * pg_dump -xc -O obeder_production -U postgres | gzip > obeder.sql.gz 36 | - на своей тачке 37 | * scp deployer@obeder-1.staging.ul.restr.im:obeder.sql.gz obeder.sql.gz 38 | * kubectl cp obeder.sql.gz obeder/db-2830043074-s18jf:/obeder.sql.gz 39 | - в контейнере базы (kubectl exec db-2830043074-s18jf bash --namespace=obeder -it) 40 | * SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'obeder_production' AND pid <> pg_backend_pid(); 41 | * drop database obeder_production; 42 | * create database obeder_production; 43 | * zcat obeder.sql.gz | psql -U postgres obeder_production 44 | 45 | ## перетащить картинки в кубер 46 | rsync -rvP deployer@obeder-1.staging.ul.restr.im:apps/obeder/current/public/uploads/ public/uploads 47 | kubectl cp public/uploads obeder/web-1260677708-3mxwf:/app/public/uploads --container=obeder 48 | 49 | 50 | ## Задеплоить в кубер 51 | * docker-compose build 52 | * docker tag obeder_web docker-registry.restr.im:5000/obeder/web:1 53 | * docker push docker-registry.restr.im:5000/obeder/web:1 54 | * set image deployment/web obeder=docker-registry.restr.im:5000/obeder/web:1 55 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css 4 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Restream/obeder/f8fbf8d87842cfc8c5a23c8e4cf1d92584f31454/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. JavaScript code in this file should be added after the last require_* statement. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | //= require turbolinks 16 | //= require bootstrap-sprockets 17 | //= require cocoon 18 | //= require_tree . 19 | -------------------------------------------------------------------------------- /app/assets/javascripts/cable.js: -------------------------------------------------------------------------------- 1 | // Action Cable provides the framework to deal with WebSockets in Rails. 2 | // You can generate new channels where WebSocket features live using the rails generate channel command. 3 | // 4 | //= require action_cable 5 | //= require_self 6 | //= require_tree ./channels 7 | 8 | (function() { 9 | this.App || (this.App = {}); 10 | 11 | App.cable = ActionCable.createConsumer(); 12 | 13 | }).call(this); 14 | -------------------------------------------------------------------------------- /app/assets/javascripts/channels/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Restream/obeder/f8fbf8d87842cfc8c5a23c8e4cf1d92584f31454/app/assets/javascripts/channels/.keep -------------------------------------------------------------------------------- /app/assets/javascripts/web/admin/menus/edit.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var menuForm = $('#menu-form__form'); 3 | var publishBtn = $('#menu-publish__btn'); 4 | var validationModal = $('#menu-validation__modal'); 5 | var validationSubmitBtn = $('#menu-validation__submit-btn'); 6 | 7 | var validationUrl = validationModal.attr('validationUrl'); 8 | 9 | var validationMarkers = { 10 | inProgress: $('#menu-validation__marker__in-validation'), 11 | ok: $('#menu-validation__marker__ok'), 12 | failed: $('#menu-validation__marker__failed') 13 | }; 14 | 15 | var validationErrorsList = $('#menu-validation__errors-list'); 16 | 17 | menuForm.change(function(e) { 18 | publishBtn.prop('disabled', true); 19 | }); 20 | 21 | validationModal.on('show.bs.modal', function (e) { 22 | validationMarkers.inProgress.show(); 23 | validationMarkers.ok.hide(); 24 | validationMarkers.failed.hide(); 25 | }); 26 | 27 | validationModal.on('hide.bs.modal', function (e) { 28 | validationSubmitBtn.prop('disabled', true); 29 | validationErrorsList.empty(); 30 | }); 31 | 32 | menuForm 33 | .on('cocoon:after-insert', function(e) { 34 | publishBtn.prop('disabled', true); 35 | }) 36 | .on('cocoon:after-remove', function(e) { 37 | publishBtn.prop('disabled', true); 38 | }); 39 | 40 | publishBtn.click(function(e) { 41 | $.get(validationUrl, function(data) { 42 | validationMarkers.inProgress.hide(); 43 | 44 | if (!data.valid) { 45 | validationMarkers.failed.show(); 46 | var errors = $.map(data.errors, function(error) { 47 | return $('
  • ').text(error); 48 | }); 49 | validationErrorsList.html(errors); 50 | validationSubmitBtn.prop('disabled', true); 51 | } else { 52 | validationMarkers.ok.show(); 53 | validationSubmitBtn.prop('disabled', false); 54 | } 55 | }); 56 | }); 57 | })(); 58 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css.sass: -------------------------------------------------------------------------------- 1 | @import "bootstrap-sprockets" 2 | @import "bootstrap" 3 | -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/admin/menus_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::Admin::MenusController < Api::ApplicationController 2 | def validate 3 | menu = MenuPublishType.current_menu(params[:date]) 4 | if menu.valid? 5 | render json: { valid: true } 6 | else 7 | render json: { valid: false, errors: menu.errors[:dishes] } 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/controllers/api/application_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::ApplicationController < ActionController::API 2 | include AuthHelper 3 | end 4 | -------------------------------------------------------------------------------- /app/controllers/api/dishes_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::DishesController < Api::ApplicationController 2 | def index 3 | q = Dish.ransack(params[:q]) 4 | dishes = q.result.page(params[:page]) 5 | render json: dishes 6 | end 7 | 8 | def create 9 | dish = Dish.new(dish_params) 10 | if dish.save 11 | render json: dish 12 | else 13 | render json: { errors: dish.errors } 14 | end 15 | end 16 | 17 | def update 18 | dish = Dish.find(params[:id]) 19 | if dish.update(dish_params) 20 | render json: dish 21 | else 22 | render json: { errors: dish.errors } 23 | end 24 | end 25 | 26 | def destroy 27 | dish = Dish.find(params[:id]) 28 | if dish.delete 29 | render json: dish 30 | else 31 | render json: { errors: dish.errors } 32 | end 33 | end 34 | 35 | private 36 | 37 | def dish_params 38 | params.require(:dish).permit(:name, :description, :dish_type) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/controllers/api/user/application_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::User::ApplicationController < Api::ApplicationController 2 | end 3 | -------------------------------------------------------------------------------- /app/controllers/api/user/menus_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::User::MenusController < Api::User::ApplicationController 2 | def index 3 | user_menus = current_user.user_menus.includes(:menu).for_week 4 | render json: user_menus 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/controllers/api/user_menu_votes_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::UserMenuVotesController < Api::ApplicationController 2 | def update 3 | vote_type = UserMenuVoteType.new(user_menu_vote_params) 4 | 5 | if vote_type.valid? 6 | VotesService.assign_dish_vote(vote_type, current_user) 7 | render json: vote_type 8 | else 9 | render json: { errors: vote_type.errors } 10 | end 11 | end 12 | 13 | private 14 | 15 | def user_menu_vote_params 16 | params.require(:user_menu_vote).permit(:dish_id, :voted).merge(user_menu_id: params[:user_menu_id]) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/controllers/api/user_menus_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::UserMenusController < Api::ApplicationController 2 | def index 3 | date = params[:date] || Date.current 4 | user_menus = UserMenu.for_date(date) 5 | 6 | render json: user_menus 7 | end 8 | 9 | def update 10 | user_menu = UserMenuUpdateType.find(params[:id]) 11 | 12 | if user_menu.update(user_menu_params) 13 | user_menu.dishes.delete_all 14 | user_menu.dishes << Dish.where(id: dishes_params.map { |dish| dish[:id] }) 15 | 16 | render json: user_menu 17 | else 18 | render json: { errors: user_menu.errors } 19 | end 20 | end 21 | 22 | private 23 | 24 | def user_menu_params 25 | params.require(:user_menu).permit(:description, :neem) 26 | end 27 | 28 | def dishes_params 29 | params.require(:user_menu).permit(dishes: [:id])[:dishes] || [] 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/controllers/api/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::UsersController < Api::ApplicationController 2 | def show 3 | user = current_user 4 | render json: user 5 | end 6 | 7 | def update 8 | user = current_user 9 | 10 | if user.update(user_params) 11 | render json: user 12 | else 13 | render json: { errors: user.errors } 14 | end 15 | end 16 | 17 | def user_params 18 | params.require(:user).permit(:neem) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery with: :exception 3 | end 4 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Restream/obeder/f8fbf8d87842cfc8c5a23c8e4cf1d92584f31454/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/web/admin/application_controller.rb: -------------------------------------------------------------------------------- 1 | class Web::Admin::ApplicationController < Web::ApplicationController 2 | layout 'admin' 3 | 4 | after_action :verify_authorized 5 | end 6 | -------------------------------------------------------------------------------- /app/controllers/web/admin/daily_reports_controller.rb: -------------------------------------------------------------------------------- 1 | class Web::Admin::DailyReportsController < Web::Admin::ApplicationController 2 | before_action :authorize_cook 3 | 4 | def index 5 | @date = Date.parse(params[:date]) 6 | @neem_users = User.joins(user_menus: :menu) 7 | .where('users.neem = ? OR (user_menus.neem = ? AND menus.date = ?)', true, true, @date) 8 | .where.not(role: 'cook') 9 | .distinct 10 | @user_menus = UserMenu.with_users.with_dishes.for_date(@date) 11 | .where.not(user: @neem_users, users: { role: 'cook' }) 12 | .ordered_by_user_name 13 | 14 | @dishes_stats = @user_menus.map(&:dishes).flatten.group_by(&:name) 15 | .map { |key, value| { type: key, count: value.count } } 16 | 17 | menu = Menu.find_by(date: @date) 18 | @default_dishes = menu.dishes.where(menu_dishes: { default: true }).ordered_by_name if menu.present? 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/controllers/web/admin/dishes_controller.rb: -------------------------------------------------------------------------------- 1 | class Web::Admin::DishesController < Web::Admin::ApplicationController 2 | before_action :authorize_cook 3 | 4 | def index 5 | params[:q] ||= {} 6 | params[:q][:dish_type_eq] ||= Dish::DEFAULT_DISH_TYPE 7 | 8 | @q = Dish.order(:name).ransack(params[:q]) 9 | @dishes = @q.result.page(params[:page]) 10 | @selected_dish_type = params[:q][:dish_type_eq] 11 | end 12 | 13 | def new 14 | dish_type = params.dig(:q, :dish_type_eq) || Dish::DEFAULT_DISH_TYPE 15 | @dish = Dish.new(dish_type: dish_type) 16 | end 17 | 18 | def edit 19 | @dish = Dish.find(params[:id]) 20 | end 21 | 22 | def create 23 | @dish = Dish.new(dish_params) 24 | 25 | if @dish.save 26 | redirect_to helpers.current_dishes_path(@dish.dish_type) 27 | else 28 | render :new 29 | end 30 | end 31 | 32 | def update 33 | @dish = Dish.find(params[:id]) 34 | 35 | if @dish.update dish_params 36 | redirect_to helpers.current_dishes_path(@dish.dish_type) 37 | else 38 | render :edit 39 | end 40 | end 41 | 42 | def destroy 43 | @dish = Dish.find(params[:id]) 44 | @dish.destroy 45 | 46 | redirect_to helpers.current_dishes_path(@dish.dish_type) 47 | end 48 | 49 | private 50 | 51 | def dish_params 52 | params.require(:dish).permit(:name, :description, :dish_type, :image, :image_cache) 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /app/controllers/web/admin/menus_controller.rb: -------------------------------------------------------------------------------- 1 | class Web::Admin::MenusController < Web::Admin::ApplicationController 2 | before_action :authorize_cook 3 | 4 | def edit 5 | @menu = current_menu 6 | end 7 | 8 | def update 9 | @menu = current_menu 10 | 11 | if @menu.update(menu_params) 12 | f(:success) 13 | redirect_to edit_admin_menu_path(@menu.date) 14 | else 15 | f(:error) 16 | render action: :edit 17 | end 18 | end 19 | 20 | def approve 21 | @menu = current_menu 22 | @menu.approve! 23 | 24 | f(:success) 25 | redirect_to edit_admin_menu_path(@menu.date) 26 | end 27 | 28 | private 29 | 30 | def menu_params 31 | params.require(:menu).permit(menu_dishes_attributes: [:id, :default, :dish_id, :_destroy]) 32 | end 33 | 34 | def current_menu 35 | Menu.current_menu(params[:date]) 36 | end 37 | 38 | def date 39 | Date.parse(params[:date]) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/controllers/web/admin/monthly_reports_controller.rb: -------------------------------------------------------------------------------- 1 | class Web::Admin::MonthlyReportsController < Web::Admin::ApplicationController 2 | before_action :authorize_cook 3 | 4 | def index 5 | @date = date 6 | @users = User.without_role('cook').order(:name) 7 | @menus = Menu.in_date_range(@date.beginning_of_month, @date.end_of_month).order(:date) 8 | @user_menus = UserMenu.joins(:menu).where(menu: @menus, neem: false) 9 | .pluck(:'menus.date', :'user_id') 10 | end 11 | 12 | def export 13 | send_data(MonthlyReportService.export(date), 14 | filename: MonthlyReportService.filename(date), 15 | type: MonthlyReportService.type) 16 | end 17 | 18 | private 19 | 20 | def date 21 | Date.parse(params[:date]) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/controllers/web/admin/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Web::Admin::UsersController < Web::Admin::ApplicationController 2 | before_action :authorize_admin 3 | 4 | def index 5 | @q = User.order(:name).ransack(params[:q]) 6 | @users = @q.result.page(params[:page]) 7 | end 8 | 9 | def new 10 | @user = User.new 11 | end 12 | 13 | def edit 14 | @user = User.find(params[:id]) 15 | end 16 | 17 | def create 18 | @user = User.new(user_params) 19 | 20 | if @user.save 21 | DishesService.set_default_dishes_for_user(@user) 22 | UserMailer.change_password(@user).deliver 23 | redirect_to admin_users_path 24 | else 25 | render :new 26 | end 27 | end 28 | 29 | def update 30 | @user = User.find(params[:id]) 31 | 32 | if @user.update user_params 33 | redirect_to admin_users_path 34 | else 35 | render :edit 36 | end 37 | end 38 | 39 | def destroy 40 | @user = User.find(params[:id]) 41 | @user.destroy 42 | 43 | redirect_to admin_users_path 44 | end 45 | 46 | private 47 | 48 | def user_params 49 | params.require(:user).permit(:name, :email, :neem, :description, :role) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /app/controllers/web/admin/welcome_controller.rb: -------------------------------------------------------------------------------- 1 | class Web::Admin::WelcomeController < Web::Admin::ApplicationController 2 | before_action :authorize_cook 3 | 4 | def index 5 | redirect_to edit_admin_menu_path(Date.current) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/controllers/web/application_controller.rb: -------------------------------------------------------------------------------- 1 | class Web::ApplicationController < ApplicationController 2 | include AuthHelper 3 | include FlashHelper 4 | include Pundit 5 | protect_from_forgery 6 | 7 | rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized 8 | 9 | private 10 | 11 | def user_not_authorized(exception) 12 | flash[:error] = t("pundit.#{exception.policy}.#{exception.query}") 13 | redirect_to(request.referrer || login_path) 14 | end 15 | 16 | def authorize_admin 17 | authorize :admin 18 | end 19 | 20 | def authorize_cook 21 | authorize :cook 22 | end 23 | 24 | def authorize_user 25 | authorize :user 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/controllers/web/remind_passwords_controller.rb: -------------------------------------------------------------------------------- 1 | class Web::RemindPasswordsController < Web::ApplicationController 2 | layout 'session' 3 | 4 | def edit 5 | @user_type = UserRemindPasswordType.new 6 | end 7 | 8 | def update 9 | @user_type = UserRemindPasswordType.new(user_remind_password_type_params) 10 | user = @user_type.user 11 | 12 | if @user_type.valid? && user.present? 13 | user.deactivate! 14 | UserMailer.change_password(user).deliver 15 | flash[:success] = t('password_has_been_reset') 16 | redirect_to login_path 17 | else 18 | flash.now[:error] = t('user_not_found') if (user.blank? && @user_type.valid?) 19 | render :edit 20 | end 21 | end 22 | 23 | private 24 | 25 | def user_remind_password_type_params 26 | params.require(:user_remind_password_type).permit(:email) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/controllers/web/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class Web::SessionsController < Web::ApplicationController 2 | layout 'session' 3 | 4 | def new 5 | @session = UserSignInType.new 6 | end 7 | 8 | def create 9 | @session = UserSignInType.new(user_sign_in_type_params) 10 | user = @session.user 11 | 12 | if user.blank? 13 | flash[:error] = t('user_not_found') 14 | redirect_to login_path 15 | elsif user.active? && @session.valid? 16 | sign_in(user) 17 | redirect_to (user.role.admin? || user.role.cook?) ? admin_root_path : root_path 18 | else 19 | flash.now[:error] = t('password_not_set') if user.inactive? 20 | render :new 21 | end 22 | end 23 | 24 | def destroy 25 | sign_out 26 | redirect_to login_path 27 | end 28 | 29 | private 30 | 31 | def user_sign_in_type_params 32 | params.require(:user_sign_in_type).permit(:email, :password) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/controllers/web/user/application_controller.rb: -------------------------------------------------------------------------------- 1 | class Web::User::ApplicationController < Web::ApplicationController 2 | layout 'session' 3 | end 4 | -------------------------------------------------------------------------------- /app/controllers/web/user/passwords_controller.rb: -------------------------------------------------------------------------------- 1 | class Web::User::PasswordsController < Web::User::ApplicationController 2 | def edit 3 | @user = UserPasswordEditType.new 4 | end 5 | 6 | def update 7 | @user = UserPasswordEditType.find_by_id(params[:user_id]) 8 | 9 | if @user.blank? 10 | flash[:error] = t('user_not_found') 11 | redirect_to edit_user_password_path 12 | elsif 13 | @user.active? && (current_user.try(:id) != @user.id) 14 | flash[:error] = t('authenticate_to_change_password') 15 | redirect_to login_path 16 | elsif @user.update(password_params) 17 | @user.activate! 18 | flash[:success] = t('password_changed') 19 | redirect_to login_path 20 | else 21 | render :edit 22 | end 23 | end 24 | 25 | private 26 | 27 | def password_params 28 | params.require(:user_password_edit_type).permit(:password, :password_confirmation) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/controllers/web/welcome_controller.rb: -------------------------------------------------------------------------------- 1 | class Web::WelcomeController < Web::ApplicationController 2 | before_action :authorize_user 3 | after_action :verify_authorized 4 | 5 | def index 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/decorators/dish_decorator.rb: -------------------------------------------------------------------------------- 1 | class DishDecorator < Draper::Decorator 2 | delegate_all 3 | 4 | def name_with_type 5 | [model.name, model.dish_type.text].join(' | ') 6 | end 7 | 8 | def vote_ups_percent 9 | vote_percent vote_ups_count 10 | end 11 | 12 | def vote_downs_percent 13 | vote_percent vote_downs_count 14 | end 15 | 16 | def vote_percent(vote_count) 17 | total = vote_ups_count + vote_downs_count 18 | return 0 if total.zero? 19 | 20 | (100.0 * vote_count) / total 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/decorators/dishes_decorator.rb: -------------------------------------------------------------------------------- 1 | class DishesDecorator < Draper::CollectionDecorator 2 | def names 3 | object.map(&:name).join(', ') 4 | end 5 | 6 | def names_or_default(default_dishes, default_text) 7 | default_dishes == object ? default_text : names 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/decorators/user_decorator.rb: -------------------------------------------------------------------------------- 1 | class UserDecorator < Draper::Decorator 2 | delegate_all 3 | 4 | def first_name 5 | object.name.split.last 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/decorators/users_decorator.rb: -------------------------------------------------------------------------------- 1 | class UsersDecorator < Draper::CollectionDecorator 2 | def names 3 | object.map(&:name).join(', ') 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/auth_helper.rb: -------------------------------------------------------------------------------- 1 | module AuthHelper 2 | def sign_in(user) 3 | session[:user_id] = user.id 4 | end 5 | 6 | def sign_out 7 | session[:user_id] = nil 8 | end 9 | 10 | def signed_in? 11 | current_user.present? 12 | end 13 | 14 | def authenticate_user! 15 | redirect_to new_session_path unless signed_in? 16 | end 17 | 18 | def current_user 19 | @current_user ||= User.find_by(id: session[:user_id]) 20 | end 21 | 22 | def current_user?(user) 23 | current_user == user 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/helpers/web/admin/dishes_helper.rb: -------------------------------------------------------------------------------- 1 | module Web::Admin::DishesHelper 2 | def current_dishes_path(dish_type) 3 | admin_dishes_path(q: { dish_type_eq: dish_type }) 4 | end 5 | 6 | def new_dish_path(dish_type) 7 | new_admin_dish_path(q: { dish_type_eq: dish_type }) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/helpers/web/admin/menus_helper.rb: -------------------------------------------------------------------------------- 1 | module Web::Admin::MenusHelper 2 | def col_index(menus) 3 | bootstrap_max_col_index = 12 4 | number_of_columns = menus.empty? ? 1 : menus.size 5 | bootstrap_max_col_index / number_of_columns 6 | end 7 | 8 | def dish_name(menu_dish) 9 | text = "#{ menu_dish.dish.name } (#{ menu_dish.dish.dish_type.text })" 10 | menu_dish.default? ? content_tag(:strong, text) : content_tag(:div, text) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/helpers/web/admin/monthly_reports_helper.rb: -------------------------------------------------------------------------------- 1 | module Web::Admin::MonthlyReportsHelper 2 | def report_header(date) 3 | I18n.l(date, format: :monthly_report_date) 4 | end 5 | 6 | def next_report_params(date) 7 | date.next_month 8 | end 9 | 10 | def previous_report_params(date) 11 | date.prev_month 12 | end 13 | 14 | def dates_in_month(date) 15 | (date.beginning_of_month..date.end_of_month).to_a 16 | end 17 | 18 | def cell_color_class(date) 19 | date.saturday? || date.sunday? ? 'danger' : '' 20 | end 21 | 22 | def user_ate?(user_id, date, user_menus_info) 23 | user_menus_info.include?([date, user_id]) 24 | end 25 | 26 | def report_row(user, user_index, dates, user_menus) 27 | total_by_user = 0 28 | 29 | name_cell = content_tag(:td) do 30 | [user_index + 1, user.name].join('. ') 31 | end 32 | 33 | dates_collection = dates.collect do |date| 34 | if user_ate?(user.id, date, user_menus) 35 | total_by_user += 1 36 | content_tag :td, 1, class: cell_color_class(date) 37 | else 38 | content_tag :td, '', class: cell_color_class(date) 39 | end 40 | end 41 | 42 | dates_cells = dates_collection.join().html_safe 43 | 44 | total_cell = content_tag :td, total_by_user 45 | content_tag :tr, name_cell.concat(dates_cells).concat(total_cell), class: 'text-center' 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default_url_options[:host] = "http://#{ Settings.domain }" 3 | default from: 'noreply@restream.rt.ru' 4 | layout 'mailer' 5 | 6 | def self.inherited(subclass) 7 | subclass.default template_path: "mailers/#{subclass.name.to_s.underscore}" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/mailers/user_mailer.rb: -------------------------------------------------------------------------------- 1 | class UserMailer < ApplicationMailer 2 | add_template_helper Web::Admin::MenusHelper 3 | 4 | def change_password(user) 5 | @user = user.decorate 6 | mail to: user.email 7 | end 8 | 9 | def notify_menu_changed(user, menu) 10 | @user = user.decorate 11 | @menu = menu 12 | mail( 13 | to: user.email, 14 | subject: I18n.t('user_mailer.notify_menu_changed.subject', date: I18n.l(menu.date)) 15 | ) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Restream/obeder/f8fbf8d87842cfc8c5a23c8e4cf1d92584f31454/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/concerns/voteable_concern.rb: -------------------------------------------------------------------------------- 1 | module VoteableConcern 2 | extend ActiveSupport::Concern 3 | included do |base| 4 | has_many :votes, -> { where voteable_type: base.name }, foreign_key: :voteable_id 5 | has_many :vote_ups, -> { where voteable_type: base.name }, foreign_key: :voteable_id 6 | has_many :vote_downs, -> { where voteable_type: base.name }, foreign_key: :voteable_id 7 | end 8 | 9 | def voted_by(user) 10 | change_vote(VoteUp, VoteDown, user) 11 | end 12 | 13 | def unvoted_by(user) 14 | change_vote(VoteDown, VoteUp, user) 15 | end 16 | 17 | def rating 18 | vote_ups_count - vote_downs_count 19 | end 20 | 21 | private 22 | 23 | def change_vote(be_created_vote, be_destroyed_vote, user) 24 | be_destroyed_vote.where(voteable: self, user: user).destroy_all 25 | be_created_vote.find_or_create_by!(voteable: self, user: user) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/models/dish.rb: -------------------------------------------------------------------------------- 1 | class Dish < ApplicationRecord 2 | DEFAULT_DISH_TYPE = 'soup'.freeze 3 | 4 | extend Enumerize 5 | include DishRepository 6 | 7 | has_many :menu_dishes 8 | has_many :menus, through: :menu_dishes 9 | has_many :user_menu_dishes 10 | has_many :user_menus, through: :user_menu_dishes 11 | 12 | validates :name, :dish_type, presence: true 13 | 14 | mount_uploader :image, DishImageUploader 15 | 16 | enumerize :dish_type, in: [:soup, :main_dish, :side_dish, :salad, :separate_dish] 17 | end 18 | -------------------------------------------------------------------------------- /app/models/menu.rb: -------------------------------------------------------------------------------- 1 | class Menu < ApplicationRecord 2 | include MenuRepository 3 | include AASM 4 | 5 | has_many :menu_dishes 6 | has_many :dishes, through: :menu_dishes 7 | has_many :user_menus 8 | has_many :users, through: :user_menus 9 | 10 | accepts_nested_attributes_for :menu_dishes, reject_if: :all_blank, allow_destroy: true 11 | 12 | aasm do 13 | state :editable, initial: true 14 | state :approved 15 | state :published 16 | 17 | event :approve do 18 | transitions from: :editable, to: :approved 19 | end 20 | 21 | event :publish do 22 | transitions from: :approved, to: :published 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/models/menu_dish.rb: -------------------------------------------------------------------------------- 1 | class MenuDish < ApplicationRecord 2 | include MenuDishRepository 3 | include VoteableConcern 4 | 5 | belongs_to :dish 6 | belongs_to :menu 7 | 8 | validates :dish, presence: true 9 | validates :menu, presence: true 10 | end 11 | -------------------------------------------------------------------------------- /app/models/menu_observer.rb: -------------------------------------------------------------------------------- 1 | class MenuObserver < ActiveRecord::Observer 2 | def after_commit(menu) 3 | MenusService.create_user_menus(menu) if menu.approved? 4 | MenusService.notify_menu_published(menu) if menu.published? 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ApplicationRecord 2 | extend Enumerize 3 | include UserRepository 4 | include AASM 5 | 6 | has_many :user_menus, dependent: :destroy 7 | has_many :menus, through: :user_menus 8 | 9 | acts_as_paranoid 10 | 11 | validates :name, :role, :email, presence: true 12 | validates :email, uniqueness: true 13 | validates :email, email: true, if: 'role.cook?' 14 | validates :email, restream_email: true, unless: 'role.cook?' 15 | validates_confirmation_of :password 16 | 17 | has_secure_password validations: false 18 | 19 | enumerize :role, in: [:admin, :cook, :user], default: :user 20 | 21 | aasm do 22 | state :inactive, initial: true 23 | state :active 24 | 25 | event :activate do 26 | transitions from: [:active, :inactive], to: :active 27 | end 28 | 29 | event :deactivate do 30 | transitions from: [:active, :inactive], to: :inactive 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/models/user_menu.rb: -------------------------------------------------------------------------------- 1 | class UserMenu < ApplicationRecord 2 | include UserMenuRepository 3 | 4 | belongs_to :menu 5 | belongs_to :user 6 | 7 | has_many :user_menu_dishes, dependent: :destroy 8 | has_many :dishes, through: :user_menu_dishes 9 | end 10 | -------------------------------------------------------------------------------- /app/models/user_menu_dish.rb: -------------------------------------------------------------------------------- 1 | class UserMenuDish < ApplicationRecord 2 | belongs_to :dish 3 | belongs_to :user_menu 4 | end 5 | -------------------------------------------------------------------------------- /app/models/vote.rb: -------------------------------------------------------------------------------- 1 | class Vote < ActiveRecord::Base 2 | belongs_to :user 3 | 4 | before_create :default_values 5 | validates_uniqueness_of :voteable_id, scope: %w[voteable_type user_id] 6 | 7 | def default_values; end 8 | end 9 | -------------------------------------------------------------------------------- /app/models/vote_down.rb: -------------------------------------------------------------------------------- 1 | class VoteDown < Vote 2 | belongs_to :voteable, polymorphic: true, counter_cache: true 3 | 4 | default_scope { where(vote: false) } 5 | 6 | def default_values 7 | self.vote = false 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/models/vote_up.rb: -------------------------------------------------------------------------------- 1 | class VoteUp < Vote 2 | belongs_to :voteable, polymorphic: true, counter_cache: true 3 | 4 | default_scope { where(vote: true) } 5 | 6 | def default_values 7 | self.vote = true 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/policies/admin_policy.rb: -------------------------------------------------------------------------------- 1 | class AdminPolicy < ApplicationPolicy 2 | attr_reader :user, :record 3 | 4 | def initialize(user, record) 5 | raise Pundit::NotAuthorizedError.new(policy: 'admin') if (user.blank? || !user.role.admin?) 6 | end 7 | 8 | def method_missing(name, *args) 9 | true 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/policies/application_policy.rb: -------------------------------------------------------------------------------- 1 | class ApplicationPolicy 2 | attr_reader :user, :record 3 | end 4 | -------------------------------------------------------------------------------- /app/policies/cook_policy.rb: -------------------------------------------------------------------------------- 1 | class CookPolicy < ApplicationPolicy 2 | attr_reader :user, :record 3 | 4 | def initialize(user, record) 5 | raise Pundit::NotAuthorizedError.new(policy: 'cook') if (user.blank? || user.role.user?) 6 | end 7 | 8 | def method_missing(name, *args) 9 | true 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/policies/user_policy.rb: -------------------------------------------------------------------------------- 1 | class UserPolicy < ApplicationPolicy 2 | attr_reader :user, :record 3 | 4 | def initialize(user, record) 5 | raise Pundit::NotAuthorizedError.new(policy: 'user') if user.blank? 6 | end 7 | 8 | def method_missing(name, *args) 9 | true 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/repositories/dish_repository.rb: -------------------------------------------------------------------------------- 1 | module DishRepository 2 | extend ActiveSupport::Concern 3 | included do 4 | scope :ordered_by_name, -> { order(:name) } 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/repositories/menu_dish_repository.rb: -------------------------------------------------------------------------------- 1 | module MenuDishRepository 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | scope :default, -> { where(default: true) } 6 | scope :with_dish, -> { includes(:dish) } 7 | scope :ordered_by_dish, -> { order('dishes.dish_type, dishes.id') } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/repositories/menu_repository.rb: -------------------------------------------------------------------------------- 1 | module MenuRepository 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | scope :for_date, -> (date) { where(date: date) } 6 | scope :current_menu, -> (date) { find_or_create_by(date: date) } 7 | scope :for_date_range, -> (date, offset) { where(date: (date - offset.days)..(date + offset.days)).order(:date) } 8 | scope :except_date, -> (date) { where.not(date: date) } 9 | scope :closest_days_menus, -> (date) do 10 | includes(menu_dishes: :dish) 11 | .for_date_range(date, Settings.closest_menus_date_offset) 12 | .except_date(date) 13 | end 14 | scope :in_date_range, -> (start_date, end_date) { where(date: (start_date..end_date)) } 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/repositories/user_menu_repository.rb: -------------------------------------------------------------------------------- 1 | module UserMenuRepository 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | scope :em, -> { where(user: User.where(neem: false)) } 6 | scope :for_date, -> (date) { where(menu: Menu.where(date: date)) } 7 | scope :for_week, -> { where(menu: Menu.published.where('date >= ?', Date.current)) } 8 | scope :ordered_by_user_name, -> { order('users.name') } 9 | scope :with_users, -> { includes(:user) } 10 | scope :with_dishes, -> { includes(:dishes) } 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/repositories/user_repository.rb: -------------------------------------------------------------------------------- 1 | module UserRepository 2 | extend ActiveSupport::Concern 3 | included do 4 | scope :without_role, -> (role) { where.not(role: role) } 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/serializers/dish_serializer.rb: -------------------------------------------------------------------------------- 1 | class DishSerializer < ActiveModel::Serializer 2 | attributes :id, :name, :description, :dish_type, :image 3 | end 4 | -------------------------------------------------------------------------------- /app/serializers/menu_dish_serializer.rb: -------------------------------------------------------------------------------- 1 | class MenuDishSerializer < ActiveModel::Serializer 2 | attributes :id, :dish_id, :default, :name, :description, :rating 3 | 4 | def name 5 | object.dish.name 6 | end 7 | 8 | def description 9 | object.dish.description 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/serializers/menu_serializer.rb: -------------------------------------------------------------------------------- 1 | class MenuSerializer < ActiveModel::Serializer 2 | attributes :id, :date 3 | has_many :menu_dishes 4 | end 5 | -------------------------------------------------------------------------------- /app/serializers/user_menu_serializer.rb: -------------------------------------------------------------------------------- 1 | class UserMenuSerializer < ActiveModel::Serializer 2 | attributes :id, :date, :user_name, :description, :dishes, :neem, :editable 3 | 4 | def user_name 5 | object.user.name 6 | end 7 | 8 | def date 9 | object.menu.date 10 | end 11 | 12 | def dishes 13 | menu_dishes = object.menu.menu_dishes.includes(:dish, :votes).ordered_by_dish 14 | user_dish_ids = object.dishes.pluck(:id) 15 | 16 | menu_dishes.map do |menu_dish| 17 | dish = menu_dish.dish 18 | 19 | placed_users_votes = menu_dish.votes.index_by(&:user_id) 20 | current_user_vote = placed_users_votes[current_user.id].presence 21 | 22 | dish_hash = DishSerializer.new(dish).attributes 23 | dish_hash[:selected] = user_dish_ids.include?(dish.id) 24 | dish_hash[:default] = menu_dish.default 25 | dish_hash[:rating_up] = menu_dish.dish.vote_ups_count 26 | dish_hash[:rating_down] = menu_dish.dish.vote_downs_count 27 | dish_hash[:voted] = current_user_vote&.vote 28 | dish_hash 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/serializers/user_menu_vote_type_serializer.rb: -------------------------------------------------------------------------------- 1 | class UserMenuVoteTypeSerializer < ActiveModel::Serializer 2 | attributes :user_menu_id, :dish_id, :rating_up, :rating_down 3 | 4 | delegate :user_menu_id, :dish_id, to: :object 5 | 6 | def rating_up 7 | object.voteable.dish.vote_ups_count 8 | end 9 | 10 | def rating_down 11 | object.voteable.dish.vote_downs_count 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/serializers/user_serializer.rb: -------------------------------------------------------------------------------- 1 | class UserSerializer < ActiveModel::Serializer 2 | attributes :name, :email, :neem, :description 3 | end 4 | -------------------------------------------------------------------------------- /app/services/dishes_service.rb: -------------------------------------------------------------------------------- 1 | class DishesService 2 | class << self 3 | def set_default_dishes_for_user(user) 4 | menus = Menu.where('date >= ?', Date.current).published 5 | menus.each do |menu| 6 | user_menu = UserMenu.create(user: user, menu: menu, neem: user.neem) 7 | menu_dishes = menu.menu_dishes.with_dish.default 8 | dishes = menu_dishes.map(&:dish) 9 | user_menu.dishes << dishes 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/services/menus_service.rb: -------------------------------------------------------------------------------- 1 | class MenusService 2 | class << self 3 | def create_user_menus(menu) 4 | UserMenusCreateWorker.perform_async(menu.id) 5 | end 6 | 7 | def notify_menu_published(menu) 8 | UserMenusNotifyWorker.perform_async(menu.id) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/services/votes_service.rb: -------------------------------------------------------------------------------- 1 | class VotesService 2 | class << self 3 | def assign_dish_vote(vote_type, voter) 4 | voteable = vote_type.voteable 5 | prev_counters = voteable.slice(:vote_ups_count, :vote_downs_count) 6 | 7 | vote_type.voted ? voteable.voted_by(voter) : voteable.unvoted_by(voter) 8 | voteable.reload 9 | 10 | update_total_dish_counters(prev_counters, voteable) 11 | end 12 | 13 | private 14 | def update_total_dish_counters(prev_counters, voteable) 15 | actual_counters = voteable.slice(:vote_ups_count, :vote_downs_count) 16 | 17 | total_votes = voteable.dish.slice(:vote_ups_count, :vote_downs_count) 18 | total_votes[:vote_ups_count] += actual_counters[:vote_ups_count] - prev_counters[:vote_ups_count] 19 | total_votes[:vote_downs_count] += actual_counters[:vote_downs_count] - prev_counters[:vote_downs_count] 20 | 21 | voteable.dish.update(total_votes) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/types/menu_publish_type.rb: -------------------------------------------------------------------------------- 1 | class MenuPublishType < Menu 2 | validates_with MenuPublishReadinessValidator 3 | end 4 | -------------------------------------------------------------------------------- /app/types/user_menu_update_type.rb: -------------------------------------------------------------------------------- 1 | class UserMenuUpdateType < UserMenu 2 | validates :editable, inclusion: { in: [true] } 3 | end 4 | -------------------------------------------------------------------------------- /app/types/user_menu_vote_type.rb: -------------------------------------------------------------------------------- 1 | class UserMenuVoteType < ActiveType::Object 2 | attribute :voted, :boolean 3 | attribute :dish_id, :integer 4 | attribute :user_menu_id, :integer 5 | 6 | validates :voted, inclusion: { in: [true, false] } 7 | validates :dish_id, presence: true 8 | validates :user_menu_id, presence: true 9 | validate :check_user_menu_exists 10 | 11 | def check_user_menu_exists 12 | @user_menu ||= UserMenu.find_by(id: user_menu_id) 13 | errors.add(:user_menu_id, :invalid) if @user_menu.nil? 14 | end 15 | 16 | def voteable 17 | @voteable ||= MenuDish.find_by(menu_id: @user_menu.menu_id, dish_id: dish_id) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/types/user_password_edit_type.rb: -------------------------------------------------------------------------------- 1 | class UserPasswordEditType < User 2 | attribute :password, :string 3 | attribute :password_confirmation, :string 4 | 5 | validates :password, presence: true, confirmation: true 6 | validates :password_confirmation, presence: true 7 | end 8 | -------------------------------------------------------------------------------- /app/types/user_remind_password_type.rb: -------------------------------------------------------------------------------- 1 | class UserRemindPasswordType < ActiveType::Object 2 | attribute :email, :string 3 | validates :email, presence: true, email: true 4 | 5 | def user 6 | User.find_by(email: email.mb_chars.downcase) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/types/user_sign_in_type.rb: -------------------------------------------------------------------------------- 1 | class UserSignInType < ActiveType::Object 2 | attribute :email, :string 3 | attribute :password, :string 4 | 5 | validates :email, :password, presence: true 6 | validate :check_authenticate, if: :email 7 | 8 | def user 9 | User.find_by(email: email.mb_chars.downcase) 10 | end 11 | 12 | private 13 | 14 | def check_authenticate 15 | unless user.try(:authenticate, password) 16 | errors.add(:password, :user_or_password_invalid) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/uploaders/dish_image_uploader.rb: -------------------------------------------------------------------------------- 1 | class DishImageUploader < CarrierWave::Uploader::Base 2 | include CarrierWave::MiniMagick 3 | 4 | storage :file 5 | process resize_to_fit: [550, 550] 6 | 7 | version :thumb do 8 | process resize_to_fit: [40, 40] 9 | end 10 | 11 | def store_dir 12 | "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" 13 | end 14 | 15 | def extension_whitelist 16 | %w(jpg jpeg gif png) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/validators/email_validator.rb: -------------------------------------------------------------------------------- 1 | class EmailValidator < ActiveModel::EachValidator 2 | def validate_each(record, attribute, value) 3 | unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i 4 | record.errors.add(attribute, :invalid_email) 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/validators/menu_publish_readiness_validator.rb: -------------------------------------------------------------------------------- 1 | class MenuPublishReadinessValidator < ActiveModel::Validator 2 | def validate(record) 3 | errors = record.errors 4 | dishes = record.dishes 5 | 6 | errors.add(:dishes, :less_than_two_soups) if less_than?(dishes, :soup, 2) 7 | errors.add(:dishes, :less_than_two_salads) if less_than?(dishes, :salad, 2) 8 | errors.add(:dishes, :less_than_two_main_dishes) if main_and_separate_dishes_count(dishes) < 2 9 | errors.add(:dishes, :incorrect_defaults) unless correct_defaults?(record) 10 | errors.add(:dishes, :main_to_side_dishes_mismatch) unless quantity_matches?(dishes, :main_dish, :side_dish) 11 | end 12 | 13 | private 14 | 15 | def less_than?(dishes, dish_type, required_number) 16 | dishes.where(dish_type: dish_type).size < required_number 17 | end 18 | 19 | def single_default?(record, dish_type) 20 | record.dishes.where(dish_type: dish_type).one? do |dish| 21 | dish.menu_dishes.where(menu: record).exists?(default: true) 22 | end 23 | end 24 | 25 | def quantity_matches?(dishes, dish_one, dish_two) 26 | dishes.where(dish_type: dish_one).size == dishes.where(dish_type: dish_two).size 27 | end 28 | 29 | def main_and_separate_dishes_count(dishes) 30 | dishes.where(dish_type: :main_dish) 31 | .or(dishes.where(dish_type: :separate_dish)) 32 | .size 33 | end 34 | 35 | def correct_defaults?(record) 36 | single_default?(record, :soup) && 37 | single_default?(record, :salad) && 38 | (single_default?(record, :main_dish) && single_default?(record, :side_dish) || single_default?(record, :separate_dish)) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/validators/restream_email_validator.rb: -------------------------------------------------------------------------------- 1 | class RestreamEmailValidator < ActiveModel::EachValidator 2 | def validate_each(record, attribute, value) 3 | unless value =~ /\A([^@\s]+)@(restream.)?rt.ru\z/i 4 | record.errors.add(attribute, :invalid_restream_email) 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/views/kaminari/_first_page.html.haml: -------------------------------------------------------------------------------- 1 | %li 2 | = link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, :remote => remote 3 | -------------------------------------------------------------------------------- /app/views/kaminari/_gap.html.haml: -------------------------------------------------------------------------------- 1 | %li.disabled 2 | = content_tag :a, raw(t 'views.pagination.truncate') 3 | -------------------------------------------------------------------------------- /app/views/kaminari/_last_page.html.haml: -------------------------------------------------------------------------------- 1 | %li 2 | = link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {:remote => remote} 3 | -------------------------------------------------------------------------------- /app/views/kaminari/_next_page.html.haml: -------------------------------------------------------------------------------- 1 | %li 2 | = link_to_unless current_page.last?, raw(t 'views.pagination.next'), url, :rel => 'next', :remote => remote 3 | -------------------------------------------------------------------------------- /app/views/kaminari/_page.html.haml: -------------------------------------------------------------------------------- 1 | - if page.current? 2 | %li.active 3 | = content_tag :a, page, remote: remote, rel: (page.next? ? 'next' : (page.prev? ? 'prev' : nil)) 4 | - else 5 | %li 6 | = link_to page, url, remote: remote, rel: (page.next? ? 'next' : (page.prev? ? 'prev' : nil)) 7 | -------------------------------------------------------------------------------- /app/views/kaminari/_paginator.html.haml: -------------------------------------------------------------------------------- 1 | = paginator.render do 2 | %ul.pagination 3 | = first_page_tag unless current_page.first? 4 | = prev_page_tag unless current_page.first? 5 | - each_page do |page| 6 | - if page.left_outer? || page.right_outer? || page.inside_window? 7 | = page_tag page 8 | - elsif !page.was_truncated? 9 | = gap_tag 10 | = next_page_tag unless current_page.last? 11 | = last_page_tag unless current_page.last? 12 | -------------------------------------------------------------------------------- /app/views/kaminari/_prev_page.html.haml: -------------------------------------------------------------------------------- 1 | %li 2 | = link_to_unless current_page.first?, raw(t 'views.pagination.previous'), url, :rel => 'prev', :remote => remote 3 | -------------------------------------------------------------------------------- /app/views/layouts/admin.html.haml: -------------------------------------------------------------------------------- 1 | !!! 2 | %html 3 | %head 4 | %title 5 | = t('obeder') 6 | = csrf_meta_tags 7 | = favicon_link_tag '/favicon.ico' 8 | = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' 9 | = javascript_include_tag 'application', 'data-turbolinks-track': 'reload' 10 | %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1' } 11 | %body 12 | = render 'web/shared/nav' 13 | .container 14 | = bootstrap_flash 15 | = yield 16 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.haml: -------------------------------------------------------------------------------- 1 | %hmtl 2 | %body 3 | = yield 4 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.haml: -------------------------------------------------------------------------------- 1 | = yield 2 | -------------------------------------------------------------------------------- /app/views/layouts/session.html.haml: -------------------------------------------------------------------------------- 1 | !!! 2 | %html 3 | %head 4 | %title 5 | = t('obeder') 6 | = csrf_meta_tags 7 | = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' 8 | = javascript_include_tag 'application', 'data-turbolinks-track': 'reload' 9 | %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1' } 10 | %body 11 | .container 12 | %br 13 | = bootstrap_flash 14 | = yield 15 | -------------------------------------------------------------------------------- /app/views/mailers/user_mailer/change_password.ru.html.haml: -------------------------------------------------------------------------------- 1 | Здравствуйте, #{ @user.first_name }! 2 | %br 3 | %br 4 | #{ link_to('Придумайте пароль' , edit_user_password_url(user_id: @user.id)) }. 5 | -------------------------------------------------------------------------------- /app/views/mailers/user_mailer/notify_menu_changed.ru.html.haml: -------------------------------------------------------------------------------- 1 | Здравствуйте, #{ @user.first_name }! 2 | %br 3 | %br 4 | Опубликовано #{ link_to("меню на #{l(@menu.date)}", root_url) }. 5 | %ul 6 | - @menu.menu_dishes.each do |menu_dish| 7 | %li 8 | = dish_name(menu_dish) 9 | -------------------------------------------------------------------------------- /app/views/web/admin/daily_reports/index.html.haml: -------------------------------------------------------------------------------- 1 | .row 2 | .col-md-3 3 | = link_to admin_daily_reports_path(date: @date - 1), class: 'hidden-print' do 4 | %h2.glyphicon.glyphicon-chevron-left 5 | .col-md-6 6 | %h1.text-center 7 | =l @date, format: :menu_date 8 | .col-md-3 9 | .text-right 10 | = link_to admin_daily_reports_path(date: @date + 1), class: 'hidden-print' do 11 | %h2.glyphicon.glyphicon-chevron-right 12 | 13 | .well 14 | .row 15 | .col-md-9 16 | .row 17 | - @dishes_stats.in_groups(2, false) do |group| 18 | .col-md-6 19 | - group.each do |stat| 20 | #{stat[:type]}: 21 | %b #{stat[:count]} 22 | %br 23 | %hr 24 | .row 25 | .col-md-12 26 | %b 27 | = t('.dont_eat') 28 | = @neem_users.decorate.names 29 | .col-md-3.text-right.hidden-print 30 | %button.btn.btn-lg.btn-primary{onclick: "window.print()"} 31 | %span.glyphicon.glyphicon-print 32 | = t('.print') 33 | 34 | %p 35 | = t('.default_dishes') 36 | = @default_dishes && @default_dishes.decorate.names 37 | 38 | %table.table.table-condensed 39 | - @user_menus.each_with_index do |user_menu, i| 40 | %tr 41 | %td.visible-print-block 42 | %span.glyphicon.glyphicon-unchecked 43 | %td 44 | #{i + 1}. 45 | = user_menu.user.name 46 | %td 47 | .small 48 | = user_menu.dishes.ordered_by_name.decorate.names_or_default(@default_dishes, t('.default_marker')) 49 | %td 50 | = user_menu.description 51 | -------------------------------------------------------------------------------- /app/views/web/admin/dishes/_form.html.haml: -------------------------------------------------------------------------------- 1 | = simple_form_for @dish, url: url, html: { method: method, class: 'form-horizontal' } do |f| 2 | = f.input :name 3 | = f.input :description 4 | = f.input :dish_type 5 | = f.input :image_cache, as: :hidden 6 | = f.input :image, as: :file, wrapper: :horizontal_form 7 | .form-group 8 | .col-sm-12 9 | .pull-right 10 | = image_tag(@dish.image_url) if @dish.image? 11 | .form-group 12 | .col-sm-offset-3.col-sm-9 13 | = f.submit t('save'), class: 'btn btn-primary' 14 | -------------------------------------------------------------------------------- /app/views/web/admin/dishes/edit.html.haml: -------------------------------------------------------------------------------- 1 | .row 2 | .col-md-6 3 | %h1 4 | = t('.edit_dish') 5 | %br 6 | = render 'form', url: admin_dish_path(@dish), method: :put 7 | -------------------------------------------------------------------------------- /app/views/web/admin/dishes/index.html.haml: -------------------------------------------------------------------------------- 1 | .row 2 | .col-md-6 3 | %h1 4 | = t('title.dishes') 5 | 6 | %br 7 | .col-md-6 8 | .pull-right 9 | = link_to t('form.add'), new_dish_path(@selected_dish_type), class: 'btn btn-default btn-lg btn-primary' 10 | 11 | .row 12 | .col-md-12 13 | %ul.nav.nav-pills.nav-justified{role: 'tablist'} 14 | - Dish.dish_type.values.each do |dish_type| 15 | %li{class: ('active' if dish_type.to_s == @selected_dish_type)} 16 | = link_to dish_type.text, current_dishes_path(dish_type) 17 | 18 | %br 19 | %table.table.table-hover.table-condensed.table-striped 20 | %tr 21 | %th 22 | = sort_link(@q, :name) 23 | %th{width: '10%'} 24 | = t('list.votes') 25 | %th{width: '20%'} 26 | = t('list.rating') 27 | %th{width: '20%'} 28 | = t('list.actions') 29 | - @dishes.each do |dish| 30 | %tr 31 | %td 32 | = image_tag(dish.image.thumb.url) if dish.image? 33 | = dish.name 34 | %td 35 | = t('list.rating_up_down', up: dish.vote_ups_count, down: dish.vote_downs_count) 36 | %td 37 | .progress{style: 'margin-top: 10px; height: 3px'} 38 | .progress-bar.progress-bar-success{style: "width: #{dish.decorate.vote_ups_percent}%"} 39 | .progress-bar.progress-bar-danger{style: "width: #{dish.decorate.vote_downs_percent}%"} 40 | %td{width: '200px'} 41 | = link_to edit_admin_dish_path(dish), {class: 'btn btn-info btn-sm'} do 42 | %i.glyphicon.glyphicon-plus.hidden-sm.hidden-md.hidden-lg 43 | %span.hidden-xs 44 | = t('form.edit') 45 | = link_to admin_dish_path(dish), {class: 'btn btn-danger btn-sm', method: :delete, data: { confirm: t('form.confirm') }} do 46 | %i.glyphicon.glyphicon-trash.hidden-sm.hidden-md.hidden-lg 47 | %span.hidden-xs 48 | = t('form.delete') 49 | 50 | .row 51 | .col-md-12 52 | = paginate @dishes 53 | -------------------------------------------------------------------------------- /app/views/web/admin/dishes/new.html.haml: -------------------------------------------------------------------------------- 1 | .row 2 | .col-md-6 3 | %h1 4 | = t('.add_dish') 5 | %br 6 | = render 'form', url: admin_dishes_path, method: :post 7 | -------------------------------------------------------------------------------- /app/views/web/admin/menus/_closest_days_menus.html.haml: -------------------------------------------------------------------------------- 1 | .row 2 | %br 3 | .row.well 4 | - closest_days_menus = Menu.closest_days_menus(@menu.date) 5 | - col_index = col_index(closest_days_menus) 6 | - closest_days_menus.each do |menu| 7 | %div{ class: "col-md-#{col_index}" } 8 | %b{ style: 'padding-left: 20px' } 9 | = l menu.date, format: :menu_date 10 | %ul 11 | - menu.menu_dishes.each do |menu_dish| 12 | %li 13 | = dish_name(menu_dish) 14 | -------------------------------------------------------------------------------- /app/views/web/admin/menus/_form.html.haml: -------------------------------------------------------------------------------- 1 | = render 'validation_modal' 2 | 3 | .text-center 4 | - if @menu.editable? 5 | %button{ id: 'menu-publish__btn', class: 'btn btn-lg btn-success', data: { toggle: :modal, target: '#menu-validation__modal' } } 6 | = t('.publish_menu') 7 | %br 8 | %br 9 | 10 | = simple_form_for @menu, url: admin_menu_path(@menu.date), method: :put, html: { id: 'menu-form__form' } do |f| 11 | .row 12 | .col-md-12 13 | = f.error_notification 14 | 15 | = f.simple_fields_for :menu_dishes do |menu_dish| 16 | = render 'menu_dish_fields', f: menu_dish 17 | 18 | .links.text-right 19 | = link_to_add_association t('.add_dish'), f, :menu_dishes, class: 'btn btn-lg btn-info' 20 | 21 | %br 22 | .well 23 | .pull-left 24 | = f.button :submit, value: t('.save'), class: 'btn btn-lg btn-primary' 25 | .clearfix 26 | -------------------------------------------------------------------------------- /app/views/web/admin/menus/_menu_dish_fields.html.haml: -------------------------------------------------------------------------------- 1 | .nested-fields 2 | .row 3 | .col-md-2 4 | = f.input :default, label: t('.default') 5 | .col-md-7 6 | = f.input :dish_id, as: :select, collection: Dish.ordered_by_name.decorate, 7 | label_method: :name_with_type, label: false 8 | .col-md-3.text-right 9 | = link_to_remove_association t('.delete_dish'), f, class: 'btn btn-danger' 10 | %hr 11 | -------------------------------------------------------------------------------- /app/views/web/admin/menus/_validation_modal.html.haml: -------------------------------------------------------------------------------- 1 | #menu-validation__modal.modal.fade{ role: :dialog, validationUrl: validate_api_admin_menu_path(@menu.date) } 2 | .modal-dialog{ role: :document } 3 | .modal-content 4 | .modal-header 5 | %button.close{ data: { dismiss: :modal } } 6 | %span{ aria: { hidden: true } } 7 | × 8 | %h4 9 | = t('.publish_modal_header') 10 | .modal-body 11 | %p 12 | %span.glyphicon.glyphicon-warning-sign 13 | = t('.check_if_menu_saved') 14 | %p#menu-validation__marker__in-validation 15 | %span.glyphicon.glyphicon-refresh 16 | = t('.validation_in_progress') 17 | %p#menu-validation__marker__ok 18 | %span.glyphicon.glyphicon-ok 19 | = t('.validation_ok') 20 | %p#menu-validation__marker__failed 21 | %span.glyphicon.glyphicon-remove 22 | = t('.validation_failed') 23 | %ol#menu-validation__errors-list 24 | .modal-footer 25 | = simple_form_for @menu, url: approve_admin_menu_path(@menu.date), method: :put do |f| 26 | = f.button :submit, value: t('.confirm'), id: 'menu-validation__submit-btn', class: 'btn btn-primary', disabled: true 27 | -------------------------------------------------------------------------------- /app/views/web/admin/menus/edit.html.haml: -------------------------------------------------------------------------------- 1 | = render 'closest_days_menus' 2 | .row 3 | .col-md-3 4 | = link_to edit_admin_menu_path(@menu.date - 1) do 5 | %h2.glyphicon.glyphicon-chevron-left 6 | .col-md-6 7 | %h1.text-center 8 | = t('.menu_for') 9 | = l @menu.date, format: :menu_date 10 | .col-md-3 11 | .text-right 12 | = link_to edit_admin_menu_path(@menu.date + 1) do 13 | %h2.glyphicon.glyphicon-chevron-right 14 | 15 | %br 16 | %br 17 | = render 'form' 18 | = javascript_include_tag 'web/admin/menus/edit' 19 | -------------------------------------------------------------------------------- /app/views/web/admin/monthly_reports/index.html.haml: -------------------------------------------------------------------------------- 1 | .row 2 | .col-md-3 3 | = link_to admin_monthly_reports_path(date: previous_report_params(@date)) do 4 | %h2.glyphicon.glyphicon-chevron-left 5 | .col-md-6 6 | %h1.text-center 7 | = report_header(@date) 8 | .col-md-3 9 | .text-right 10 | = link_to admin_monthly_reports_path(date: next_report_params(@date)) do 11 | %h2.glyphicon.glyphicon-chevron-right 12 | 13 | %br 14 | 15 | .text-right 16 | = link_to t('.export_to_excel'), export_admin_monthly_reports_path(date: @date), target: '_blank', class: 'btn btn-default' 17 | 18 | %br 19 | 20 | %table.table.table-bordered 21 | - dates = dates_in_month(@date) 22 | %tr.text-center 23 | %td 24 | = t('.name') 25 | - dates.each do |date| 26 | %td{ class: cell_color_class(date) } 27 | = date.day 28 | %td 29 | = t('.sum') 30 | - @users.each_with_index do |user, i| 31 | = report_row(user, i, dates, @user_menus) 32 | 33 | %p.text-right 34 | = t('.total') 35 | = @user_menus.size 36 | -------------------------------------------------------------------------------- /app/views/web/admin/users/_form.html.haml: -------------------------------------------------------------------------------- 1 | = simple_form_for @user, url: url, html: { method: method, class: 'form-horizontal' } do |f| 2 | = f.input :name 3 | = f.input :email, input_html: { placeholder: 'username@restream.rt.ru, username@rt.ru' } 4 | = f.input :description 5 | = f.input :role, as: :select, include_blank: false 6 | .form-group 7 | .col-sm-offset-3.col-sm-9 8 | = f.submit 'Сохранить', class: 'btn btn-primary' 9 | -------------------------------------------------------------------------------- /app/views/web/admin/users/edit.html.haml: -------------------------------------------------------------------------------- 1 | .row 2 | .col-md-6 3 | %h1 Редактировать пользователя 4 | %br 5 | = render 'form', url: admin_user_path(@user), method: :put 6 | -------------------------------------------------------------------------------- /app/views/web/admin/users/index.html.haml: -------------------------------------------------------------------------------- 1 | .row 2 | .col-md-6 3 | %h1 Пользователи 4 | %br 5 | .col-md-6 6 | .pull-right 7 | = link_to t('add'), new_admin_user_path, class: 'btn btn-default btn-lg btn-primary' 8 | 9 | .row 10 | .col-md-12 11 | 12 | %table.table.table-hover.table-condensed.table-striped 13 | %tr 14 | %th Фамилия Имя 15 | %th Email 16 | %th Действия 17 | - @users.each do |user| 18 | %tr 19 | %td 20 | = user.name 21 | %span.label{ class: "label-#{ user.active? ? 'success' : 'danger' }" } 22 | = user.aasm.human_state 23 | %td 24 | = user.email 25 | %td 26 | = link_to t('edit'), edit_admin_user_path(user), class: 'btn btn-info btn-sm' 27 | = link_to t('delete'), admin_user_path(user), method: :delete, data: { confirm: t('are_you_sure')}, class: 'btn btn-danger btn-sm' 28 | 29 | .row 30 | .col-md-12 31 | = paginate @users 32 | -------------------------------------------------------------------------------- /app/views/web/admin/users/new.html.haml: -------------------------------------------------------------------------------- 1 | .row 2 | .col-md-6 3 | %h1 4 | = t('.add_user') 5 | %br 6 | = render 'form', url: admin_users_path, method: :post 7 | -------------------------------------------------------------------------------- /app/views/web/remind_passwords/edit.html.haml: -------------------------------------------------------------------------------- 1 | .page-header 2 | %h1 3 | = t('.title') 4 | = simple_form_for @user_type, url: remind_password_path, method: :put, html: { class: 'form-horizontal' } do |f| 5 | .row 6 | .col-md-5 7 | = f.input :email 8 | .form-group 9 | .col-sm-offset-3.col-sm-9 10 | = f.button :submit, t('.reset_password'), class: 'btn btn-primary' 11 | -------------------------------------------------------------------------------- /app/views/web/sessions/new.html.haml: -------------------------------------------------------------------------------- 1 | .page-header 2 | %h1= t('.title') 3 | = simple_form_for @session, url: session_path, html: { class: 'form-horizontal' } do |f| 4 | .row 5 | .col-md-5 6 | = f.input :email 7 | = f.input :password 8 | .form-group 9 | .col-sm-offset-3.col-sm-9 10 | = f.button :submit, t('.login'), class: 'btn btn-primary' 11 | = link_to t('.have_you_forgotten_password'), edit_remind_password_path 12 | -------------------------------------------------------------------------------- /app/views/web/shared/_nav.html.haml: -------------------------------------------------------------------------------- 1 | %nav.navbar.navbar-default 2 | .container-fluid 3 | .navbar-header 4 | %button.navbar-toggle.collapsed{ aria: { expanded: false }, data: { target: '#navbar', toggle: 'collapse' }, type: 'button' } 5 | = link_to t('obeder'), root_path, class: 'navbar-brand' 6 | .collapse.navbar-collapse#navbar 7 | %ul.nav.navbar-nav 8 | = active_link_to t('.menu'), edit_admin_menu_path(Date.current), class_active: 'active', wrap_tag: :li 9 | %li.dropdown 10 | %a.dropdown-toggle{ data: { toggle: :dropdown }, role: :button, aria: { haspopup: true, expanded: false } } 11 | = t('.reports') 12 | %span.caret 13 | %ul.dropdown-menu 14 | = active_link_to t('.daily-reports'), admin_daily_reports_path(date: Date.current), class_active: 'active', wrap_tag: :li 15 | = active_link_to t('.monthly-reports'), admin_monthly_reports_path(date: Date.current), class_active: 'active', wrap_tag: :li 16 | = active_link_to t('.dishes'), admin_dishes_path, class_active: 'active', wrap_tag: :li 17 | - if current_user.role.admin? 18 | = active_link_to t('.users'), admin_users_path, class_active: 'active', wrap_tag: :li 19 | %ul.nav.navbar-nav.navbar-right 20 | %li 21 | %p.navbar-text 22 | = current_user.name 23 | %li 24 | = link_to t('logout'), logout_path 25 | -------------------------------------------------------------------------------- /app/views/web/user/passwords/edit.html.haml: -------------------------------------------------------------------------------- 1 | .page-header 2 | %h1= t('.title') 3 | = simple_form_for @user, url: user_password_path, method: :put, html: { class: 'form-horizontal' } do |f| 4 | .row 5 | .col-md-7 6 | = f.input :password 7 | = f.input :password_confirmation 8 | 9 | .form-group 10 | .col-sm-offset-3.col-sm-9 11 | = f.button :submit, t('save'), class: 'btn btn-primary' 12 | -------------------------------------------------------------------------------- /app/workers/user_menu_freeze_worker.rb: -------------------------------------------------------------------------------- 1 | class UserMenuFreezeWorker 2 | include Sidekiq::Worker 3 | 4 | def perform 5 | tomorrow_user_menus = UserMenu.joins(:menu, :user).where(menus: { date: Date.tomorrow }) 6 | tomorrow_user_menus.update(editable: false) 7 | 8 | neem_user_menus = tomorrow_user_menus.where(users: { neem: true }) 9 | neem_user_menus.update(neem: true) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/workers/user_menus_create_worker.rb: -------------------------------------------------------------------------------- 1 | class UserMenusCreateWorker 2 | include Sidekiq::Worker 3 | sidekiq_options queue: 'critical' 4 | 5 | def perform(menu_id) 6 | menu = Menu.find_by(id: menu_id) 7 | 8 | ActiveRecord::Base.transaction do 9 | User.find_each do |user| 10 | user_menu = UserMenu.create(user: user, menu: menu, neem: user.neem) 11 | menu_dishes = menu.menu_dishes.default 12 | user_menu.dishes << menu_dishes.map(&:dish) 13 | end 14 | menu.publish! 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/workers/user_menus_notify_worker.rb: -------------------------------------------------------------------------------- 1 | class UserMenusNotifyWorker 2 | include Sidekiq::Worker 3 | 4 | def perform(menu_id) 5 | menu = Menu.find_by(id: menu_id) 6 | 7 | User.find_each { |user| UserMailer.notify_menu_changed(user, menu).deliver } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | APP_PATH = File.expand_path('../config/application', __dir__) 8 | require_relative '../config/boot' 9 | require 'rails/commands' 10 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | require_relative '../config/boot' 8 | require 'rake' 9 | Rake.application.run 10 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a starting point to setup your application. 15 | # Add necessary setup steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | # puts "\n== Copying sample files ==" 22 | # unless File.exist?('config/database.yml') 23 | # cp 'config/database.yml.sample', 'config/database.yml' 24 | # end 25 | 26 | puts "\n== Preparing database ==" 27 | system! 'bin/rails db:setup' 28 | 29 | puts "\n== Removing old logs and tempfiles ==" 30 | system! 'bin/rails log:clear tmp:clear' 31 | 32 | puts "\n== Restarting application server ==" 33 | system! 'bin/rails restart' 34 | end 35 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) 11 | spring = lockfile.specs.detect { |spec| spec.name == "spring" } 12 | if spring 13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 14 | gem 'spring', spring.version 15 | require 'spring/binstub' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a way to update your development environment automatically. 15 | # Add necessary update steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | puts "\n== Updating database ==" 22 | system! 'bin/rails db:migrate' 23 | 24 | puts "\n== Removing old logs and tempfiles ==" 25 | system! 'bin/rails log:clear tmp:clear' 26 | 27 | puts "\n== Restarting application server ==" 28 | system! 'bin/rails restart' 29 | end 30 | -------------------------------------------------------------------------------- /clock.rb: -------------------------------------------------------------------------------- 1 | require './config/boot' 2 | require './config/environment' 3 | require 'clockwork' 4 | 5 | module Clockwork 6 | configure do |config| 7 | config[:tz] = Time.zone.name 8 | end 9 | 10 | every(1.day, 'freeze_user_menu', at: Settings.freeze_user_menu_at) do 11 | UserMenuFreezeWorker.perform_async 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /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 Obeder 10 | class Application < Rails::Application 11 | config.generators do |g| 12 | g.test_framework :test_unit, fixture: false, fixture_replacement: :factory_girl 13 | end 14 | 15 | config.time_zone = 'Samara' 16 | config.i18n.default_locale = :ru 17 | config.autoload_paths += Dir["#{config.root}/lib/**/"] 18 | config.active_record.observers = :menu_observer 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /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: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: postgresql 3 | encoding: unicode 4 | host: <%= ENV.fetch("DATABASE_HOST") {"127.0.0.1"} %> 5 | port: <%= ENV.fetch("DATABASE_PORT") {5432} %> 6 | pool: <%= ENV.fetch("DATABASE_POOL") {5} %> 7 | username: <%= ENV.fetch("DATABASE_USERNAME") {"postgres"} %> 8 | password: <%= ENV.fetch("DATABASE_PASSWORD") {""} %> 9 | 10 | development: 11 | <<: *default 12 | database: obeder_development 13 | 14 | test: 15 | <<: *default 16 | database: obeder_test 17 | 18 | production: 19 | <<: *default 20 | database: obeder_production 21 | -------------------------------------------------------------------------------- /config/deploy.rb: -------------------------------------------------------------------------------- 1 | lock '3.8.0' 2 | 3 | set :application, 'obeder' 4 | set :repo_url, 'git@github.com:Restream/obeder.git' 5 | 6 | set :puma_role, :web 7 | set :keep_releases, 3 8 | 9 | append :linked_dirs, 'tmp/pids', 'tmp/sockets', 'log', 'public/uploads' 10 | 11 | namespace :deploy do 12 | namespace :assets do 13 | desc 'build frontend' 14 | task :build_frontend do 15 | on roles(:web) do 16 | within release_path do 17 | execute 'yarn' 18 | execute "npm run build -C #{release_path}" 19 | end 20 | end 21 | end 22 | end 23 | 24 | desc 'Restart obeder application' 25 | task :restart_obeder do 26 | on roles(:all) do 27 | execute 'sudo service obeder_puma restart' 28 | execute 'sudo service obeder_sidekiq restart' 29 | execute 'sudo service obeder_clockwork restart' 30 | end 31 | end 32 | end 33 | 34 | before 'deploy:assets:precompile', 'deploy:assets:build_frontend' 35 | after 'deploy:finished', 'deploy:restart_obeder' 36 | -------------------------------------------------------------------------------- /config/deploy/production.rb: -------------------------------------------------------------------------------- 1 | set :host, 'obeder-1.staging.ul.restr.im' 2 | role :web, fetch(:host) 3 | role :db, fetch(:host) 4 | 5 | set :branch, :master 6 | set :rails_env, :production 7 | 8 | set :user, 'deployer' 9 | set :homedir, "/home/#{fetch(:user)}" 10 | set :ssh_options, user: fetch(:user) 11 | 12 | set :deploy_to, "#{fetch(:homedir)}/apps/#{fetch(:application)}" 13 | -------------------------------------------------------------------------------- /config/deploy/vagrant.rb: -------------------------------------------------------------------------------- 1 | set :host, '10.10.10.10' 2 | role :web, fetch(:host) 3 | role :db, fetch(:host) 4 | 5 | set :branch, :master 6 | set :rails_env, :production 7 | 8 | set :user, 'vagrant' 9 | set :homedir, "/home/#{fetch(:user)}" 10 | set :ssh_options, user: fetch(:user) 11 | 12 | set :deploy_to, "#{fetch(:homedir)}/apps/#{fetch(:application)}" 13 | -------------------------------------------------------------------------------- /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 = true 31 | 32 | config.action_mailer.perform_caching = false 33 | 34 | # send emails via sendgrid 35 | config.action_mailer.delivery_method = :smtp 36 | config.action_mailer.smtp_settings = { 37 | port: '587', 38 | address: 'smtp.sendgrid.net', 39 | user_name: ENV['SENDGRID_USERNAME'], 40 | password: ENV['SENDGRID_PASSWORD'], 41 | domain: Settings.domain, 42 | authentication: :plain, 43 | } 44 | 45 | # Print deprecation notices to the Rails logger. 46 | config.active_support.deprecation = :log 47 | 48 | # Raise an error on page load if there are pending migrations. 49 | config.active_record.migration_error = :page_load 50 | 51 | # Debug mode disables concatenation and preprocessing of assets. 52 | # This option may cause significant delays in view rendering with a large 53 | # number of complex assets. 54 | config.assets.debug = true 55 | 56 | # Suppress logger output for asset requests. 57 | config.assets.quiet = true 58 | 59 | # Raises error for missing translations 60 | # config.action_view.raise_on_missing_translations = true 61 | 62 | # Use an evented file watcher to asynchronously detect changes in source code, 63 | # routes, locales, etc. This feature depends on the listen gem. 64 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 65 | 66 | config.after_initialize do 67 | Bullet.enable = true 68 | Bullet.alert = true 69 | Bullet.bullet_logger = true 70 | Bullet.console = true 71 | Bullet.rails_logger = true 72 | Bullet.add_footer = true 73 | Bullet.raise = false 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /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 | 43 | config.after_initialize do 44 | Bullet.enable = true 45 | Bullet.bullet_logger = true 46 | Bullet.raise = true 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /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 | # ) 7 | -------------------------------------------------------------------------------- /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 | Rails.application.config.assets.precompile += %w( web/admin/menus/edit.js ) 13 | -------------------------------------------------------------------------------- /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/config.rb: -------------------------------------------------------------------------------- 1 | Config.setup do |config| 2 | config.const_name = 'Settings' 3 | end 4 | -------------------------------------------------------------------------------- /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 Guide for Upgrading Ruby on Rails 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/observers.rb: -------------------------------------------------------------------------------- 1 | # https://github.com/rails/rails-observers/issues/45 2 | MenuObserver.instance 3 | -------------------------------------------------------------------------------- /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: '_obeder_session', expire_after: 1.month 4 | -------------------------------------------------------------------------------- /config/initializers/sidekiq.rb: -------------------------------------------------------------------------------- 1 | Sidekiq.configure_server do |config| 2 | config.redis = { 3 | url: "redis://#{Settings.redis.host}:#{Settings.redis.port}/#{Settings.redis.sidekiq_db}" 4 | } 5 | end 6 | 7 | Sidekiq.configure_client do |config| 8 | config.redis = { 9 | url: "redis://#{Settings.redis.host}:#{Settings.redis.port}/#{Settings.redis.sidekiq_db}" 10 | } 11 | end 12 | -------------------------------------------------------------------------------- /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/activerecord.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | activerecord: 3 | attributes: 4 | dish: 5 | name: 'Name' 6 | description: 'Description' 7 | dish_type: 'Dish type' 8 | -------------------------------------------------------------------------------- /config/locales/activerecord.ru.yml: -------------------------------------------------------------------------------- 1 | ru: 2 | activerecord: 3 | attributes: 4 | dish: 5 | name: Название 6 | description: Описание 7 | dish_type: Тип блюда 8 | image: Фото блюда 9 | user: 10 | name: Фамилия Имя 11 | email: Почта 12 | neem: Не есть 13 | description: Описание 14 | role: Роль 15 | aasm_state: 16 | active: Активный 17 | inactive: Неактивный 18 | user_sign_in_type: 19 | email: Почта 20 | password: Пароль 21 | user_password_edit_type: 22 | password: Введите пароль 23 | password_confirmation: Повторите пароль 24 | 25 | errors: 26 | messages: 27 | less_than_two_soups: Менее двух супов 28 | less_than_two_salads: Менее двух салатов 29 | incorrect_defaults: Блюда по умолчанию не указаны или указаны неверно 30 | main_to_side_dishes_mismatch: Количество гарниров не совпадает с количеством основных блюд 31 | less_than_two_main_dishes: Недостаточно основных или самостоятельных блюд 32 | invalid_email: Неправильная почта 33 | invalid_restream_email: "Введите корпоративную почту: username@restream.rt.ru или username@rt.ru" 34 | blank: Обязательное поле 35 | 36 | models: 37 | user: 38 | attributes: 39 | email: 40 | blank: Введите почту 41 | taken: Занято! 42 | name: 43 | blank: Введите фамилию и имя 44 | role: 45 | blank: Выберите роль 46 | user_remind_password_type: 47 | attributes: 48 | email: 49 | blank: Введите почту 50 | user_sign_in_type: 51 | attributes: 52 | email: 53 | blank: Введите почту 54 | password: 55 | blank: Введите пароль 56 | user_or_password_invalid: Неправильные почта или пароль 57 | user_password_edit_type: 58 | attributes: 59 | password: 60 | blank: Введите пароль 61 | password_confirmation: 62 | blank: Введите пароль 63 | confirmation: Пароли не совпадают 64 | user_menu: 65 | attributes: 66 | editable: 67 | inclusion: Меню на сегодня нельзя редактировать 68 | menu_dish_vote_type: 69 | attributes: 70 | user_menu_id: 71 | invalid: Указан неверный идентификатор меню пользователя 72 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | 25 | form: 26 | confirm: Are you sure? 27 | edit: Edit 28 | delete: Delete 29 | add: Create 30 | 31 | list: 32 | no_data: No data 33 | actions: Actions 34 | rating: Rating 35 | 36 | title: 37 | dishes: Dishes 38 | -------------------------------------------------------------------------------- /config/locales/enumerize.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | enumerize: 3 | dish: 4 | dish_type: 5 | soup: Soup 6 | main_dish: Main dish 7 | side_dish: Side dish 8 | salad: Salad 9 | separate_dish: Separate dish 10 | -------------------------------------------------------------------------------- /config/locales/enumerize.ru.yml: -------------------------------------------------------------------------------- 1 | ru: 2 | enumerize: 3 | dish: 4 | dish_type: 5 | soup: Суп 6 | main_dish: Основное блюдо 7 | side_dish: Гарнир 8 | salad: Салат 9 | separate_dish: Самостоятельное блюдо 10 | user: 11 | role: 12 | admin: Админ 13 | cook: Повар 14 | user: Пользователь 15 | -------------------------------------------------------------------------------- /config/locales/ru.yml: -------------------------------------------------------------------------------- 1 | ru: 2 | errors: 3 | messages: 4 | carrierwave_processing_error: Нельзя изменить размер изображения 5 | carrierwave_integrity_error: Не изображение 6 | carrierwave_download_error: Не получается загрузить изображение 7 | extension_whitelist_error: "Неправильное расширение %{extension}. Разрешены только %{allowed_types}" 8 | 9 | user_mailer: 10 | change_password: 11 | subject: "Обедер: Установка пароля" 12 | notify_menu_changed: 13 | subject: "Обедер: Новое меню на %{date}" 14 | 15 | date: 16 | formats: 17 | menu_date: '%d %B, %A' 18 | monthly_report_date: '%B, %Y' 19 | 20 | flash: 21 | web: 22 | admin: 23 | menus: 24 | approve: 25 | success: Меню опубликовано 26 | update: 27 | success: Меню обновлено 28 | error: Произошла ошибка при обновлении меню. Блюдо не может быть пустым. Выберите блюдо из списка или удалите его. 29 | form: 30 | confirm: Вы уверены? 31 | edit: Редактировать 32 | delete: Удалить 33 | add: Добавить 34 | 35 | list: 36 | no_data: Нет данных 37 | actions: Действия 38 | rating: Рейтинг 39 | votes: Оценки 40 | rating_up_down: "За %{up}, против %{down}" 41 | 42 | title: 43 | dishes: Блюда 44 | 45 | save: Сохранить 46 | add: Добавить 47 | edit: Редактировать 48 | delete: Удалить 49 | are_you_sure: Вы уверены? 50 | password_changed: Пароль изменён 51 | authenticate_to_change_password: Войдите, чтобы сменить пароль 52 | password_not_set: Пароль не установлен 53 | password_has_been_reset: Пароль сброшен, проверьте почту 54 | user_not_found: Пользователь не найден 55 | logout: Выйти 56 | obeder: Обедер 57 | 58 | pundit: 59 | admin: Войдите как администратор 60 | cook: Войдите как повар 61 | user: Войдите 62 | -------------------------------------------------------------------------------- /config/locales/simple_form.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | simple_form: 3 | "yes": 'Yes' 4 | "no": 'No' 5 | required: 6 | text: 'required' 7 | mark: '*' 8 | # You can uncomment the line below if you need to overwrite the whole required html. 9 | # When using html, text and mark won't be used. 10 | # html: '*' 11 | error_notification: 12 | default_message: "Please review the problems below:" 13 | # Examples 14 | # labels: 15 | # defaults: 16 | # password: 'Password' 17 | # user: 18 | # new: 19 | # email: 'E-mail to sign in.' 20 | # edit: 21 | # email: 'E-mail.' 22 | # hints: 23 | # defaults: 24 | # username: 'User name to sign in.' 25 | # password: 'No special characters, please.' 26 | # include_blanks: 27 | # defaults: 28 | # age: 'Rather not say' 29 | # prompts: 30 | # defaults: 31 | # age: 'Select your age' 32 | -------------------------------------------------------------------------------- /config/locales/simple_form.ru.yml: -------------------------------------------------------------------------------- 1 | ru: 2 | simple_form: 3 | "yes": 'Да' 4 | "no": 'Нет' 5 | required: 6 | text: 'required' 7 | mark: '*' 8 | error_notification: 9 | default_message: 'Please review the problems below:' 10 | -------------------------------------------------------------------------------- /config/locales/views.ru.yml: -------------------------------------------------------------------------------- 1 | ru: 2 | web: 3 | admin: 4 | users: 5 | new: 6 | add_user: Добавить пользователя 7 | dishes: 8 | new: 9 | add_dish: Добавить блюдо 10 | edit: 11 | edit_dish: Редактировать блюдо 12 | menus: 13 | edit: 14 | menu_for: Меню на 15 | closest_days_menus: 16 | closest_days_menus_btn: Меню на ближайшие дни 17 | menu_dish_fields: 18 | default: По умолчанию 19 | delete_dish: Удалить блюдо 20 | form: 21 | publish_menu: Опубликовать меню 22 | add_dish: Добавить блюдо 23 | save: Сохранить 24 | validation_modal: 25 | publish_modal_header: Подтвердите публикацию меню 26 | check_if_menu_saved: Проверьте, сохранено ли меню! 27 | validation_in_progress: Проверка меню... 28 | validation_ok: Проверка пройдена 29 | validation_failed: Во время проверки возникли ошибки 30 | confirm: Подтвердить 31 | daily_reports: 32 | index: 33 | print: Печать 34 | dont_eat: 'Не едят:' 35 | default_dishes: 'Блюда по умолчанию:' 36 | default_marker: По умолчанию 37 | monthly_reports: 38 | index: 39 | name: ФИО 40 | total: 'Итого:' 41 | sum: ∑ 42 | export_to_excel: Скачать Excel-файл 43 | shared: 44 | nav: 45 | menu: Меню 46 | reports: Отчёты 47 | daily-reports: Ежедневные отчёты 48 | monthly-reports: Ежемесячные отчёты 49 | dishes: Блюда 50 | users: Пользователи 51 | sessions: 52 | new: 53 | title: Вход 54 | login: Вход 55 | have_you_forgotten_password: Сбросить пароль 56 | remind_passwords: 57 | edit: 58 | title: Восстановление пароля 59 | reset_password: Сбросить пароль 60 | user: 61 | passwords: 62 | edit: 63 | title: Установка пароля 64 | monthly_report: 65 | upload_date: 'Дата выгрузки:' 66 | name: ФИО 67 | sum: ∑ 68 | total: 'Итого:' 69 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | threads_count = ENV.fetch('RAILS_MAX_THREADS') { 5 }.to_i 2 | threads threads_count, threads_count 3 | port ENV.fetch('PORT') { 3000 } 4 | environment ENV.fetch('RAILS_ENV', 'development') 5 | plugin :tmp_restart 6 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | require 'sidekiq/web' 2 | 3 | Rails.application.routes.draw do 4 | mount Sidekiq::Web => '/sidekiq' 5 | 6 | scope module: :web do 7 | root to: 'welcome#index' 8 | 9 | resources :user, only: [] do 10 | scope module: :user do 11 | resource :password, only: [:edit, :update] 12 | end 13 | end 14 | resource :remind_password, only: [:edit, :update] 15 | resource :session, only: [:new, :create, :destroy] 16 | get :login, to: 'sessions#new' 17 | get :logout, to: 'sessions#destroy' 18 | 19 | namespace :admin do 20 | root to: 'welcome#index' 21 | resources :dishes, only: [:index, :edit, :new, :create, :update, :destroy] 22 | resources :daily_reports, only: [:index] 23 | resources :users, only: [:index, :new, :create, :edit, :update, :destroy] 24 | resources :menus, param: :date, only: [:edit, :update] do 25 | member do 26 | put :approve 27 | end 28 | end 29 | resources :monthly_reports, only: [:index] do 30 | collection do 31 | get :export 32 | end 33 | end 34 | end 35 | end 36 | 37 | namespace :api do 38 | resource :user, only: [:show, :update] do 39 | scope module: :user do 40 | resources :menus, only: [:index] 41 | end 42 | end 43 | namespace :admin do 44 | resources :menus, param: :date do 45 | member do 46 | get :validate 47 | end 48 | end 49 | end 50 | resources :dishes, only: [:index, :create, :update, :destroy] 51 | resources :menus, only: [:index, :create, :update, :destroy] 52 | resources :user_menus, only: [:index, :update] 53 | resources :user_menu_votes, only: [:update], param: :user_menu_id 54 | end 55 | 56 | match '*path' => 'web/welcome#index', via: :get 57 | end 58 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rails secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 910d52a482cb33fba436228450ec6bc6266d1b39e6dbba652f3fbf19de569d5413737b548ccd5d6047e79e8258e7ffd3afe18a5a798fdbda0369074da3af6f59 15 | 16 | test: 17 | secret_key_base: 9352c1dddd5405fb75cca02f8ec9cf1a0ae0c7cabc417df0005e53b1df438058b4ef0096a6905cb70981e5d6f3cfce2414bbe7e163e7d90dd9398b2def99e36a 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /config/settings.yml: -------------------------------------------------------------------------------- 1 | closest_menus_date_offset: 2 2 | domain: obeder.restream.pro 3 | freeze_user_menu_at: '15:00' 4 | redis: 5 | host: <%= ENV['REDIS_HOST'] || '127.0.0.1' %> 6 | port: <%= ENV['REDIS_PORT'] || 6379 %> 7 | sidekiq_db: <%= ENV['REDIS_DB'] || 'db0' %> 8 | -------------------------------------------------------------------------------- /config/settings/development.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Restream/obeder/f8fbf8d87842cfc8c5a23c8e4cf1d92584f31454/config/settings/development.yml -------------------------------------------------------------------------------- /config/settings/production.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Restream/obeder/f8fbf8d87842cfc8c5a23c8e4cf1d92584f31454/config/settings/production.yml -------------------------------------------------------------------------------- /config/settings/test.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Restream/obeder/f8fbf8d87842cfc8c5a23c8e4cf1d92584f31454/config/settings/test.yml -------------------------------------------------------------------------------- /config/sidekiq.yml: -------------------------------------------------------------------------------- 1 | :concurrency: 5 2 | :pidfile: tmp/pids/sidekiq.pid 3 | staging: 4 | :concurrency: 10 5 | production: 6 | :concurrency: 20 7 | :queues: 8 | - default 9 | - [critical, 2] 10 | -------------------------------------------------------------------------------- /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/20170204092318_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | enable_extension 'uuid-ossp' 4 | 5 | create_table :users, id: :uuid do |t| 6 | t.string :name 7 | t.string :email 8 | t.boolean :neem 9 | t.string :description 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20170204092406_create_dishes.rb: -------------------------------------------------------------------------------- 1 | class CreateDishes < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :dishes do |t| 4 | t.string :name 5 | t.text :description 6 | t.string :dish_type 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20170204094633_create_menus.rb: -------------------------------------------------------------------------------- 1 | class CreateMenus < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :menus do |t| 4 | t.date :date 5 | t.boolean :ready 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20170204095020_create_menus_users.rb: -------------------------------------------------------------------------------- 1 | class CreateMenusUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :menus_users do |t| 4 | t.uuid :user_id 5 | t.references :menu, foreign_key: true 6 | end 7 | add_foreign_key :menus_users, :users 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20170204095411_create_dishes_menus_users.rb: -------------------------------------------------------------------------------- 1 | class CreateDishesMenusUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :dishes_menus_users do |t| 4 | t.references :menus_user, foreign_key: true 5 | t.references :dish, foreign_key: true 6 | t.boolean :neem 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20170204100217_create_dishes_menus.rb: -------------------------------------------------------------------------------- 1 | class CreateDishesMenus < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :dishes_menus do |t| 4 | t.references :menu, foreign_key: true 5 | t.references :dish, foreign_key: true 6 | t.boolean :default 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20170204111159_rename_tables.rb: -------------------------------------------------------------------------------- 1 | class RenameTables < ActiveRecord::Migration[5.0] 2 | def change 3 | rename_table :menus_users, :user_menus 4 | rename_table :dishes_menus_users, :user_menu_dishes 5 | rename_table :dishes_menus, :menu_dishes 6 | 7 | rename_column :user_menu_dishes, :menus_user_id, :user_menu_id 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20170204142618_add_ready_default_to_menus.rb: -------------------------------------------------------------------------------- 1 | class AddReadyDefaultToMenus < ActiveRecord::Migration[5.0] 2 | def change 3 | change_column_default :menus, :ready, false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170204142829_add_default_default_to_menu_dishes.rb: -------------------------------------------------------------------------------- 1 | class AddDefaultDefaultToMenuDishes < ActiveRecord::Migration[5.0] 2 | def change 3 | change_column_default :menu_dishes, :default, false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170211084848_add_neem_default_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddNeemDefaultToUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | change_column_default :users, :neem, false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170211085009_add_neem_default_to_user_menu_dishes.rb: -------------------------------------------------------------------------------- 1 | class AddNeemDefaultToUserMenuDishes < ActiveRecord::Migration[5.0] 2 | def change 3 | change_column_default :user_menu_dishes, :neem, false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170211102339_add_description_to_user_menus.rb: -------------------------------------------------------------------------------- 1 | class AddDescriptionToUserMenus < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :user_menus, :description, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170211143820_remove_neem_from_user_menu_dishes.rb: -------------------------------------------------------------------------------- 1 | class RemoveNeemFromUserMenuDishes < ActiveRecord::Migration[5.0] 2 | def change 3 | remove_column :user_menu_dishes, :neem, :boolean, default: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170211145604_add_neem_to_user_menu.rb: -------------------------------------------------------------------------------- 1 | class AddNeemToUserMenu < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :user_menus, :neem, :boolean, default: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170317115018_add_password_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddPasswordToUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :users, :password_digest, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170318184540_add_role_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddRoleToUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :users, :role, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170328131232_add_unique_index_to_user_menu_dishes.rb: -------------------------------------------------------------------------------- 1 | class UserMenuDish < ActiveRecord::Base; end 2 | 3 | class AddUniqueIndexToUserMenuDishes < ActiveRecord::Migration[5.0] 4 | def change 5 | grouped = UserMenuDish.all.group_by { |model| [model.user_menu_id, model.dish_id] } 6 | grouped.values.each do |duplicates| 7 | duplicates.shift 8 | duplicates.each { |duplicate| duplicate.destroy } 9 | end 10 | 11 | add_index :user_menu_dishes, [:user_menu_id, :dish_id], unique: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20170328151632_add_aasm_state_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddAasmStateToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :aasm_state, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170329202145_add_editable_to_user_menus.rb: -------------------------------------------------------------------------------- 1 | class AddEditableToUserMenus < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :user_menus, :editable, :boolean, default: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170404071643_add_image_to_dishes.rb: -------------------------------------------------------------------------------- 1 | class AddImageToDishes < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :dishes, :image, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170404135527_add_assm_state_to_menu.rb: -------------------------------------------------------------------------------- 1 | class Menu < ApplicationRecord; end 2 | 3 | class AddAssmStateToMenu < ActiveRecord::Migration[5.0] 4 | def change 5 | add_column :menus, :aasm_state, :string 6 | 7 | Menu.where(ready: true).update_all(aasm_state: :published) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20170404215854_remove_ready_from_menu.rb: -------------------------------------------------------------------------------- 1 | class RemoveReadyFromMenu < ActiveRecord::Migration[5.0] 2 | def change 3 | remove_column :menus, :ready, :boolean, default: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180215191335_create_votes.rb: -------------------------------------------------------------------------------- 1 | class CreateVotes < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :votes do |t| 4 | t.boolean :vote, default: false, null: false 5 | t.references :voteable, polymorphic: true, null: false 6 | t.uuid :user_id 7 | t.timestamps 8 | end 9 | add_foreign_key :votes, :users 10 | 11 | add_index :votes, :user_id 12 | add_index :votes, [:voteable_id, :voteable_type] 13 | 14 | add_index :votes, %w[user_id voteable_id voteable_type], unique: true, name: 'fk_one_vote_per_user_per_entity' 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20180216060838_add_menu_dish_counters.rb: -------------------------------------------------------------------------------- 1 | class AddMenuDishCounters < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :menu_dishes, :vote_downs_count, :integer, default: 0 4 | add_column :menu_dishes, :vote_ups_count, :integer, default: 0 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20180219133606_add_dish_counters.rb: -------------------------------------------------------------------------------- 1 | class AddDishCounters < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :dishes, :vote_downs_count, :integer, default: 0 4 | add_column :dishes, :vote_ups_count, :integer, default: 0 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20180402120933_add_deleted_at_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddDeletedAtToUser < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :users, :deleted_at, :datetime 4 | add_index :users, :deleted_at 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # Note that this schema.rb definition is the authoritative source for your 6 | # database schema. If you need to create the application database on another 7 | # system, you should be using db:schema:load, not running all the migrations 8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 9 | # you'll amass, the slower it'll run and the greater likelihood for issues). 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 20180402120933) do 14 | 15 | # These are extensions that must be enabled in order to support this database 16 | enable_extension "plpgsql" 17 | enable_extension "uuid-ossp" 18 | 19 | create_table "dishes", force: :cascade do |t| 20 | t.string "name" 21 | t.text "description" 22 | t.string "dish_type" 23 | t.string "image" 24 | t.integer "vote_downs_count", default: 0 25 | t.integer "vote_ups_count", default: 0 26 | end 27 | 28 | create_table "menu_dishes", force: :cascade do |t| 29 | t.integer "menu_id" 30 | t.integer "dish_id" 31 | t.boolean "default", default: false 32 | t.integer "vote_downs_count", default: 0 33 | t.integer "vote_ups_count", default: 0 34 | t.index ["dish_id"], name: "index_menu_dishes_on_dish_id", using: :btree 35 | t.index ["menu_id"], name: "index_menu_dishes_on_menu_id", using: :btree 36 | end 37 | 38 | create_table "menus", force: :cascade do |t| 39 | t.date "date" 40 | t.string "aasm_state" 41 | end 42 | 43 | create_table "user_menu_dishes", force: :cascade do |t| 44 | t.integer "user_menu_id" 45 | t.integer "dish_id" 46 | t.index ["dish_id"], name: "index_user_menu_dishes_on_dish_id", using: :btree 47 | t.index ["user_menu_id", "dish_id"], name: "index_user_menu_dishes_on_user_menu_id_and_dish_id", unique: true, using: :btree 48 | t.index ["user_menu_id"], name: "index_user_menu_dishes_on_user_menu_id", using: :btree 49 | end 50 | 51 | create_table "user_menus", force: :cascade do |t| 52 | t.uuid "user_id" 53 | t.integer "menu_id" 54 | t.text "description" 55 | t.boolean "neem", default: false 56 | t.boolean "editable", default: true 57 | t.index ["menu_id"], name: "index_user_menus_on_menu_id", using: :btree 58 | end 59 | 60 | create_table "users", id: :uuid, default: -> { "uuid_generate_v4()" }, force: :cascade do |t| 61 | t.string "name" 62 | t.string "email" 63 | t.boolean "neem", default: false 64 | t.string "description" 65 | t.string "password_digest" 66 | t.string "role" 67 | t.string "aasm_state" 68 | t.datetime "deleted_at" 69 | t.index ["deleted_at"], name: "index_users_on_deleted_at", using: :btree 70 | end 71 | 72 | create_table "votes", force: :cascade do |t| 73 | t.boolean "vote", default: false, null: false 74 | t.string "voteable_type", null: false 75 | t.integer "voteable_id", null: false 76 | t.uuid "user_id" 77 | t.datetime "created_at", null: false 78 | t.datetime "updated_at", null: false 79 | t.index ["user_id", "voteable_id", "voteable_type"], name: "fk_one_vote_per_user_per_entity", unique: true, using: :btree 80 | t.index ["user_id"], name: "index_votes_on_user_id", using: :btree 81 | t.index ["voteable_id", "voteable_type"], name: "index_votes_on_voteable_id_and_voteable_type", using: :btree 82 | t.index ["voteable_type", "voteable_id"], name: "index_votes_on_voteable_type_and_voteable_id", using: :btree 83 | end 84 | 85 | add_foreign_key "menu_dishes", "dishes" 86 | add_foreign_key "menu_dishes", "menus" 87 | add_foreign_key "user_menu_dishes", "dishes" 88 | add_foreign_key "user_menu_dishes", "user_menus" 89 | add_foreign_key "user_menus", "menus" 90 | add_foreign_key "user_menus", "users" 91 | add_foreign_key "votes", "users" 92 | end 93 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | web: 5 | build: . 6 | volumes: 7 | - .:/app 8 | - ~/.ssh/id_rsa:/root/.ssh/id_rsa:ro 9 | - ~/.ssh/known_hosts:/root/.ssh/known_hosts:rw 10 | - ~/.bash_history:/root/.bash_history 11 | - ~/.gitconfig:/root/.gitconfig:ro 12 | - bundle_cache:/usr/local/bundle 13 | ports: 14 | - 3000:3000 15 | links: 16 | - db 17 | - redis 18 | env_file: 19 | - web.env 20 | - secrets.env 21 | 22 | db: 23 | image: postgres:9.6 24 | 25 | redis: 26 | image: redis:3.0 27 | 28 | volumes: 29 | bundle_cache: 30 | -------------------------------------------------------------------------------- /frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["es2015", { "modules": false }], 4 | "stage-2" 5 | ], 6 | "plugins": ["transform-runtime"], 7 | "comments": false, 8 | "env": { 9 | "test": { 10 | "plugins": [ "istanbul" ] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | .idea 6 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # obeder 2 | 3 | > Super obeder 4 | 5 | ## Build Setup 6 | 7 | ``` bash 8 | # install dependencies 9 | npm install 10 | 11 | # serve with hot reload at localhost:8080 12 | npm run dev 13 | 14 | # build for production with minification 15 | npm run build 16 | 17 | # build for production and view the bundle analyzer report 18 | npm run build --report 19 | ``` 20 | 21 | For detailed explanation on how things work, checkout the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader). 22 | -------------------------------------------------------------------------------- /frontend/build/build.js: -------------------------------------------------------------------------------- 1 | // https://github.com/shelljs/shelljs 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | var ora = require('ora') 7 | var path = require('path') 8 | var shell = require('shelljs') 9 | var webpack = require('webpack') 10 | var config = require('../config') 11 | var webpackConfig = require('./webpack.prod.conf') 12 | 13 | var spinner = ora('building for production...') 14 | spinner.start() 15 | 16 | var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory) 17 | shell.rm('-rf', assetsPath) 18 | shell.mkdir('-p', assetsPath) 19 | shell.config.silent = true 20 | shell.cp('-R', 'static/*', assetsPath) 21 | shell.config.silent = false 22 | 23 | webpack(webpackConfig, function (err, stats) { 24 | spinner.stop() 25 | if (err) throw err 26 | process.stdout.write(stats.toString({ 27 | colors: true, 28 | modules: false, 29 | children: false, 30 | chunks: false, 31 | chunkModules: false 32 | }) + '\n\n') 33 | }) 34 | -------------------------------------------------------------------------------- /frontend/build/check-versions.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | var semver = require('semver') 3 | var packageConfig = require('../../package.json') 4 | 5 | function exec (cmd) { 6 | return require('child_process').execSync(cmd).toString().trim() 7 | } 8 | 9 | var versionRequirements = [ 10 | { 11 | name: 'node', 12 | currentVersion: semver.clean(process.version), 13 | versionRequirement: packageConfig.engines.node 14 | }, 15 | { 16 | name: 'npm', 17 | currentVersion: exec('npm --version'), 18 | versionRequirement: packageConfig.engines.npm 19 | } 20 | ] 21 | 22 | module.exports = function () { 23 | var warnings = [] 24 | for (var i = 0; i < versionRequirements.length; i++) { 25 | var mod = versionRequirements[i] 26 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 27 | warnings.push(mod.name + ': ' + 28 | chalk.red(mod.currentVersion) + ' should be ' + 29 | chalk.green(mod.versionRequirement) 30 | ) 31 | } 32 | } 33 | 34 | if (warnings.length) { 35 | console.log('') 36 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 37 | console.log() 38 | for (var i = 0; i < warnings.length; i++) { 39 | var warning = warnings[i] 40 | console.log(' ' + warning) 41 | } 42 | console.log() 43 | process.exit(1) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /frontend/build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('eventsource-polyfill') 3 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 4 | 5 | hotClient.subscribe(function (event) { 6 | if (event.action === 'reload') { 7 | window.location.reload() 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /frontend/build/dev-server.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | 3 | var config = require('../config') 4 | if (!process.env.NODE_ENV) { 5 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) 6 | } 7 | 8 | var opn = require('opn') 9 | var path = require('path') 10 | var express = require('express') 11 | var webpack = require('webpack') 12 | var proxyMiddleware = require('http-proxy-middleware') 13 | var webpackConfig = require('./webpack.dev.conf') 14 | // default port where dev server listens for incoming traffic 15 | var port = process.env.PORT || config.dev.port 16 | // automatically open browser, if not set will be false 17 | var autoOpenBrowser = !!config.dev.autoOpenBrowser 18 | // Define HTTP proxies to your custom API backend 19 | // https://github.com/chimurai/http-proxy-middleware 20 | var proxyTable = config.dev.proxyTable 21 | 22 | var app = express() 23 | var compiler = webpack(webpackConfig) 24 | 25 | var devMiddleware = require('webpack-dev-middleware')(compiler, { 26 | publicPath: webpackConfig.output.publicPath, 27 | quiet: true 28 | }) 29 | 30 | var hotMiddleware = require('webpack-hot-middleware')(compiler, { 31 | log: () => {} 32 | }) 33 | // force page reload when html-webpack-plugin template changes 34 | compiler.plugin('compilation', function (compilation) { 35 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 36 | hotMiddleware.publish({ action: 'reload' }) 37 | cb() 38 | }) 39 | }) 40 | 41 | // proxy api requests 42 | Object.keys(proxyTable).forEach(function (context) { 43 | var options = proxyTable[context] 44 | if (typeof options === 'string') { 45 | options = { target: options } 46 | } 47 | app.use(proxyMiddleware(options.filter || context, options)) 48 | }) 49 | 50 | // handle fallback for HTML5 history API 51 | app.use(require('connect-history-api-fallback')()) 52 | 53 | // serve webpack bundle output 54 | app.use(devMiddleware) 55 | 56 | // enable hot-reload and state-preserving 57 | // compilation error display 58 | app.use(hotMiddleware) 59 | 60 | // serve pure static assets 61 | var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 62 | app.use(staticPath, express.static('./static')) 63 | 64 | var uri = 'http://localhost:' + port 65 | 66 | devMiddleware.waitUntilValid(function () { 67 | console.log('> Listening at ' + uri + '\n') 68 | }) 69 | 70 | module.exports = app.listen(port, function (err) { 71 | if (err) { 72 | console.log(err) 73 | return 74 | } 75 | 76 | // when env is testing, don't need open it 77 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') { 78 | opn(uri) 79 | } 80 | }) 81 | -------------------------------------------------------------------------------- /frontend/build/utils.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | 5 | exports.assetsPath = function (_path) { 6 | var assetsSubDirectory = process.env.NODE_ENV === 'production' 7 | ? config.build.assetsSubDirectory 8 | : config.dev.assetsSubDirectory 9 | return path.posix.join(assetsSubDirectory, _path) 10 | } 11 | 12 | exports.cssLoaders = function (options) { 13 | options = options || {} 14 | // generate loader string to be used with extract text plugin 15 | function generateLoaders (loaders) { 16 | var sourceLoader = loaders.map(function (loader) { 17 | var extraParamChar 18 | if (/\?/.test(loader)) { 19 | loader = loader.replace(/\?/, '-loader?') 20 | extraParamChar = '&' 21 | } else { 22 | loader = loader + '-loader' 23 | extraParamChar = '?' 24 | } 25 | return loader + (options.sourceMap ? extraParamChar + 'sourceMap' : '') 26 | }).join('!') 27 | 28 | // Extract CSS when that option is specified 29 | // (which is the case during production build) 30 | if (options.extract) { 31 | return ExtractTextPlugin.extract({ 32 | use: sourceLoader, 33 | fallback: 'vue-style-loader' 34 | }) 35 | } else { 36 | return ['vue-style-loader', sourceLoader].join('!') 37 | } 38 | } 39 | 40 | // http://vuejs.github.io/vue-loader/en/configurations/extract-css.html 41 | return { 42 | css: generateLoaders(['css']), 43 | postcss: generateLoaders(['css']), 44 | less: generateLoaders(['css', 'less']), 45 | sass: generateLoaders(['css', 'sass?indentedSyntax']), 46 | scss: generateLoaders(['css', 'sass']), 47 | stylus: generateLoaders(['css', 'stylus']), 48 | styl: generateLoaders(['css', 'stylus']) 49 | } 50 | } 51 | 52 | // Generate loaders for standalone style files (outside of .vue) 53 | exports.styleLoaders = function (options) { 54 | var output = [] 55 | var loaders = exports.cssLoaders(options) 56 | for (var extension in loaders) { 57 | var loader = loaders[extension] 58 | output.push({ 59 | test: new RegExp('\\.' + extension + '$'), 60 | loader: loader 61 | }) 62 | } 63 | return output 64 | } 65 | -------------------------------------------------------------------------------- /frontend/build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | const utils = require('./utils'); 2 | const config = require('../config'); 3 | const isProduction = process.env.NODE_ENV === 'production'; 4 | 5 | module.exports = { 6 | loaders: utils.cssLoaders({ 7 | sourceMap: isProduction 8 | ? config.build.productionSourceMap 9 | : config.dev.cssSourceMap, 10 | extract: isProduction, 11 | }), 12 | postcss: [ 13 | require('postcss-import')({ 14 | path: 'src/assets/styles', 15 | }), 16 | require('postcss-cssnext'), 17 | ], 18 | }; 19 | -------------------------------------------------------------------------------- /frontend/build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const utils = require('./utils'); 3 | const config = require('../config'); 4 | const vueLoaderConfig = require('./vue-loader.conf'); 5 | const eslintFriendlyFormatter = require('eslint-friendly-formatter'); 6 | 7 | function resolve(dir) { 8 | return path.join(__dirname, '..', dir); 9 | } 10 | 11 | module.exports = { 12 | entry: { 13 | app: './frontend/src/main.js', 14 | styles: ['font-awesome/css/font-awesome.min.css', 'hint.css'], 15 | fetch: 'whatwg-fetch', 16 | }, 17 | output: { 18 | path: config.build.assetsRoot, 19 | filename: '[name].js', 20 | publicPath: process.env.NODE_ENV === 'production' 21 | ? config.build.assetsPublicPath 22 | : config.dev.assetsPublicPath, 23 | }, 24 | resolve: { 25 | extensions: ['.js', '.vue', '.json'], 26 | modules: [ 27 | resolve('src'), 28 | resolve('../node_modules'), 29 | ], 30 | alias: { 31 | vue$: 'vue/dist/vue.common.js', 32 | src: resolve('src'), 33 | assets: resolve('src/assets'), 34 | styles: resolve('src/assets/styles'), 35 | components: resolve('src/components'), 36 | }, 37 | }, 38 | module: { 39 | rules: [ 40 | { 41 | test: /\.(js|vue)$/, 42 | loader: 'eslint-loader', 43 | enforce: 'pre', 44 | include: [resolve('src'), resolve('test')], 45 | options: { 46 | formatter: eslintFriendlyFormatter, 47 | }, 48 | }, 49 | { 50 | test: /\.vue$/, 51 | loader: 'vue-loader', 52 | options: vueLoaderConfig, 53 | }, 54 | { 55 | test: /\.js$/, 56 | loader: 'babel-loader', 57 | include: [resolve('src'), resolve('test')], 58 | }, 59 | { 60 | test: /\.json$/, 61 | loader: 'json-loader', 62 | }, 63 | { 64 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 65 | loader: 'url-loader', 66 | query: { 67 | limit: 10000, 68 | name: utils.assetsPath('img/[name].[hash:7].[ext]'), 69 | }, 70 | }, 71 | { 72 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 73 | loader: 'url-loader', 74 | query: { 75 | limit: 10000, 76 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]'), 77 | }, 78 | }, 79 | ], 80 | }, 81 | }; 82 | -------------------------------------------------------------------------------- /frontend/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var webpack = require('webpack') 3 | var config = require('../config') 4 | var merge = require('webpack-merge') 5 | var baseWebpackConfig = require('./webpack.base.conf') 6 | var HtmlWebpackPlugin = require('html-webpack-plugin') 7 | var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 8 | 9 | // add hot-reload related code to entry chunks 10 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 11 | baseWebpackConfig.entry[name] = ['./frontend/build/dev-client'].concat(baseWebpackConfig.entry[name]) 12 | }) 13 | 14 | module.exports = merge(baseWebpackConfig, { 15 | module: { 16 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) 17 | }, 18 | devtool: 'source-map', 19 | plugins: [ 20 | new webpack.DefinePlugin({ 21 | 'process.env': config.dev.env 22 | }), 23 | new webpack.ProvidePlugin({ 24 | fetch: 'imports-loader?this=>global!exports-loader?global.fetch!whatwg-fetch', 25 | }), 26 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 27 | new webpack.HotModuleReplacementPlugin(), 28 | new webpack.NoEmitOnErrorsPlugin(), 29 | // https://github.com/ampedandwired/html-webpack-plugin 30 | new HtmlWebpackPlugin({ 31 | filename: 'index.html', 32 | template: 'frontend/index.html', 33 | inject: true 34 | }), 35 | new FriendlyErrorsPlugin() 36 | ] 37 | }) 38 | -------------------------------------------------------------------------------- /frontend/build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var utils = require('./utils') 3 | var webpack = require('webpack') 4 | var config = require('../config') 5 | var merge = require('webpack-merge') 6 | var baseWebpackConfig = require('./webpack.base.conf') 7 | var HtmlWebpackPlugin = require('html-webpack-plugin') 8 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 9 | var env = config.build.env 10 | 11 | var webpackConfig = merge(baseWebpackConfig, { 12 | module: { 13 | rules: utils.styleLoaders({ 14 | sourceMap: config.build.productionSourceMap, 15 | extract: true 16 | }) 17 | }, 18 | devtool: config.build.productionSourceMap ? '#source-map' : false, 19 | output: { 20 | path: config.build.assetsRoot, 21 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 22 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 23 | }, 24 | plugins: [ 25 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 26 | new webpack.DefinePlugin({ 27 | 'process.env': env 28 | }), 29 | new webpack.ProvidePlugin({ 30 | fetch: 'imports-loader?this=>global!exports-loader?global.fetch!whatwg-fetch', 31 | }), 32 | new webpack.optimize.UglifyJsPlugin({ 33 | compress: { 34 | warnings: false 35 | } 36 | }), 37 | // extract css into its own file 38 | new ExtractTextPlugin(utils.assetsPath('css/[name].[contenthash].css')), 39 | // generate dist index.html with correct asset hash for caching. 40 | // you can customize output by editing /index.html 41 | // see https://github.com/ampedandwired/html-webpack-plugin 42 | new HtmlWebpackPlugin({ 43 | filename: config.build.index, 44 | template: 'frontend/index.html', 45 | inject: true, 46 | minify: { 47 | removeComments: true, 48 | collapseWhitespace: true, 49 | removeAttributeQuotes: true 50 | // more options: 51 | // https://github.com/kangax/html-minifier#options-quick-reference 52 | }, 53 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 54 | chunksSortMode: 'dependency' 55 | }), 56 | // split vendor js into its own file 57 | new webpack.optimize.CommonsChunkPlugin({ 58 | name: 'vendor', 59 | minChunks: function (module, count) { 60 | // any required modules inside node_modules are extracted to vendor 61 | return ( 62 | module.resource && 63 | /\.js$/.test(module.resource) && 64 | module.resource.indexOf( 65 | path.join(__dirname, '../node_modules') 66 | ) === 0 67 | ) 68 | } 69 | }), 70 | // extract webpack runtime and module manifest to its own file in order to 71 | // prevent vendor hash from being updated whenever app bundle is updated 72 | new webpack.optimize.CommonsChunkPlugin({ 73 | name: 'manifest', 74 | chunks: ['vendor'] 75 | }) 76 | ] 77 | }) 78 | 79 | if (config.build.productionGzip) { 80 | var CompressionWebpackPlugin = require('compression-webpack-plugin') 81 | 82 | webpackConfig.plugins.push( 83 | new CompressionWebpackPlugin({ 84 | asset: '[path].gz[query]', 85 | algorithm: 'gzip', 86 | test: new RegExp( 87 | '\\.(' + 88 | config.build.productionGzipExtensions.join('|') + 89 | ')$' 90 | ), 91 | threshold: 10240, 92 | minRatio: 0.8 93 | }) 94 | ) 95 | } 96 | 97 | if (config.build.bundleAnalyzerReport) { 98 | var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 99 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 100 | } 101 | 102 | module.exports = webpackConfig 103 | -------------------------------------------------------------------------------- /frontend/config/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var prodEnv = require('./prod.env') 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"' 6 | }) 7 | -------------------------------------------------------------------------------- /frontend/config/index.js: -------------------------------------------------------------------------------- 1 | // see http://vuejs-templates.github.io/webpack for documentation. 2 | var path = require('path') 3 | 4 | module.exports = { 5 | build: { 6 | env: require('./prod.env'), 7 | index: path.resolve(__dirname, '../../app/views/web/welcome/index.html'), 8 | assetsRoot: path.resolve(__dirname, '../../public'), 9 | assetsSubDirectory: 'static', 10 | assetsPublicPath: '/', 11 | productionSourceMap: true, 12 | // Gzip off by default as many popular static hosts such as 13 | // Surge or Netlify already gzip all static assets for you. 14 | // Before setting to `true`, make sure to: 15 | // npm install --save-dev compression-webpack-plugin 16 | productionGzip: false, 17 | productionGzipExtensions: ['js', 'css'], 18 | // Run the build command with an extra argument to 19 | // View the bundle analyzer report after build finishes: 20 | // `npm run build --report` 21 | // Set to `true` or `false` to always turn it on or off 22 | bundleAnalyzerReport: process.env.npm_config_report 23 | }, 24 | dev: { 25 | env: require('./dev.env'), 26 | port: 8080, 27 | autoOpenBrowser: true, 28 | assetsSubDirectory: 'static', 29 | assetsPublicPath: '/', 30 | proxyTable: { 31 | '/api': { 32 | target: 'http://localhost:3000', 33 | changeOrigin: true 34 | } 35 | }, 36 | // CSS Sourcemaps off by default because relative paths are "buggy" 37 | // with this option, according to the CSS-Loader README 38 | // (https://github.com/webpack/css-loader#sourcemaps) 39 | // In our experience, they generally work as expected, 40 | // just be aware of this issue when enabling this option. 41 | cssSourceMap: false 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /frontend/config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"' 3 | } 4 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Обедер 7 | 8 | 9 | 10 |
    11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 77 | -------------------------------------------------------------------------------- /frontend/src/api/apiClient.js: -------------------------------------------------------------------------------- 1 | import queryString from 'query-string'; 2 | import urljoin from 'url-join'; 3 | import humps from 'humps'; 4 | 5 | export const baseUrl = '/api'; 6 | 7 | export function request(url, method, urlParams = false, body = false) { 8 | let urlParamsPart = ''; 9 | if (urlParams) { 10 | const urlParamsAsString = queryString.stringify(urlParams); 11 | urlParamsPart = `?${urlParamsAsString}`; 12 | } 13 | const options = { method }; 14 | options.credentials = 'same-origin'; 15 | if (body) { 16 | const jsonWraper = humps.decamelizeKeys(body); 17 | const headers = new Headers({ 18 | 'Content-Type': 'application/json', 19 | }); 20 | options.body = JSON.stringify(jsonWraper); 21 | options.headers = headers; 22 | } 23 | return fetch(`${url}${urlParamsPart}`, options) 24 | .then(response => response.text()) 25 | .then(text => text && JSON.parse(text)) 26 | .then(json => humps.camelizeKeys(json)); 27 | } 28 | 29 | export function apiClientFactory(url) { 30 | return { 31 | get(page, pageSize) { 32 | return request(url, 'GET', 33 | { 34 | page, 35 | page_size: pageSize, 36 | }, 37 | ); 38 | }, 39 | getOne(id) { 40 | return request(urljoin(url, id), 'GET'); 41 | }, 42 | create(fields) { 43 | return request(url, 'POST', false, fields); 44 | }, 45 | remove(id) { 46 | return request(urljoin(url, id), 'DELETE'); 47 | }, 48 | save(id, fields) { 49 | return request(urljoin(url, id), 'PUT', false, fields); 50 | }, 51 | }; 52 | } 53 | 54 | -------------------------------------------------------------------------------- /frontend/src/api/users.js: -------------------------------------------------------------------------------- 1 | import urljoin from 'url-join'; 2 | 3 | import { baseUrl, request, apiClientFactory } from './apiClient'; 4 | 5 | const url = urljoin(baseUrl, 'user'); 6 | const apiWrapper = apiClientFactory(url); 7 | 8 | apiWrapper.getUser = () => apiWrapper.getOne(null); 9 | apiWrapper.saveUser = fields => apiWrapper.save(null, fields); 10 | 11 | apiWrapper.getMenus = () => request(urljoin(url, 'menus'), 'GET'); 12 | apiWrapper.setMenu = (id, { dishes, description, neem }) => { 13 | const menuUrl = urljoin(baseUrl, 'user_menus', id); 14 | const payload = { 15 | user_menu: { dishes, description, neem }, 16 | }; 17 | 18 | return request(menuUrl, 'PUT', false, payload); 19 | }; 20 | apiWrapper.setVote = (id, { dish_id, voted }) => { 21 | const menuUrl = urljoin(baseUrl, 'user_menu_votes', id); 22 | const payload = { 23 | user_menu_vote: { dish_id, voted }, 24 | }; 25 | 26 | return request(menuUrl, 'PUT', false, payload); 27 | }; 28 | 29 | export default apiWrapper; 30 | -------------------------------------------------------------------------------- /frontend/src/assets/styles/variables.css: -------------------------------------------------------------------------------- 1 | @custom-media --desktop only screen and (min-width: 800px); 2 | 3 | :root { 4 | --desktop-width: 800px; 5 | } 6 | 7 | @custom-media --tablet (max-width: 800px ); 8 | -------------------------------------------------------------------------------- /frontend/src/assets/styles/voter-theme.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --unvotedColor: #999; 3 | --unvotedColorBorder: #999; 4 | --votedDownColor: #e73b00; 5 | --votedDownColorBorder: #e73b00; 6 | --votedUpColor: #38B5C7; 7 | --votedUpColorBorder: #38B5C7; 8 | --textSuccess: #38B5C7; 9 | --textDanger: #e73b00; 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 66 | 67 | 132 | -------------------------------------------------------------------------------- /frontend/src/components/ImageModal.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 67 | 68 | 110 | -------------------------------------------------------------------------------- /frontend/src/components/Menu.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 72 | 73 | 74 | 117 | -------------------------------------------------------------------------------- /frontend/src/components/Menu/MenuVoter.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 39 | 121 | -------------------------------------------------------------------------------- /frontend/src/components/Switcher.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 42 | 43 | 113 | -------------------------------------------------------------------------------- /frontend/src/contants/dishTypes.js: -------------------------------------------------------------------------------- 1 | export default { 2 | side_dish: 'side_dish', 3 | soup: 'soup', 4 | main_dish: 'main_dish', 5 | salad: 'salad', 6 | separate_dish: 'separate_dish', 7 | }; 8 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue'; 4 | import App from './App'; 5 | import router from './router'; 6 | 7 | /* eslint-disable no-new */ 8 | new Vue({ 9 | el: '#app', 10 | router, 11 | template: '', 12 | components: { App }, 13 | }); 14 | -------------------------------------------------------------------------------- /frontend/src/presenters/MenuPresenter.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import dishTypes from 'contants/dishTypes'; 3 | 4 | moment.locale('ru'); 5 | 6 | export default { 7 | date(date) { 8 | return moment(date).format('dddd, D MMMM'); 9 | }, 10 | 11 | dishType(type) { 12 | switch (type) { 13 | case dishTypes.side_dish: 14 | return 'Гарниры'; 15 | case dishTypes.soup: 16 | return 'Супы'; 17 | case dishTypes.main_dish: 18 | return 'Основные блюда'; 19 | case dishTypes.salad: 20 | return 'Салаты'; 21 | case dishTypes.separate_dish: 22 | return 'Самостоятельное блюдо'; 23 | default: 24 | return type; 25 | } 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /frontend/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | 4 | import Menu from 'components/Menu'; 5 | 6 | Vue.use(Router); 7 | 8 | export default new Router({ 9 | mode: 'history', 10 | routes: [ 11 | { 12 | path: '/', 13 | name: 'Menu', 14 | component: Menu, 15 | }, 16 | { 17 | path: '/logout', 18 | beforeEnter: () => { 19 | // NOTE redirect to logout_path of rails 20 | window.location.replace('/logout'); 21 | }, 22 | }, 23 | ], 24 | }); 25 | -------------------------------------------------------------------------------- /frontend/static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Restream/obeder/f8fbf8d87842cfc8c5a23c8e4cf1d92584f31454/frontend/static/.gitkeep -------------------------------------------------------------------------------- /kube/db-deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: db 5 | namespace: obeder 6 | spec: 7 | replicas: 1 8 | template: 9 | metadata: 10 | name: db 11 | labels: 12 | app: db 13 | spec: 14 | containers: 15 | - name: db 16 | image: postgres 17 | env: 18 | - name: PGDATA 19 | value: /var/lib/postgresql/data/pgdata 20 | ports: 21 | - containerPort: 5432 22 | volumeMounts: 23 | - name: db-files 24 | mountPath: /var/lib/postgresql/data 25 | volumes: 26 | - name: db-files 27 | persistentVolumeClaim: 28 | claimName: db 29 | -------------------------------------------------------------------------------- /kube/db-service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: db 5 | namespace: obeder 6 | spec: 7 | clusterIP: None 8 | ports: 9 | - port: 5432 10 | targetPort: 5432 11 | selector: 12 | app: db 13 | -------------------------------------------------------------------------------- /kube/db-volume-claim.yml: -------------------------------------------------------------------------------- 1 | kind: PersistentVolumeClaim 2 | apiVersion: v1 3 | metadata: 4 | name: db 5 | namespace: obeder 6 | spec: 7 | accessModes: 8 | - ReadWriteMany 9 | resources: 10 | requests: 11 | storage: 1Gi 12 | -------------------------------------------------------------------------------- /kube/db-volume.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolume 3 | metadata: 4 | name: db 5 | namespace: obeder 6 | spec: 7 | capacity: 8 | storage: 1Gi 9 | accessModes: 10 | - ReadWriteMany 11 | rbd: 12 | monitors: 13 | - 10.73.0.44:6789 14 | - 10.73.0.45:6789 15 | - 10.73.0.42:6789 16 | pool: kubepool 17 | image: obeder-db-volume 18 | user: admin 19 | secretRef: 20 | name: ceph-secret 21 | fsType: ext4 22 | readOnly: false 23 | -------------------------------------------------------------------------------- /kube/namespace.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: obeder 5 | -------------------------------------------------------------------------------- /kube/redis-deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: redis 5 | namespace: obeder 6 | spec: 7 | replicas: 1 8 | template: 9 | metadata: 10 | labels: 11 | app: redis 12 | spec: 13 | containers: 14 | - name: redis 15 | image: redis 16 | ports: 17 | - containerPort: 6379 18 | -------------------------------------------------------------------------------- /kube/redis-service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: redis 5 | namespace: obeder 6 | spec: 7 | clusterIP: None 8 | ports: 9 | - port: 6379 10 | selector: 11 | app: redis 12 | -------------------------------------------------------------------------------- /kube/scheduler-deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: scheduler 5 | namespace: obeder 6 | spec: 7 | replicas: 1 8 | template: 9 | metadata: 10 | labels: 11 | app: obeder 12 | spec: 13 | containers: 14 | - name: scheduler 15 | image: "docker-registry.restr.im:5000/obeder/web:latest" 16 | command: ["/bin/sh","-c"] 17 | args: [ "clockwork clock.rb" ] 18 | env: 19 | - name: RAILS_ENV 20 | value: production 21 | - name: DATABASE_HOST 22 | value: "db" 23 | - name: DATABASE_PORT 24 | value: "5432" 25 | - name: DATABASE_USERNAME 26 | value: "postgres" 27 | - name: REDIS_HOST 28 | value: "redis" 29 | - name: REDIS_PORT 30 | value: "6379" 31 | - name: REDIS_DB 32 | value: "db0" 33 | - name: SECRET_KEY_BASE 34 | valueFrom: 35 | secretKeyRef: 36 | name: web 37 | key: secret_key_base 38 | - name: SENDGRID_USERNAME 39 | valueFrom: 40 | secretKeyRef: 41 | name: web 42 | key: sendgrid_username 43 | - name: SENDGRID_PASSWORD 44 | valueFrom: 45 | secretKeyRef: 46 | name: web 47 | key: sendgrid_password 48 | -------------------------------------------------------------------------------- /kube/sidekiq-deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: sidekiq 5 | namespace: obeder 6 | spec: 7 | replicas: 1 8 | template: 9 | metadata: 10 | labels: 11 | app: obeder 12 | spec: 13 | containers: 14 | - name: sidekiq 15 | image: "docker-registry.restr.im:5000/obeder/web:latest" 16 | command: ["/bin/sh","-c"] 17 | args: [ "sidekiq -C config/sidekiq.yml" ] 18 | env: 19 | - name: RAILS_ENV 20 | value: production 21 | - name: DATABASE_HOST 22 | value: "db" 23 | - name: DATABASE_PORT 24 | value: "5432" 25 | - name: DATABASE_USERNAME 26 | value: "postgres" 27 | - name: REDIS_HOST 28 | value: "redis" 29 | - name: REDIS_PORT 30 | value: "6379" 31 | - name: REDIS_DB 32 | value: "db0" 33 | - name: SECRET_KEY_BASE 34 | valueFrom: 35 | secretKeyRef: 36 | name: web 37 | key: secret_key_base 38 | - name: SENDGRID_USERNAME 39 | valueFrom: 40 | secretKeyRef: 41 | name: web 42 | key: sendgrid_username 43 | - name: SENDGRID_PASSWORD 44 | valueFrom: 45 | secretKeyRef: 46 | name: web 47 | key: sendgrid_password 48 | -------------------------------------------------------------------------------- /kube/web-config.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: web 5 | namespace: obeder 6 | data: 7 | nginx: | 8 | upstream puma { 9 | server localhost:3000; 10 | } 11 | 12 | server { 13 | listen 80; 14 | location /static/ { 15 | root /public/; 16 | } 17 | 18 | location /assets/ { 19 | root /public/; 20 | } 21 | 22 | location /uploads/ { 23 | root /public/; 24 | } 25 | 26 | location @obeder { 27 | proxy_http_version 1.1; 28 | proxy_pass http://puma; 29 | proxy_set_header Host $host; 30 | proxy_set_header X-Real-IP $remote_addr; 31 | proxy_set_header X-Forwarded-For $remote_addr; 32 | proxy_redirect off; 33 | } 34 | 35 | location / { 36 | try_files $uri @obeder; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /kube/web-deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: web 5 | namespace: obeder 6 | spec: 7 | replicas: 1 8 | template: 9 | metadata: 10 | labels: 11 | app: obeder 12 | spec: 13 | containers: 14 | - name: obeder 15 | image: "docker-registry.restr.im:5000/obeder/web:latest" 16 | command: ["/bin/sh","-c"] 17 | args: [ "cp -r public/ /; rake db:migrate && rails s -b '0.0.0.0'" ] 18 | env: 19 | - name: RAILS_ENV 20 | value: production 21 | - name: DATABASE_HOST 22 | value: "db" 23 | - name: DATABASE_PORT 24 | value: "5432" 25 | - name: DATABASE_USERNAME 26 | value: "postgres" 27 | - name: REDIS_HOST 28 | value: "redis" 29 | - name: REDIS_PORT 30 | value: "6379" 31 | - name: REDIS_DB 32 | value: "db0" 33 | - name: RAILS_LOG_TO_STDOUT 34 | value: "true" 35 | - name: SECRET_KEY_BASE 36 | valueFrom: 37 | secretKeyRef: 38 | name: web 39 | key: secret_key_base 40 | - name: SENDGRID_USERNAME 41 | valueFrom: 42 | secretKeyRef: 43 | name: web 44 | key: sendgrid_username 45 | - name: SENDGRID_PASSWORD 46 | valueFrom: 47 | secretKeyRef: 48 | name: web 49 | key: sendgrid_password 50 | ports: 51 | - containerPort: 3000 52 | volumeMounts: 53 | - name: static 54 | mountPath: /public 55 | - name: uploads 56 | mountPath: /app/public/uploads 57 | - name: nginx 58 | image: nginx 59 | ports: 60 | - containerPort: 80 61 | volumeMounts: 62 | - name: config 63 | mountPath: /etc/nginx/conf.d 64 | - name: static 65 | mountPath: /public 66 | - name: uploads 67 | mountPath: /public/uploads 68 | volumes: 69 | - name: config 70 | configMap: 71 | name: web 72 | items: 73 | - key: nginx 74 | path: default.conf 75 | - name: static 76 | emptyDir: {} 77 | - name: uploads 78 | persistentVolumeClaim: 79 | claimName: web 80 | -------------------------------------------------------------------------------- /kube/web-ingress.yml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | annotations: 5 | ipsNeeded: "1" 6 | name: web 7 | namespace: obeder 8 | spec: 9 | backend: 10 | serviceName: web 11 | servicePort: 1 12 | -------------------------------------------------------------------------------- /kube/web-service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: web 5 | namespace: obeder 6 | spec: 7 | ports: 8 | - port: 80 9 | selector: 10 | app: obeder 11 | -------------------------------------------------------------------------------- /kube/web-volume-claim.yml: -------------------------------------------------------------------------------- 1 | kind: PersistentVolumeClaim 2 | apiVersion: v1 3 | metadata: 4 | name: web 5 | namespace: obeder 6 | spec: 7 | accessModes: 8 | - ReadWriteMany 9 | resources: 10 | requests: 11 | storage: 1Gi 12 | -------------------------------------------------------------------------------- /kube/web-volume.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolume 3 | metadata: 4 | name: web 5 | namespace: obeder 6 | spec: 7 | capacity: 8 | storage: 1Gi 9 | accessModes: 10 | - ReadWriteMany 11 | rbd: 12 | monitors: 13 | - 10.73.0.44:6789 14 | - 10.73.0.45:6789 15 | - 10.73.0.42:6789 16 | pool: kubepool 17 | image: obeder-uploads-volume 18 | user: admin 19 | secretRef: 20 | name: ceph-secret 21 | fsType: ext4 22 | readOnly: false 23 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Restream/obeder/f8fbf8d87842cfc8c5a23c8e4cf1d92584f31454/lib/assets/.keep -------------------------------------------------------------------------------- /lib/flash_helper.rb: -------------------------------------------------------------------------------- 1 | module FlashHelper 2 | def f(key, options = {}) 3 | scope = [:flash] 4 | scope << params[:controller].split('/') 5 | scope << params[:action] 6 | 7 | msg = I18n.t(key, scope: scope) 8 | if options[:now] 9 | flash.now[key] = msg 10 | else 11 | flash[key] = msg 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Restream/obeder/f8fbf8d87842cfc8c5a23c8e4cf1d92584f31454/lib/tasks/.keep -------------------------------------------------------------------------------- /lib/tasks/users.rake: -------------------------------------------------------------------------------- 1 | namespace :users do 2 | desc 'Sending emails to inactive users for setup password' 3 | task send_change_password_emails_to_inactive_users: :environment do 4 | users = User.inactive 5 | users.each { |user| UserMailer.change_password(user).deliver } 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/templates/haml/scaffold/_form.html.haml: -------------------------------------------------------------------------------- 1 | = simple_form_for(@<%= singular_table_name %>) do |f| 2 | = f.error_notification 3 | 4 | .form-inputs 5 | <%- attributes.each do |attribute| -%> 6 | = f.<%= attribute.reference? ? :association : :input %> :<%= attribute.name %> 7 | <%- end -%> 8 | 9 | .form-actions 10 | = f.button :submit 11 | -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Restream/obeder/f8fbf8d87842cfc8c5a23c8e4cf1d92584f31454/log/.keep -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obeder", 3 | "version": "1.0.0", 4 | "description": "Super obeder", 5 | "author": "Pavel Beketov ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node frontend/build/dev-server.js", 9 | "build": "node frontend/build/build.js", 10 | "lint": "eslint --ext .js,.vue frontend/src", 11 | "webpack": "webpack --config frontend/build/webpack.prod.conf.js --display-error-details" 12 | }, 13 | "dependencies": { 14 | "font-awesome": "^4.7.0", 15 | "hint.css": "^2.4.1", 16 | "exports-loader": "^0.6.4", 17 | "humps": "^2.0.0", 18 | "imports-loader": "^0.7.1", 19 | "lodash": "^4.17.4", 20 | "moment": "^2.17.1", 21 | "postcss-cssnext": "^2.9.0", 22 | "postcss-import": "^9.1.0", 23 | "query-string": "^4.3.1", 24 | "uglify-js": "^2.7.5", 25 | "url-join": "^1.1.0", 26 | "vue": "^2.1.10", 27 | "vue-hint.css": "^0.0.2", 28 | "vue-router": "^2.2.0", 29 | "whatwg-fetch": "^2.0.3" 30 | }, 31 | "devDependencies": { 32 | "autoprefixer": "^6.7.2", 33 | "babel-core": "^6.22.1", 34 | "babel-eslint": "^7.1.1", 35 | "babel-loader": "^6.2.10", 36 | "babel-plugin-transform-runtime": "^6.22.0", 37 | "babel-preset-es2015": "^6.22.0", 38 | "babel-preset-stage-2": "^6.22.0", 39 | "babel-register": "^6.22.0", 40 | "chalk": "^1.1.3", 41 | "connect-history-api-fallback": "^1.3.0", 42 | "css-loader": "^0.26.1", 43 | "eslint": "^3.14.1", 44 | "eslint-config-airbnb-base": "^11.0.1", 45 | "eslint-friendly-formatter": "^2.0.7", 46 | "eslint-import-resolver-webpack": "^0.8.1", 47 | "eslint-loader": "^1.6.1", 48 | "eslint-plugin-html": "^2.0.0", 49 | "eslint-plugin-import": "^2.2.0", 50 | "eventsource-polyfill": "^0.9.6", 51 | "express": "^4.14.1", 52 | "extract-text-webpack-plugin": "^2.0.0-rc.2", 53 | "file-loader": "^0.10.0", 54 | "friendly-errors-webpack-plugin": "^1.1.3", 55 | "function-bind": "^1.1.0", 56 | "html-webpack-plugin": "^2.28.0", 57 | "http-proxy-middleware": "^0.17.3", 58 | "json-loader": "^0.5.4", 59 | "opn": "^4.0.2", 60 | "ora": "^1.1.0", 61 | "semver": "^5.3.0", 62 | "shelljs": "^0.7.6", 63 | "url-loader": "^0.5.7", 64 | "vue-loader": "^10.3.0", 65 | "vue-style-loader": "^2.0.0", 66 | "vue-template-compiler": "^2.1.10", 67 | "webpack": "^2.2.1", 68 | "webpack-bundle-analyzer": "^2.2.1", 69 | "webpack-dev-middleware": "^1.10.0", 70 | "webpack-hot-middleware": "^2.16.1", 71 | "webpack-merge": "^2.6.1" 72 | }, 73 | "engines": { 74 | "node": ">= 4.0.0", 75 | "npm": ">= 3.0.0" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    The page you were looking for doesn't exist.

    62 |

    You may have mistyped the address or the page may have moved.

    63 |
    64 |

    If you are the application owner check the logs for more information.

    65 |
    66 | 67 | 68 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    The change you wanted was rejected.

    62 |

    Maybe you tried to change something you didn't have access to.

    63 |
    64 |

    If you are the application owner check the logs for more information.

    65 |
    66 | 67 | 68 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    We're sorry, but something went wrong.

    62 |
    63 |

    If you are the application owner check the logs for more information.

    64 |
    65 | 66 | 67 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Restream/obeder/f8fbf8d87842cfc8c5a23c8e4cf1d92584f31454/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Restream/obeder/f8fbf8d87842cfc8c5a23c8e4cf1d92584f31454/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/assets/.sprockets-manifest-e7137b478274418cb22c220cde5c97d7.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Restream/obeder/f8fbf8d87842cfc8c5a23c8e4cf1d92584f31454/public/assets/.sprockets-manifest-e7137b478274418cb22c220cde5c97d7.json -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Restream/obeder/f8fbf8d87842cfc8c5a23c8e4cf1d92584f31454/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /secrets.sample.env: -------------------------------------------------------------------------------- 1 | SENDGRID_USERNAME=username 2 | SENDGRID_PASSWORD=password 3 | -------------------------------------------------------------------------------- /test/controllers/api/admin/menus_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::Admin::MenusControllerTest < ActionController::TestCase 4 | setup do 5 | @menu = create :menu 6 | end 7 | 8 | test 'validate' do 9 | get :validate, params: { date: @menu.date.to_s } 10 | assert_response :success 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/controllers/api/dishes_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::DishesControllerTest < ActionController::TestCase 4 | setup do 5 | @dish = create :dish 6 | @dish_attrs = attributes_for :dish, :salad 7 | end 8 | 9 | test 'index' do 10 | get :index 11 | assert_response :success 12 | end 13 | 14 | test 'create' do 15 | post :create, params: { dish: @dish_attrs } 16 | assert_response :success 17 | 18 | assert { Dish.count == 2 } 19 | end 20 | 21 | test 'update' do 22 | put :update, params: { id: @dish, dish: @dish_attrs } 23 | assert_response :success 24 | end 25 | 26 | test 'destroy' do 27 | delete :destroy, params: { id: @dish } 28 | assert_response :success 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/controllers/api/user/menus_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::User::MenusControllerTest < ActionController::TestCase 4 | setup do 5 | @user = create :user 6 | sign_in @user 7 | end 8 | 9 | test 'index' do 10 | get :index 11 | assert_response :success 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/controllers/api/user_menu_votes_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::UserMenuVotesControllerTest < ActionController::TestCase 4 | setup do 5 | @user_menu = create :user_menu_with_user_menu_dishes 6 | @dish = @user_menu.dishes.first 7 | @menu_dish = create :menu_dish, menu_id: @user_menu.menu_id, dish_id: @dish.id 8 | 9 | sign_in @user_menu.user 10 | end 11 | 12 | test 'update vote up for dish' do 13 | assert_difference 'VoteUp.count' do 14 | put :update, params: { user_menu_id: @user_menu.id, user_menu_vote: { dish_id: @dish.id, voted: true } } 15 | assert_response :success 16 | end 17 | @dish.reload 18 | assert { @dish.vote_ups_count == 1 } 19 | assert { @dish.vote_downs_count.zero? } 20 | end 21 | 22 | test 'update vote down for dish' do 23 | assert_difference 'VoteDown.count' do 24 | put :update, params: { user_menu_id: @user_menu.id, user_menu_vote: { dish_id: @dish.id, voted: false } } 25 | assert_response :success 26 | end 27 | @dish.reload 28 | assert { @dish.vote_ups_count.zero? } 29 | assert { @dish.vote_downs_count == 1 } 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/controllers/api/user_menus_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::UserMenusControllerTest < ActionController::TestCase 4 | setup do 5 | @user_menu = create :user_menu, :with_dishes 6 | @user_menu_attrs = attributes_for :user_menu 7 | end 8 | 9 | test 'index' do 10 | get :index 11 | assert_response :success 12 | end 13 | 14 | test 'update' do 15 | @user_menu_attrs[:dishes] = attributes_for_list(:dish, 3) 16 | 17 | put :update, params: { id: @user_menu.id, user_menu: @user_menu_attrs } 18 | assert_response :success 19 | end 20 | 21 | test 'update remove dishes' do 22 | @user_menu_attrs[:dishes] = [''] 23 | 24 | put :update, params: { id: @user_menu.id, user_menu: @user_menu_attrs } 25 | assert_response :success 26 | assert { UserMenuDish.count.zero? } 27 | end 28 | 29 | test 'update remove dish' do 30 | @user_menu_attrs[:dishes] = [Dish.first.as_json] 31 | 32 | put :update, params: { id: @user_menu.id, user_menu: @user_menu_attrs } 33 | assert_response :success 34 | assert { UserMenuDish.count == 1 } 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/controllers/api/users_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::UsersControllerTest < ActionController::TestCase 4 | setup do 5 | @user = create :user, neem: false 6 | sign_in @user 7 | end 8 | 9 | test 'show' do 10 | get :show, params: { id: @user.id } 11 | assert_response :success 12 | end 13 | 14 | test 'update' do 15 | put :update, params: { id: @user.id, user: { neem: true } } 16 | assert_response :success 17 | assert { @user.reload.neem } 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/controllers/web/admin/dishes_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Web::Admin::DishesControllerTest < ActionController::TestCase 4 | setup do 5 | @user = create :user, :cook 6 | sign_in @user 7 | end 8 | 9 | test 'index' do 10 | get :index 11 | assert_response :success 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/controllers/web/admin/menus_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Web::Admin::MenusControllerTest < ActionController::TestCase 4 | setup do 5 | @menu = create :menu 6 | @user = create :user, :cook 7 | sign_in @user 8 | end 9 | 10 | test 'edit' do 11 | get :edit, params: { date: @menu.date.to_s } 12 | assert_response :success 13 | end 14 | 15 | test 'update' do 16 | put :update, params: { date: @menu.date, menu: { date: generate(:date) } } 17 | assert_response :redirect 18 | end 19 | 20 | test 'approve' do 21 | put :approve, params: { date: @menu.date.to_s } 22 | assert_response :redirect 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/controllers/web/admin/monthly_reports_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Web::Admin::MonthlyReportsControllerTest < ActionController::TestCase 4 | setup do 5 | @admin = create :user, :admin 6 | sign_in @admin 7 | end 8 | 9 | test 'index' do 10 | get :index, params: { date: Date.current } 11 | assert_response :success 12 | end 13 | 14 | test 'export' do 15 | get :export, params: { date: Date.current } 16 | assert_response :success 17 | assert { response.content_type == 'application/vnd.ms-excel' } 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/controllers/web/admin/users_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Web::Admin::UsersControllerTest < ActionController::TestCase 4 | setup do 5 | @user = create :user, :with_user_menu_dishes, neem: false 6 | @admin = create :user, :admin 7 | sign_in @admin 8 | @user_attrs = attributes_for :user 9 | end 10 | 11 | test 'index' do 12 | get :index 13 | assert_response :success 14 | end 15 | 16 | test 'new' do 17 | get :new 18 | assert_response :success 19 | end 20 | 21 | test 'create' do 22 | assert_difference('User.count', +1) do 23 | post :create, params: { user: @user_attrs } 24 | end 25 | assert_redirected_to admin_users_path 26 | end 27 | 28 | test 'edit' do 29 | get :edit, params: { id: @user.id } 30 | assert_response :success 31 | end 32 | 33 | test 'update' do 34 | new_email = generate(:email) 35 | put :update, params: { id: @user.id, user: { email: new_email } } 36 | assert { @user.reload.email = new_email } 37 | assert_response :redirect 38 | end 39 | 40 | test 'destroy' do 41 | assert_difference('User.count', -1) do 42 | delete :destroy, params: { id: @user.id } 43 | end 44 | assert_redirected_to admin_users_path 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/controllers/web/remind_passwords_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Web::RemindPasswordsControllerTest < ActionController::TestCase 4 | setup do 5 | @user = create :user 6 | end 7 | 8 | test 'edit' do 9 | get :edit 10 | assert_response :success 11 | end 12 | 13 | test 'update' do 14 | post :update, params: { user_remind_password_type: { email: @user.email } } 15 | assert_response :redirect 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/controllers/web/sessions_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Web::SessionsControllerTest < ActionController::TestCase 4 | setup do 5 | @user = create :user 6 | end 7 | 8 | test 'new' do 9 | get :new 10 | assert_response :success 11 | end 12 | 13 | test 'create' do 14 | post :create, params: { user_sign_in_type: { email: @user.email, password: @user.password } } 15 | assert_response :redirect 16 | end 17 | 18 | test 'destroy' do 19 | sign_in @user 20 | 21 | delete :destroy 22 | assert_response :redirect 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/controllers/web/user/passwords_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Web::User::PasswordsControllerTest < ActionController::TestCase 4 | setup do 5 | @user = create :user, :with_user_menu_dishes 6 | end 7 | 8 | test 'edit' do 9 | get :edit, params: { user_id: @user.id } 10 | assert_response :success 11 | end 12 | 13 | test 'update' do 14 | new_password = generate(:string) 15 | put :update, params: { user_id: @user.id, user_password_edit_type: { password: new_password, password_confirmation: new_password } } 16 | assert_redirected_to login_path 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/controllers/web/welcome_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Web::WelcomeControllerTest < ActionController::TestCase 4 | setup do 5 | @user = create :user 6 | sign_in @user 7 | end 8 | 9 | test 'index' do 10 | get :index 11 | assert_response :success 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/factories/dishes.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :dish do 3 | name 4 | description 5 | image { generate :image } 6 | dish_type :soup 7 | 8 | trait :salad do 9 | dish_type :salad 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/factories/files/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Restream/obeder/f8fbf8d87842cfc8c5a23c8e4cf1d92584f31454/test/factories/files/.keep -------------------------------------------------------------------------------- /test/factories/files/dish.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Restream/obeder/f8fbf8d87842cfc8c5a23c8e4cf1d92584f31454/test/factories/files/dish.jpg -------------------------------------------------------------------------------- /test/factories/menu_dishes.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :menu_dish do 3 | dish 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/factories/menus.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :menu do 3 | date 4 | 5 | trait :approved do 6 | aasm_state :approved 7 | end 8 | 9 | trait :published do 10 | aasm_state :published 11 | end 12 | 13 | trait :with_dish do 14 | menu_dishes { create :menu_dish } 15 | end 16 | 17 | trait :with_dishes do 18 | menu_dishes { create_list :menu_dish, 3 } 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/factories/sequences.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | sequence :string, aliases: [:name, :description] do |n| 3 | "string-#{n}" 4 | end 5 | 6 | sequence :email do |n| 7 | "example-#{n}@restream.rt.ru" 8 | end 9 | 10 | sequence :integer do |n| 11 | n 12 | end 13 | 14 | sequence :float do |n| 15 | n.to_f + 0.5 16 | end 17 | 18 | sequence :date do |n| 19 | Date.current + n.days 20 | end 21 | 22 | sequence :image do 23 | Rack::Test::UploadedFile.new(File.join(Rails.root, 'test', 'factories', 'files', 'dish.jpg'), 'image/jpg') 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/factories/user_menu_dishes.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :user_menu_dish do 3 | dish 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/factories/user_menus.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :user_menu do 3 | description 4 | user 5 | menu 6 | 7 | trait :with_dish do 8 | after(:create) do |user_menu| 9 | create :user_menu_dish, user_menu: user_menu 10 | end 11 | end 12 | 13 | trait :with_dishes do 14 | after(:create) do |user_menu| 15 | create_list :user_menu_dish, 3, user_menu: user_menu 16 | end 17 | end 18 | 19 | factory :user_menu_with_user_menu_dishes, traits: [:with_dishes] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/factories/users.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :user do 3 | name 4 | email { generate :email } 5 | password { generate :string } 6 | role 'user' 7 | description 8 | aasm_state 'active' 9 | 10 | trait :admin do 11 | role 'admin' 12 | end 13 | 14 | trait :cook do 15 | role 'cook' 16 | end 17 | 18 | trait :with_user_menu_dishes do 19 | after(:create) do |user| 20 | create_list :user_menu_with_user_menu_dishes, 3, user: user 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/mailers/previews/user_mailer_preview.rb: -------------------------------------------------------------------------------- 1 | # Preview all emails at http://localhost:3000/rails/mailers/user_mailer 2 | class UserMailerPreview < ActionMailer::Preview 3 | def change_password 4 | UserMailer.change_password(User.first) 5 | end 6 | 7 | def notify_menu_changed 8 | UserMailer.notify_menu_changed(User.first, Menu.last) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/mailers/user_mailer_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UserMailerTest < ActionMailer::TestCase 4 | setup do 5 | @user = create :user 6 | end 7 | 8 | test 'change_password' do 9 | mail = UserMailer.change_password(@user) 10 | assert_equal [@user.email], mail.to 11 | assert_equal ['noreply@restream.rt.ru'], mail.from 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rails/test_help' 4 | require 'factory_girl_rails' 5 | require 'mocha/mini_test' 6 | require 'wrong' 7 | 8 | class ActiveSupport::TestCase 9 | include FactoryGirl::Syntax::Methods 10 | include Wrong 11 | include AuthHelper 12 | 13 | CarrierWave.root = 'test/factories/files' 14 | end 15 | -------------------------------------------------------------------------------- /test/workers/user_menu_freeze_worker_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UserMenuFreezeWorkerTest < ActiveSupport::TestCase 4 | setup do 5 | @em_user = create :user, neem: false 6 | @neem_user = create :user, neem: true 7 | menu = create :menu, date: Date.tomorrow 8 | 9 | create :user_menu, user: @em_user, menu: menu, neem: false 10 | create :user_menu, user: @neem_user, menu: menu, neem: false 11 | end 12 | 13 | test 'user_menus_freeze' do 14 | worker = UserMenuFreezeWorker.new 15 | worker.perform 16 | 17 | @em_user.reload 18 | assert @em_user.user_menus.where(neem: false).exists? 19 | 20 | @neem_user.reload 21 | assert @neem_user.user_menus.where(neem: true).exists? 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/workers/user_menus_create_worker_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | class UserMenusCreateWorkerTest < ActiveSupport::TestCase 3 | setup do 4 | @user = create :user 5 | @menu = create :menu, :approved 6 | end 7 | 8 | test 'user_menus_create' do 9 | worker = UserMenusCreateWorker.new 10 | worker.perform(@menu.id) 11 | 12 | @user.reload 13 | assert @user.user_menus.where(menu: @menu).exists? 14 | 15 | @menu.reload 16 | assert @menu.published? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Restream/obeder/f8fbf8d87842cfc8c5a23c8e4cf1d92584f31454/tmp/.keep -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Restream/obeder/f8fbf8d87842cfc8c5a23c8e4cf1d92584f31454/vendor/assets/javascripts/.keep -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Restream/obeder/f8fbf8d87842cfc8c5a23c8e4cf1d92584f31454/vendor/assets/stylesheets/.keep -------------------------------------------------------------------------------- /web.env: -------------------------------------------------------------------------------- 1 | DATABASE_HOST=db 2 | DATABASE_PORT=5432 3 | DATABASE_USERNAME=postgres 4 | DATABASE_PASSWORD= 5 | REDIS_HOST=redis 6 | REDIS_PORT=6379 7 | REDIS_DB=db0 8 | --------------------------------------------------------------------------------