diff --git a/.rubocop.yml b/.rubocop.yml index c68a2f1..3c01f3f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,11 +1,9 @@ -Rails: - Enabled: true - AllCops: Exclude: - 'bin/*' - 'node_modules/**/*' - 'db/schema.rb' + - 'app/models/achievements/**/*' Metrics/MethodLength: Exclude: @@ -14,13 +12,18 @@ Metrics/AbcSize: Exclude: - 'db/migrate/*' -Style/ClassAndModuleChildren: - Enabled: false -Style/RegexpLiteral: - EnforcedStyle: slashes +Rails: + Enabled: true +Rails/InverseOf: + Enabled: true + Style/Documentation: Exclude: - 'db/migrate/*' - -Rails/InverseOf: - Enabled: true \ No newline at end of file + - 'app/controllers/*' +Style/RegexpLiteral: + Enabled: true + EnforcedStyle: slashes +Style/ClassAndModuleChildren: + Enabled: true + EnforcedStyle: compact diff --git a/.travis.yml b/.travis.yml index 10960c0..b5df232 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,12 +2,6 @@ language: ruby rvm: - 2.4.4 -# We need Ubuntu 14 (Trusty) to use Postgres 9.6 -dist: trusty - -addons: - postgresql: 9.6 - cache: directories: - vendor/bundle diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..afe3509 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,35 @@ +# Contributing to riverrats.com.au + +Thanks for taking the time to contribute to the River Rats official website! Help is always appreciated. + + + +## Branches + +We follow the branching model outlined in the article [A successful Git branching model](https://nvie.com/posts/a-successful-git-branching-model/). If you can't be bothered to read all that, here's the important bits. + +There are two main branches: + + 1. `master`: The current production version of the website. + 2. `develop`: Where future releases are under construction. Unless you're writing a bug fix you'll likely want to start here. + +Then there are three types of supporting branches: + + * `feature-*`: A branch which introduces a new feature. e.g. `feature-player-nicknames`. + * `release-*`: A branch devoted to polishing up the project for a new release. e.g. `release-1.2.1`. Note that only project maintainers will create this kind of branch. + * `hotfix-*`: For fixing bugs in production. + + + +## Redis + +To run the development server you'll need to have an instance of Redis up and running. Jobs are managed by `sidekiq`, so you'll also need to start `sidekiq`. Once Redis is running you can achieve this with the command (from the root project directory): + +``` +$ bundle exec sidekiq -C config/sidekiq.yml +``` + + +## Elasticsearch + +If you want to use searching functionality you'll also need to install elasticsearch on your machine. diff --git a/Gemfile b/Gemfile index 531b6f4..e36a987 100644 --- a/Gemfile +++ b/Gemfile @@ -35,8 +35,6 @@ gem 'jbuilder', '~> 2.8' gem 'bcrypt', platform: :ruby group :development, :test do - # Call 'byebug' anywhere in the code to stop execution and get a debugger - # console gem 'byebug', platforms: %i[mri mingw x64_mingw] # Adds support for Capybara system testing and selenium driver gem 'capybara', '~> 3.12' @@ -45,7 +43,7 @@ end group :development do gem 'capistrano', '~> 3.11.0', require: false - gem 'capistrano-bundler', '~> 1.3', require: false + gem 'capistrano-bundler', '~> 1.4', require: false gem 'capistrano-passenger', require: false gem 'capistrano-rails', '~> 1.3', require: false gem 'capistrano-rvm', require: false @@ -68,9 +66,10 @@ gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] gem 'autoprefixer-rails' gem 'bootsnap', require: false gem 'connection_pool' +gem 'coveralls', require: false gem 'devise' gem 'elasticsearch' -gem 'friendly_id', '~> 5.1.0' +gem 'friendly_id', '~> 5.2.4' gem 'ice_cube' gem 'js-routes' gem 'kaminari' diff --git a/Gemfile.lock b/Gemfile.lock index aefd4a2..0815d33 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -48,7 +48,7 @@ GEM sshkit (>= 1.6.1, != 1.7.0) arel (9.0.0) ast (2.4.0) - autoprefixer-rails (9.1.4) + autoprefixer-rails (9.3.1) execjs bcrypt (3.1.12) bindex (0.5.0) @@ -61,7 +61,7 @@ GEM i18n rake (>= 10.0.0) sshkit (>= 1.9.0) - capistrano-bundler (1.3.0) + capistrano-bundler (1.4.0) capistrano (~> 3.1) sshkit (~> 1.2) capistrano-passenger (0.2.0) @@ -92,6 +92,12 @@ GEM coffee-script-source (1.12.2) concurrent-ruby (1.1.3) connection_pool (2.2.2) + coveralls (0.7.1) + multi_json (~> 1.3) + rest-client + simplecov (>= 0.7) + term-ansicolor + thor crass (1.0.4) devise (4.5.0) bcrypt (~> 3.0) @@ -99,6 +105,9 @@ GEM railties (>= 4.1.0, < 6.0) responders warden (~> 1.2.3) + docile (1.3.1) + domain_name (0.5.20180417) + unf (>= 0.0.5, < 1.0.0) elasticsearch (6.1.0) elasticsearch-api (= 6.1.0) elasticsearch-transport (= 6.1.0) @@ -112,11 +121,13 @@ GEM faraday (0.15.3) multipart-post (>= 1.2, < 3) ffi (1.9.25) - friendly_id (5.1.0) + friendly_id (5.2.4) activerecord (>= 4.0.0) globalid (0.4.1) activesupport (>= 4.2.0) hashie (3.6.0) + http-cookie (1.0.3) + domain_name (~> 0.5) i18n (1.1.1) concurrent-ruby (~> 1.0) ice_cube (0.16.3) @@ -127,6 +138,7 @@ GEM js-routes (1.4.4) railties (>= 3.2) sprockets-rails + json (2.1.0) kaminari (1.1.1) activesupport (>= 4.1.0) kaminari-actionview (= 1.1.1) @@ -170,6 +182,7 @@ GEM net-scp (1.2.1) net-ssh (>= 2.6.5) net-ssh (5.0.2) + netrc (0.11.0) nio4r (2.3.1) nokogiri (1.8.5) mini_portile2 (~> 2.3.0) @@ -181,7 +194,7 @@ GEM mimemagic (~> 0.3.0) terrapin (~> 0.6.0) parallel (1.12.1) - parser (2.5.1.2) + parser (2.5.3.0) ast (~> 2.4.0) pg (1.1.3) powerpack (0.1.2) @@ -229,14 +242,18 @@ GEM responders (2.4.0) actionpack (>= 4.2.0, < 5.3) railties (>= 4.2.0, < 5.3) - rubocop (0.59.2) + rest-client (2.0.2) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 4.0) + netrc (~> 0.8) + rubocop (0.60.0) jaro_winkler (~> 1.5.1) parallel (~> 1.10) parser (>= 2.5, != 2.5.1.1) powerpack (~> 0.1) rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) - unicode-display_width (~> 1.0, >= 1.0.1) + unicode-display_width (~> 1.4.0) ruby-progressbar (1.10.0) ruby_dep (1.5.0) rubyzip (1.2.2) @@ -258,13 +275,18 @@ GEM activemodel (>= 4.2) elasticsearch (>= 5) hashie - selenium-webdriver (3.14.0) + selenium-webdriver (3.141.0) childprocess (~> 0.5) - rubyzip (~> 1.2) + rubyzip (~> 1.2, >= 1.2.2) sidekiq (5.2.3) connection_pool (~> 2.2, >= 2.2.2) rack-protection (>= 1.5.0) redis (>= 3.3.5, < 5) + simplecov (0.16.1) + docile (~> 1.1) + json (>= 1.8, < 3) + simplecov-html (~> 0.10.0) + simplecov-html (0.10.2) spring (2.0.2) activesupport (>= 4.2) spring-watcher-listen (2.0.1) @@ -277,14 +299,17 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - sshkit (1.17.0) + sshkit (1.18.0) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) + term-ansicolor (1.6.0) + tins (~> 1.0) terrapin (0.6.0) climate_control (>= 0.0.3, < 1.0) thor (0.20.3) thread_safe (0.3.6) tilt (2.0.8) + tins (1.16.3) turbolinks (5.2.0) turbolinks-source (~> 5.2) turbolinks-source (5.2.0) @@ -292,6 +317,9 @@ GEM thread_safe (~> 0.1) uglifier (4.1.20) execjs (>= 0.3.0, < 3) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.5) unicode-display_width (1.4.0) unicorn (5.4.1) kgio (~> 2.6) @@ -322,16 +350,17 @@ DEPENDENCIES bootsnap byebug capistrano (~> 3.11.0) - capistrano-bundler (~> 1.3) + capistrano-bundler (~> 1.4) capistrano-passenger capistrano-rails (~> 1.3) capistrano-rvm capybara (~> 3.12) coffee-rails (~> 4.2) connection_pool + coveralls devise elasticsearch - friendly_id (~> 5.1.0) + friendly_id (~> 5.2.4) ice_cube jbuilder (~> 2.8) js-routes diff --git a/README.md b/README.md index e21e637..7c620d2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,22 @@ -# riverrats.com.au +# River Rats Poker League [![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=sean0x42/riverrats.com.au&identifier=126777895)](https://dependabot.com) +[![Coverage Status](https://coveralls.io/repos/github/sean0x42/riverrats.com.au/badge.svg?branch=master)](https://coveralls.io/github/sean0x42/riverrats.com.au?branch=master) [![Build Status](https://travis-ci.com/sean0x42/riverrats.com.au.svg?token=y4PzktMpXpMzBmaZHNGq&branch=develop)](https://travis-ci.com/sean0x42/riverrats.com.au) -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/a5bd9fd7908c4e838dd9824ed347b559)](https://www.codacy.com/app/sean_19/riverrats.com.au?utm_source=github.com&utm_medium=referral&utm_content=sean0x42/riverrats.com.au&utm_campaign=Badge_Grade) +[![GitLicense](https://gitlicense.com/badge/sean0x42/riverrats.com.au)](https://gitlicense.com/license/sean0x42/riverrats.com.au) -This is a commissioned Rails webapp for the River Rats Poker League, operating throughout the Mid-North Coast (Australia). +This is a commissioned Rails webapp for the River Rats Poker League, operating +throughout the Mid-North Coast (Australia). + +## Contributing + +Contributions are welcome, and encouraged! + + * If you have a feature request, or bug report, please [create an issue](https://github.com/sean0x42/riverrats.com.au/issues/new). + * If you would like to make a suggestion, you may fork the repository, or use inline suggestions on GitHub. + * For more complex contributions, please check out our [contribution guide](https://github.com/sean0x42/riverrats.com.au/blob/master/CONTRIBUTING.md). This will explain our approach to branching, as well as some helpful advice to get your development environment up and running. + +## Author + +This website is designed, developed, and maintained by +[Sean Bailey](https://www.seanbailey.io). diff --git a/Rakefile b/Rakefile index 488c551..2d79e51 100644 --- a/Rakefile +++ b/Rakefile @@ -1,7 +1,8 @@ # frozen_string_literal: true # Add your own tasks in files placed in lib/tasks ending in .rake, -# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. +# for example lib/tasks/capistrano.rake, and they will automatically be +# available to Rake. require_relative 'config/application' diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 6839d7f..c1c91da 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -13,4 +13,4 @@ //= require rails-ujs //= require turbolinks //= require local-time -//= require js-routes \ No newline at end of file +//= require js-routes diff --git a/app/assets/stylesheets/components/_achievements.scss b/app/assets/stylesheets/components/_achievements.scss index 3e64218..7e8250e 100644 --- a/app/assets/stylesheets/components/_achievements.scss +++ b/app/assets/stylesheets/components/_achievements.scss @@ -22,7 +22,7 @@ } .title { - font-weight: bold; + font-weight: 500; } } } diff --git a/app/assets/stylesheets/components/_button.scss b/app/assets/stylesheets/components/_button.scss index 7257c15..b581915 100644 --- a/app/assets/stylesheets/components/_button.scss +++ b/app/assets/stylesheets/components/_button.scss @@ -98,9 +98,9 @@ } .button-tertiary { - background: $grey-light; - color: lighten($text, 20%); - fill: lighten($text, 20%); + background: $grey; + color: lighten($text, 15%); + fill: lighten($text, 15%); margin: 0.5rem -0.25rem; &:hover { diff --git a/app/assets/stylesheets/components/_comments.scss b/app/assets/stylesheets/components/_comments.scss new file mode 100644 index 0000000..250ed82 --- /dev/null +++ b/app/assets/stylesheets/components/_comments.scss @@ -0,0 +1,129 @@ +.comment-form { + background: $grey-lighter; + border: 1px solid $grey; + border-radius: $border-radius; + max-width: 700px; + padding: 0.75rem; + + textarea { + margin: 0 0 0.5rem; + resize: vertical; + } + + .button { + padding: 12px 18px; + } + + .comment-form--footer { + align-items: flex-start; + display: flex; + justify-content: space-between; + + label { + color: lighten($text, 20%); + line-height: 1; + } + } +} + +.game-comments { + list-style-type: none; + margin: 1.5rem 0 0; + max-width: 80ch; + padding: 0; + position: relative; + + &::before { + background: $grey-dark; + bottom: 0; + content: ""; + left: 38.5px; + position: absolute; + top: 0; + width: 2px; + z-index: 1; + } + + .comment { + background: $white; + border: 1px solid $grey-dark; + border-radius: $border-radius; + margin: 2rem 0; + position: relative; + z-index: 2; + + &.deleted { + background: $red-lighter; + border-color: $red; + color: $red-lighter-inverted; + } + } + + .comment-header { + align-items: center; + background: $grey-lighter; + border-bottom: 1px solid $grey; + border-radius: $border-radius $border-radius 0 0; + color: lighten($text, 25%); + display: flex; + padding: 0.75rem 1rem; + + a { + color: inherit; + } + + .highlight { + color: $text; + } + + .flair { + background: $zurple-light; + color: $zurple-lighter-inverted; + } + } + + .deleted .comment-header { + background: $red-light; + border-color: $red; + color: $red-light-inverted; + + .highlight { + color: $red-inverted; + font-weight: 500; + } + + .flair { + background: $red-light-inverted; + color: $red-light; + } + } + + .comment-body { + padding: 0.75rem 1rem; + + p { + margin: 0; + } + } + + .comment-footer { + align-items: center; + display: flex; + list-style-type: none; + padding: 0.25rem 1rem 0.75rem; + + a, + span { + color: lighten($text, 25%); + margin-right: 0.5rem; + } + } + + .deleted .comment-footer { + a, + span { + color: $red-lighter-inverted; + text-decoration: line-through; + } + } +} diff --git a/app/assets/stylesheets/components/_flash-messages.scss b/app/assets/stylesheets/components/_flash-messages.scss index 0597f47..9a4e794 100644 --- a/app/assets/stylesheets/components/_flash-messages.scss +++ b/app/assets/stylesheets/components/_flash-messages.scss @@ -88,7 +88,7 @@ } .flash { - margin-top: 0; margin-bottom: 1rem; + margin-top: 0; } } diff --git a/app/assets/stylesheets/components/_header-menu.scss b/app/assets/stylesheets/components/_header-menu.scss index e76e662..f258825 100755 --- a/app/assets/stylesheets/components/_header-menu.scss +++ b/app/assets/stylesheets/components/_header-menu.scss @@ -26,7 +26,7 @@ left: 0; line-height: $body-line-height; margin-top: 0.8rem; - min-width: 275px; + min-width: 240px; position: absolute; right: 0; } @@ -40,7 +40,7 @@ width: 100%; } - a { + a:not(.notification-link) { align-items: center; color: $text; cursor: pointer; @@ -61,8 +61,8 @@ } .spacer { - border-bottom: 1px solid $grey; - margin: 0.75rem 1.25rem; + border-bottom: 2px solid $grey; + margin: 0.8rem 0; width: unset; } } @@ -82,6 +82,30 @@ flex-direction: column; } +// Define triangle +.header-menu::after, +.header-menu::before { + border-style: solid; + bottom: 100%; + content: ""; + display: block; + height: 0; + position: absolute; + width: 0; +} + +.header-menu::after { + border-color: $transparent $transparent $white; + border-width: 8px; + right: 25px; +} + +.header-menu::before { + border-color: $transparent $transparent $grey; + border-width: 9px; + right: 24px; +} + @media (min-width: 980px) { .header-link, .header-menu-wrapper .header-menu-trigger { diff --git a/app/assets/stylesheets/components/_header.scss b/app/assets/stylesheets/components/_header.scss index e49cc97..678dd06 100644 --- a/app/assets/stylesheets/components/_header.scss +++ b/app/assets/stylesheets/components/_header.scss @@ -86,9 +86,10 @@ fill: $darcula-inverted; } - svg { + svg.material-icons { fill: darken($darcula-inverted, 8%); height: 22px; + margin-bottom: 0; width: 22px; } diff --git a/app/assets/stylesheets/components/_modal-form.scss b/app/assets/stylesheets/components/_modal-form.scss index 63d1de0..24112f5 100644 --- a/app/assets/stylesheets/components/_modal-form.scss +++ b/app/assets/stylesheets/components/_modal-form.scss @@ -1,13 +1,14 @@ -.admin .modal-form { +.modal-wrapper .modal-form { + padding: 0; +} + +.modal-form { background: $white; border-radius: $border-radius; + margin-top: 2rem; max-width: $modal-width; padding: 2rem; width: 100%; -} - -.modal-form { - margin-top: 2rem; hr { border: 0; @@ -50,7 +51,17 @@ margin: 0; } - input { + input:not([type="number"]) { max-width: unset; } + + textarea { + min-height: 156px; + resize: vertical; + } + + span.highlight { + color: $text-bold; + font-weight: 500; + } } diff --git a/app/assets/stylesheets/components/_model-list.scss b/app/assets/stylesheets/components/_model-list.scss index 39de0d5..96b7475 100644 --- a/app/assets/stylesheets/components/_model-list.scss +++ b/app/assets/stylesheets/components/_model-list.scss @@ -61,6 +61,14 @@ .numeric { text-align: right; } + + .icon svg { + fill: lighten($text, 25%); + height: 22px; + line-height: 1; + vertical-align: middle; + width: 22px; + } } .model-list-footer { diff --git a/app/assets/stylesheets/components/_notifications.scss b/app/assets/stylesheets/components/_notifications.scss new file mode 100644 index 0000000..204e93f --- /dev/null +++ b/app/assets/stylesheets/components/_notifications.scss @@ -0,0 +1,107 @@ +$width: 350px; + +.header-menu.notification-area { + max-width: $width; + padding: 0.75rem 1rem 1rem; + width: $width; + + .notifications-header { + align-items: center; + border-bottom: 1px solid $grey; + display: flex; + justify-content: space-between; + padding: 0 0 0.5rem; + + p { + color: $text-bold; + font-weight: 500; + margin: 0; + } + } + + .clear-all { + color: lighten($text, 15%); + display: inline-block; + margin-left: auto; + + &:hover { + text-decoration: underline; + } + } + + .button-tertiary { + margin: 0; + white-space: nowrap; + } + + .empty { + align-items: center; + display: flex; + flex-direction: column; + padding: 2.25rem 0; + + svg { + $size: 75px; + fill: darken($grey-dark, 15%); + height: $size; + margin-bottom: 0.5rem; + width: $size; + } + + p { + color: lighten($text, 20%); + margin: 0; + } + } +} + +.notification-list { + max-height: 300px; + overflow-y: auto; + + li { + margin-bottom: 1rem; + } + + .notification-body, + a.notification-link { + border-left: 2px solid $zurple-light; + color: $text; + display: inline-block; + margin-left: 9px; + padding: 0; + padding-left: 15px; + } + + .notification-header { + align-items: center; + color: $text-bold; + display: flex; + + svg { + height: 20px; + margin-right: 0.4rem; + width: 20px; + } + } +} + +.model-list.not-list { + .read { + align-items: center; + display: flex; + + svg { + fill: lighten($text, 15%); + height: 22px; + line-height: 1; + margin-right: 0.4rem; + vertical-align: middle; + width: 22px; + } + + &:hover svg { + fill: $zurple-light; + } + } +} diff --git a/app/assets/stylesheets/components/_password-banner.scss b/app/assets/stylesheets/components/_password-banner.scss new file mode 100644 index 0000000..ea991e5 --- /dev/null +++ b/app/assets/stylesheets/components/_password-banner.scss @@ -0,0 +1,21 @@ +.password-banner { + align-items: center; + background: $white; + border: 2px solid $grey; + border-radius: $border-radius; + display: flex; + justify-content: space-between; + margin: 0 0 4rem; + padding: 1.5rem; + + h2 { + line-height: 1; + margin: 0 0 0.5rem; + } + + p { + line-height: 1.5; + margin: 0; + max-width: 70ch; + } +} diff --git a/app/assets/stylesheets/components/_player.scss b/app/assets/stylesheets/components/_player.scss index 0d5b049..2194735 100644 --- a/app/assets/stylesheets/components/_player.scss +++ b/app/assets/stylesheets/components/_player.scss @@ -7,7 +7,7 @@ border: 1px solid $grey; border-radius: $border-radius; margin-top: 1.5rem; - padding: 1.5rem; + padding: 1.25rem; > *:first-child { margin-top: 0; @@ -17,9 +17,10 @@ margin-bottom: 0; } + ul { list-style-type: none; - margin: 1.5rem 0; + margin: 0.25rem 0; padding: 0; } @@ -33,16 +34,18 @@ .achievement { align-items: center; display: flex; - margin-top: 0.75rem; + margin: 1rem 0; width: 100%; .image { flex-shrink: 0; - margin-right: 1rem; + margin-right: 0.75rem; } p { + color: $text; font-size: 15px; + line-height: 1.5; margin: 0; overflow: hidden; text-overflow: ellipsis; @@ -61,6 +64,33 @@ margin: 0; text-align: center; } + + .tickets { + align-items: center; + background: $grey-lighter; + border-top: 1px solid $grey; + margin: 1.5rem -1.25rem -1.25rem; + padding: 1.25rem; + + p { + align-items: center; + display: flex; + line-height: 1; + margin: 0; + } + } + + svg { + fill: $grey-darker; + height: 22px; + margin-right: 0.5rem; + width: 22px; + } + + .highlight { + color: $text; + font-weight: 500; + } } @media (min-width: 980px) { diff --git a/app/assets/stylesheets/components/_wrapper.scss b/app/assets/stylesheets/components/_wrapper.scss index af395cf..2a7c5f6 100644 --- a/app/assets/stylesheets/components/_wrapper.scss +++ b/app/assets/stylesheets/components/_wrapper.scss @@ -19,6 +19,10 @@ &.split-header { margin-bottom: 2.8rem; } + + &.with-modal { + max-width: $modal-width; + } } @media (min-width: 980px) { diff --git a/app/assets/stylesheets/components/admin/_game-form.scss b/app/assets/stylesheets/components/admin/_game-form.scss index 741e92b..5b809f4 100644 --- a/app/assets/stylesheets/components/admin/_game-form.scss +++ b/app/assets/stylesheets/components/admin/_game-form.scss @@ -1,5 +1,5 @@ .admin form.game-form { - max-width: $modal-width + 50px; + max-width: $modal-width + 80px; .js-sortable { outline: none; diff --git a/app/assets/stylesheets/components/admin/_navigation.scss b/app/assets/stylesheets/components/admin/_navigation.scss index 5692b82..0bb7bb8 100644 --- a/app/assets/stylesheets/components/admin/_navigation.scss +++ b/app/assets/stylesheets/components/admin/_navigation.scss @@ -98,9 +98,9 @@ bottom: unset; display: block; left: unset; - position: sticky; + position: relative; right: unset; - top: 100px; + top: unset; width: 270px; z-index: 1; @@ -113,7 +113,7 @@ } a { - padding: 6px 1rem; + padding: 5px 1rem; } } diff --git a/app/assets/stylesheets/elements/_inputs.scss b/app/assets/stylesheets/elements/_inputs.scss index 927348c..a756600 100644 --- a/app/assets/stylesheets/elements/_inputs.scss +++ b/app/assets/stylesheets/elements/_inputs.scss @@ -32,10 +32,10 @@ textarea { border: 1px solid $grey-dark; border-radius: 2px; color: $grey-light-inverted; - line-height: 1.8; + line-height: 1.6; margin-top: 0.25rem; - min-height: 120px; + min-height: 96px; min-width: 200px; - padding: 10px 14px; + padding: 8px 12px; width: 100%; } diff --git a/app/assets/stylesheets/settings/_colours.scss b/app/assets/stylesheets/settings/_colours.scss index f33a992..53972a1 100644 --- a/app/assets/stylesheets/settings/_colours.scss +++ b/app/assets/stylesheets/settings/_colours.scss @@ -6,7 +6,7 @@ $zurple-inverted: hsl(278, 14%, 99%); $zurple-light: hsl(278, 51%, 61%); $zurple-light-inverted: hsl(278, 14%, 95%); $zurple-lighter: hsl(278, 51%, 71%); -$zurple-lighter-inverted: hsl(278, 14%, 95%); +$zurple-lighter-inverted: hsl(278, 14%, 98%); // Darcula $darcula-dark: hsl(270, 21%, 13%); @@ -51,5 +51,15 @@ $grey-light: hsl(278, 20%, 97%); $grey-light-inverted: hsl(278, 21%, 40%); $grey-lighter: hsl(278, 18%, 98%); +// Red +$red-dark: hsl(11, 57%, 52%); +$red-dark-inverted: hsl(11, 15%, 99%); +$red: hsl(11, 70%, 57%); +$red-inverted: hsl(11, 15%, 97%); +$red-light: hsl(11, 70%, 65%); +$red-light-inverted: hsl(11, 15%, 97%); +$red-lighter: hsl(11, 70%, 73%); +$red-lighter-inverted: hsl(11, 15%, 99%); + $transparent: rgba(0, 0, 0, 0); $white: #ffffff; diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index 730dadd..9d0ed5d 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -# Application wide connection module ApplicationCable + # Application wide connection class Connection < ActionCable::Connection::Base end end diff --git a/app/controllers/achievements_controller.rb b/app/controllers/achievements_controller.rb index e0b57f0..0994fbc 100644 --- a/app/controllers/achievements_controller.rb +++ b/app/controllers/achievements_controller.rb @@ -4,7 +4,8 @@ class AchievementsController < ApplicationController # GET /player/:username/achievements def index - @player = Player.find_by! username: params[:player_username] + @player = Player.find_by!(username: params[:player_username]) + @achievements = @player.achievements.page(params[:page]) end # GET /player/:username/achievements/:id diff --git a/app/controllers/admin/achievements_controller.rb b/app/controllers/admin/achievements_controller.rb index 29d6a56..23698f0 100644 --- a/app/controllers/admin/achievements_controller.rb +++ b/app/controllers/admin/achievements_controller.rb @@ -5,30 +5,26 @@ # Controller for achievements in the admin scope class Admin::AchievementsController < ApplicationController layout 'admin' - - # noinspection RailsParamDefResolve before_action :authenticate_player! before_action :require_admin - # GET /admin/achievements/new + # GET /admin/players/:player_username/achievements/new def new - @achievement = Achievement.new + @player = Player.find_by!(username: params[:player_username]) + @achievement = @player.achievements.build end - # POST /admin/achievements + # POST /admin/players/:player_username/achievements def create - @achievement = Achievement.new achievement_params + @player = Player.find_by!(username: params[:player_username]) + @achievement = @player.achievements.build(achievement_params) if @achievement.save - flash[:success] = Struct::Flash.new( - t('admin.achievements.create.title'), - t('admin.achievements.create.body') - ) - redirect_to admin_players_path + record_action(:achievement, 'achievements.create', + achievement: @achievement.type, player: @player.username) + redirect_to admin_players_path, + notice: t('admin.achievements.create.flash') else - if params.key?(:achievement) && params[:achievement].key?(:player_id) - @player_name = Player.find(params[:achievement][:player_id]).full_name - end render 'new' end end @@ -36,6 +32,6 @@ def create private def achievement_params - params.require(:achievement).permit(:type, :player_id, :proof) + params.require(:achievement).permit(:type, :proof) end end diff --git a/app/controllers/admin/actions_controller.rb b/app/controllers/admin/actions_controller.rb new file mode 100644 index 0000000..0af4f48 --- /dev/null +++ b/app/controllers/admin/actions_controller.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# A controller for viewing actions history +class Admin::ActionsController < ApplicationController + layout 'admin' + before_action :authenticate_player! + before_action :require_admin + + # GET /admin/actions + def index + @actions = Action.order(created_at: :desc).page(params[:page]) + end +end diff --git a/app/controllers/admin/events_controller.rb b/app/controllers/admin/events_controller.rb index 6e15c94..483b322 100644 --- a/app/controllers/admin/events_controller.rb +++ b/app/controllers/admin/events_controller.rb @@ -5,8 +5,6 @@ # A controller for events in the admin scope class Admin::EventsController < ApplicationController layout 'admin' - - # noinspection RailsParamDefResolve before_action :authenticate_player! before_action :require_admin @@ -33,6 +31,7 @@ def create end if @event.save + record_action(:event, 'events.create', event: @event.title) redirect_to admin_events_path, notice: t('admin.events.create.flash') else respond_to do |format| @@ -52,6 +51,7 @@ def update @event = Event.find params[:id] if @event.update(edit_event_params) + record_action(:event, 'events.update', event: @event.title) redirect_to admin_events_path, notice: t('admin.events.update.flash') else render 'edit' @@ -63,6 +63,7 @@ def destroy @event = Event.find params[:id] @event.destroy_from_date(params[:from]) + record_action(:event, 'events.destroy', event: @event.title) redirect_to admin_events_path, notice: t('admin.events.destroy.flash') end diff --git a/app/controllers/admin/games_controller.rb b/app/controllers/admin/games_controller.rb index bff7d05..b40eaf9 100644 --- a/app/controllers/admin/games_controller.rb +++ b/app/controllers/admin/games_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'flash_message' - # A controller for games in the admin scope class Admin::GamesController < ApplicationController layout 'admin' @@ -21,10 +19,13 @@ def new end # POST /admin/games + # rubocop:disable Metrics/MethodLength, Metrics/AbcSize def create - @game = Game.new games_params + @game = Game.new(games_params) if @game.save + @game.award_tickets(params[:game][:tickets]) + record_action(:game, 'games.create', game: @game.id) redirect_to admin_games_path, notice: t('admin.games.create.flash') else respond_to do |format| @@ -33,17 +34,20 @@ def create end end end + # rubocop:enable Metrics/MethodLength, Metrics/AbcSize # GET /admin/games/:id/edit def edit - @game = Game.find params[:id] + @game = Game.find(params[:id]) end # POST /admin/games/:id + # rubocop:disable Metrics/AbcSize def update - @game = Game.find params[:id] + @game = Game.find(params[:id]) - if @game.update games_params + if @game.update(games_params) + record_action(:game, 'games.update', game: @game.id) redirect_to admin_games_path, notice: t('admin.games.update.flash') else respond_to do |format| @@ -52,12 +56,14 @@ def update end end end + # rubocop:enable Metrics/AbcSize # DELETE /admin/games/:id def destroy - @game = Game.find params[:id] + @game = Game.find(params[:id]) @game.destroy + record_action(:game, 'games.destroy', game: @game.id) redirect_to admin_games_path, notice: t('admin.games.destroy.flash') end diff --git a/app/controllers/admin/mail_controller.rb b/app/controllers/admin/mail_controller.rb index ff99bdb..e909e04 100644 --- a/app/controllers/admin/mail_controller.rb +++ b/app/controllers/admin/mail_controller.rb @@ -5,16 +5,16 @@ # A controller for mail in the admins cope class Admin::MailController < ApplicationController layout 'admin' - - # noinspection RailsParamDefResolve before_action :authenticate_player! before_action :require_admin # GET /admin/mail - def index; end + def index + respond_to :js + end # POST /admin/mail/players.csv - def show + def generate target = params.key?(:target) ? params[:target] : 'promotional' options = {} diff --git a/app/controllers/admin/players_controller.rb b/app/controllers/admin/players_controller.rb index 77e2d08..e535a72 100644 --- a/app/controllers/admin/players_controller.rb +++ b/app/controllers/admin/players_controller.rb @@ -5,14 +5,12 @@ # A controller for players in the admin scope class Admin::PlayersController < ApplicationController layout 'admin' - - # noinspection RailsParamDefResolve before_action :authenticate_player! before_action :require_admin # GET /admin/players def index - @players = Player.order(score: :desc).page params[:page] + @players = Player.order(score: :desc).page(params[:page]) end # GET /admin/players/:username @@ -26,13 +24,16 @@ def new end # POST /admin/players + # rubocop:disable Metrics/MethodLength, Metrics/AbcSize def create player_params = new_params player_params[:password] = Devise.friendly_token(8) @player = Player.new(player_params) + @player.password_changed = false # noinspection RailsChecklist01 if @player.save + record_action(:player, 'players.create', player: @player.username) PlayerMailer.welcome(@player.id, player_params[:password]).deliver_later redirect_to admin_players_path, notice: t('admin.players.create.flash') else @@ -42,6 +43,7 @@ def create end end end + # rubocop:enable Metrics/MethodLength, Metrics/AbcSize # GET /admin/players/:username/edit def edit @@ -49,35 +51,41 @@ def edit end # PATCH /admin/players/:username + # rubocop:disable Metrics/MethodLength, Metrics/AbcSize def update @player = Player.find_by!(username: params[:username]) if @player.update edit_params + record_action(:player, 'players.update', player: @player.username) redirect_to admin_players_path, notice: t('admin.players.update.flash') else # noinspection RubyResolve @player.username = @player.username_was if @player.username_changed? - render 'edit' + respond_to do |format| + format.html { render 'edit' } + format.js { render 'failure' } + end end end + # rubocop:enable Metrics/MethodLength, Metrics/AbcSize # DELETE /admin/players/:username def destroy @player = Player.find_by!(username: params[:username]) @player.destroy + record_action(:player, 'players.destroy', player: @player.username) redirect_to admin_players_path, notice: t('admin.players.destroy.flash') end private def new_params - params[:email] = nil if params.key?(:email) && params[:email].blank? - params.require(:player).permit(:first_name, :last_name, :email) + params.require(:player).permit(:first_name, :nickname, :last_name, :email) end def edit_params - params[:email] = nil if params.key?(:email) && params[:email].blank? - params.require(:player).permit(:username, :first_name, :last_name, :email) + params.require(:player).permit(:username, :nickname, :first_name, + :last_name, :email) end end diff --git a/app/controllers/admin/regions_controller.rb b/app/controllers/admin/regions_controller.rb index a63891d..0776dca 100644 --- a/app/controllers/admin/regions_controller.rb +++ b/app/controllers/admin/regions_controller.rb @@ -5,8 +5,6 @@ # A controller for regions in the admin scope class Admin::RegionsController < ApplicationController layout 'admin' - - # noinspection RailsParamDefResolve before_action :authenticate_player! before_action :require_admin @@ -25,6 +23,7 @@ def create @region = Region.new region_params if @region.save + record_action(:region, 'regions.create', region: @region.name) redirect_to admin_regions_path, notice: t('admin.regions.create.flash') else respond_to do |format| @@ -44,6 +43,7 @@ def update @region = Region.friendly.find params[:id] if @region.update region_params + record_action(:region, 'regions.update', region: @region.name) redirect_to admin_regions_path, notice: t('admin.regions.update.flash') else render 'edit' @@ -55,6 +55,7 @@ def destroy @region = Region.friendly.find params[:id] @region.destroy + record_action(:region, 'regions.destroy', region: @region.name) redirect_to admin_regions_path, notice: t('admin.regions.destroy.flash') end diff --git a/app/controllers/admin/tickets_controller.rb b/app/controllers/admin/tickets_controller.rb new file mode 100644 index 0000000..ac88e89 --- /dev/null +++ b/app/controllers/admin/tickets_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# A controller for managing tickets in the admin panel +class Admin::TicketsController < ApplicationController + layout 'admin' + respond_to :js + before_action :authenticate_player! + before_action :require_admin + + # GET /admin/players/:player_username/tickets + def edit + @player = Player.find_by!(username: params[:player_username]) + end + + # PATCH|PUT /admin/players/:player_username/tickets + def update + tickets = params[:tickets].to_i + @player = Player.find_by!(username: params[:player_username]) + @player.tickets += tickets + @player.save + record_action(:ticket, 'tickets.update', + tickets: tickets, player: @player.username) + redirect_to admin_players_path, notice: t('admin.tickets.update.flash') + end +end diff --git a/app/controllers/admin/venues_controller.rb b/app/controllers/admin/venues_controller.rb index f15cc17..c113c01 100644 --- a/app/controllers/admin/venues_controller.rb +++ b/app/controllers/admin/venues_controller.rb @@ -25,6 +25,7 @@ def create @venue = Venue.new venue_params if @venue.save + record_action(:venue, 'venues.create', venue: @venue.name) redirect_to admin_venues_path, notice: t('admin.venues.create.flash') else respond_to do |format| @@ -44,6 +45,7 @@ def update @venue = Venue.friendly.find params[:id] if @venue.update venue_params + record_action(:venue, 'venues.update', venue: @venue.name) redirect_to admin_venues_path, notice: t('admin.venues.update.flash') else render 'edit' @@ -55,6 +57,7 @@ def destroy @venue = Venue.friendly.find params[:id] @venue.destroy + record_action(:venue, 'venues.destroy', venue: @venue.name) redirect_to admin_venues_path, notice: t('admin.venues.destroy.flash') end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index c8e8d28..18f2fa0 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -19,4 +19,11 @@ def require_admin def after_sign_in_path_for(_resource) root_path end + + def record_action(action, translation, value_hash = {}) + Action.create( + player: current_player, action: action, + description: format(t("admin.#{translation}.action"), value_hash) + ) + end end diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb new file mode 100644 index 0000000..59c1124 --- /dev/null +++ b/app/controllers/comments_controller.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +# A controller for managing comments +class CommentsController < ApplicationController + before_action :authenticate_player! + respond_to :js + + # POST /games/:id/comments + def create + @game = Game.find(params[:game_id]) + @comment = Comment.new(comment_params) + + # Add relations + @comment.player = current_player + @comment.game = @game + + if @comment.save + @comment = @game.comments.build + render 'success' + else + render 'failure' + end + end + + # GET /games/:id/comments/:id + def edit + @game = Game.find(params[:game_id]) + @comment = @game.comments.where(deleted: false).find(params[:id]) + end + + # PATCH|PUT /games/:id/comments/:id + def update + @game = Game.find(params[:game_id]) + @comment = @game.comments.build + comment = current_player.comments.where(deleted: false).find(params[:id]) + + if comment.update(comment_params) + render 'edit_success' + else + render 'edit' + end + end + + # DELETE /games/:id/comments/:id + def destroy + @game = Game.find(params[:game_id]) + @comment = @game.comments.build + comment = @game.comments.where(deleted: false).find(params[:id]) + + if comment.player_id == current_player.id || current_player.admin + comment.update(deleted: true) + render 'destroy' + else + head :forbidden + end + end + + private + + def comment_params + params.require(:comment).permit(:body) + end +end diff --git a/app/controllers/games_controller.rb b/app/controllers/games_controller.rb index 13be1e5..f47b9e3 100644 --- a/app/controllers/games_controller.rb +++ b/app/controllers/games_controller.rb @@ -10,5 +10,10 @@ def index # GET /games/:id def show @game = Game.includes(:venue).find(params[:id]) + @comment = @game.comments.build + + respond_to do |format| + format.html + end end end diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb new file mode 100644 index 0000000..b07c4d0 --- /dev/null +++ b/app/controllers/notifications_controller.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# A controller for notifications +class NotificationsController < ApplicationController + # GET /notifications + def index + @notifications = if request.format.html? + current_player.notifications.page(params[:page]) + else + current_player.unread_notifications.limit(15) + end + end + + # DELETE /notifications/:id + def destroy + @notification = current_player.notifications.find(params[:id]) + @notification.destroy + + respond_to do |format| + format.html { redirect_to notifications_path } + format.js + end + end + + # PATCH/PUT /notifications/:notification_id/mark-read + def mark_read + @notification = current_player.notifications.find(params[:notification_id]) + @notification.update(read: !@notification.read) + + respond_to do |format| + format.html { redirect_to notifications_path } + format.js { render 'success' } + end + end + + # PATCH/PUT /notifications + def clear + # rubocop:disable Rails/SkipsModelValidations + current_player.notifications.update_all(read: true) + # rubocop:enable Rails/SkipsModelValidations + head :ok, content_type: 'text/html' + end +end diff --git a/app/controllers/players/registrations_controller.rb b/app/controllers/players/registrations_controller.rb index fd09d60..33e4278 100644 --- a/app/controllers/players/registrations_controller.rb +++ b/app/controllers/players/registrations_controller.rb @@ -22,9 +22,12 @@ def create # end # PUT /resource - # def update - # super - # end + def update + super + return if current_player.password_changed + + current_player.update(password_changed: true) + end # DELETE /resource # def destroy diff --git a/app/helpers/admin/actions_helper.rb b/app/helpers/admin/actions_helper.rb new file mode 100644 index 0000000..e7a1524 --- /dev/null +++ b/app/helpers/admin/actions_helper.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# A helper for the admin actions section +module Admin::ActionsHelper +end diff --git a/app/helpers/admin/tickets_helper.rb b/app/helpers/admin/tickets_helper.rb new file mode 100644 index 0000000..d80b394 --- /dev/null +++ b/app/helpers/admin/tickets_helper.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# A helper for tickets in the admin panel +module Admin::TicketsHelper +end diff --git a/app/helpers/comments_helper.rb b/app/helpers/comments_helper.rb new file mode 100644 index 0000000..a289024 --- /dev/null +++ b/app/helpers/comments_helper.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# A helper for comments +module CommentsHelper +end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb new file mode 100644 index 0000000..d18f631 --- /dev/null +++ b/app/helpers/notifications_helper.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# A helper for notifications +module NotificationsHelper +end diff --git a/app/javascript/packs/components/better_select.js b/app/javascript/packs/components/better_select.js index 818d71c..aa7e073 100644 --- a/app/javascript/packs/components/better_select.js +++ b/app/javascript/packs/components/better_select.js @@ -21,6 +21,31 @@ const getValueAsString = (select) => { return defaultValue; }; +/** + * An event handler, that should be fired whenever the + * trigger for a better select element is clicked. + * @param event Click event. + */ +const onSelectTriggerClick = (event) => { + // Buttons inside a form will submit their parent form when clicked => prevent default + event.preventDefault(); + const wrapper = event.target.parentNode; + + // Disable all selects + document.querySelectorAll(".better-select-wrapper[active]").forEach((select) => { + if (select !== wrapper) { + select.removeAttribute("active"); + } + }); + + // Toggle wrapper + if (wrapper.hasAttribute("active")) { + wrapper.removeAttribute("active"); + } else { + wrapper.setAttribute("active", ""); + } +}; + /** * Builds a custom select wrapper around an existing select element. * @param select Existing select element. @@ -60,31 +85,6 @@ const buildSelectWrapper = (select) => { select.parentNode.insertBefore(wrapper, select); }; -/** - * An event handler, that should be fired whenever the - * trigger for a better select element is clicked. - * @param event Click event. - */ -const onSelectTriggerClick = (event) => { - // Buttons inside a form will submit their parent form when clicked => prevent default - event.preventDefault(); - const wrapper = event.target.parentNode; - - // Disable all selects - document.querySelectorAll(".better-select-wrapper[active]").forEach((select) => { - if (select !== wrapper) { - select.removeAttribute("active"); - } - }); - - // Toggle wrapper - if (wrapper.hasAttribute("active")) { - wrapper.removeAttribute("active"); - } else { - wrapper.setAttribute("active", ""); - } -}; - /** * An event handler, that should be fired whenever an * option for a better select element is clicked. diff --git a/app/javascript/packs/game_editor.js b/app/javascript/packs/game_editor.js index 2dd7af8..896f1b9 100644 --- a/app/javascript/packs/game_editor.js +++ b/app/javascript/packs/game_editor.js @@ -30,7 +30,7 @@ function constructPlayerElement(player) { const element = document.createElement("li"); element.setAttribute("data-player-id", player.id); element.innerHTML = ``; - element.appendChild(player.asElement()); + element.appendChild(player.asElement(1)); // Construct remove button const button = document.createElement("button"); @@ -254,7 +254,6 @@ function onSubmitForm(event) { // Get fields const positionField = player.querySelector(".js-position-field"); const destroyField = player.querySelector(".js-destroy-field"); - console.log(player); // Set if (destroyField.value === true || destroyField.value === "true") { diff --git a/app/javascript/packs/navigation.js b/app/javascript/packs/navigation.js index 881fac5..80cf4f4 100644 --- a/app/javascript/packs/navigation.js +++ b/app/javascript/packs/navigation.js @@ -92,6 +92,7 @@ const init = () => { menuOverlay = document.querySelector(".header-menu-overlay"); document.querySelectorAll(".header-menu-wrapper").forEach(bindToWrapperEvents); + // Register events for movile admin navigation trigger const adminNavTrigger = document.querySelector(".mobile-admin-navigation-trigger"); if (adminNavTrigger !== null) { adminNavTrigger.addEventListener("click", () => { @@ -99,6 +100,14 @@ const init = () => { nav.setAttribute("active", ""); }); } + + // Register events for notification clear button + const clearButton = document.querySelector(".notification-area .clear-all"); + if (clearButton !== null) { + clearButton.addEventListener("click", () => { + disableMenu(); + }); + } }; -addEventListener("turbolinks:load", init); \ No newline at end of file +addEventListener("turbolinks:load", init); diff --git a/app/javascript/packs/player/player.js b/app/javascript/packs/player/player.js index 7c44967..0051c9a 100644 --- a/app/javascript/packs/player/player.js +++ b/app/javascript/packs/player/player.js @@ -27,9 +27,10 @@ export class Player { /** * An element representation of this player. + * @param position Player position (optional) * @returns {HTMLElement} Element containing information about this player. */ - asElement() { + asElement(positon = null) { // Init const player = document.createElement("div"); const nameSpan = document.createElement("span"); diff --git a/app/javascript/packs/polyfill/includes.js b/app/javascript/packs/polyfill/includes.js index 96e8366..a147278 100644 --- a/app/javascript/packs/polyfill/includes.js +++ b/app/javascript/packs/polyfill/includes.js @@ -2,7 +2,7 @@ if (!Array.prototype.includes) { Object.defineProperty(Array.prototype, "includes", { - value: function(searchElement, n = 0) { + value: (searchElement, n = 0) => { if (this === null) { throw new TypeError("\"this\" is null or undefined."); } diff --git a/app/models/action.rb b/app/models/action.rb new file mode 100644 index 0000000..68446e0 --- /dev/null +++ b/app/models/action.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# Represents a single administrative action in the admin panel +class Action < ApplicationRecord + belongs_to :player + + enum action: %i[player game event venue region achievement ticket comment] + + with_options presence: true do + validates :action + validates :description, length: { minimum: 3 } + end +end diff --git a/app/models/comment.rb b/app/models/comment.rb new file mode 100644 index 0000000..015357a --- /dev/null +++ b/app/models/comment.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# A comment on a game +class Comment < ApplicationRecord + default_scope { order(created_at: :asc) } + + after_create :send_notifications + + belongs_to :game + belongs_to :player + + validates :body, presence: true, length: { within: 3..440 } + validates :deleted, inclusion: { in: [true, false] } + + protected + + def send_notifications + CommentNotificationsWorker.perform_async(id) + end +end diff --git a/app/models/game.rb b/app/models/game.rb index 48552b3..aeab74b 100644 --- a/app/models/game.rb +++ b/app/models/game.rb @@ -2,9 +2,6 @@ # Represents a single game recorded by tournament directors class Game < ApplicationRecord - belongs_to :venue - belongs_to :season - default_scope { order(id: :desc) } searchkick callbacks: :async @@ -12,20 +9,22 @@ class Game < ApplicationRecord after_save :update_stats, :update_ranks after_destroy :update_stats, :update_ranks - has_many :games_players, - class_name: 'GamesPlayer', - dependent: :delete_all, - inverse_of: :game + belongs_to :venue + belongs_to :season + has_many :players, through: :games_players - has_many :referees, dependent: :delete_all, inverse_of: :game has_many :players, through: :referees - accepts_nested_attributes_for :games_players, - reject_if: :all_blank, - allow_destroy: true - accepts_nested_attributes_for :referees, - reject_if: :all_blank, - allow_destroy: true + with_options dependent: :delete_all, inverse_of: :game do + has_many :games_players, class_name: 'GamesPlayer' + has_many :referees + has_many :comments + end + + with_options reject_if: :all_blank, allow_destroy: true do + accepts_nested_attributes_for :games_players + accepts_nested_attributes_for :referees + end validates :venue, :season, :played_on, presence: true validate :player_count, :referee_count, :no_duplicate_players, @@ -51,6 +50,10 @@ def game_played_by Player.joins(:games_players).where(games_players: { game_id: id }) end + def award_tickets(tickets) + GameTicketsWorker.perform_async(id, tickets) unless tickets.nil? + end + private def player_count diff --git a/app/models/games_player.rb b/app/models/games_player.rb index 6b7977c..0e86d51 100644 --- a/app/models/games_player.rb +++ b/app/models/games_player.rb @@ -15,6 +15,7 @@ class GamesPlayer < ApplicationRecord before_save :calc_score after_save :update_stats after_destroy :update_stats + after_create :send_create_notification with_options presence: true do validates :game, :player @@ -46,4 +47,8 @@ def calc_score def update_stats CalculatePlayerStatsWorker.perform_async(player.id) end + + def send_create_notification + GameNotificationWorker.perform_in(10.seconds, id, player.id) + end end diff --git a/app/models/notification.rb b/app/models/notification.rb new file mode 100644 index 0000000..04cd339 --- /dev/null +++ b/app/models/notification.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# A single notification item. +class Notification < ApplicationRecord + default_scope { order(created_at: :desc) } + + enum icon: %i[game comment] + belongs_to :player + + validates :message, presence: true, length: { within: 5..200 } +end diff --git a/app/models/player.rb b/app/models/player.rb index 678ec5e..5ce9936 100644 --- a/app/models/player.rb +++ b/app/models/player.rb @@ -4,6 +4,7 @@ require 'username_lib' # Represents a single player +# rubocop:disable Metrics/ClassLength class Player < ApplicationRecord devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable @@ -12,81 +13,103 @@ class Player < ApplicationRecord searchkick callbacks: :async, word_start: %i[full_name username] + # Active record callbacks before_validation :gen_username, on: :create + # Relationships with_options dependent: :nullify, inverse_of: :player do - has_many :games_players, class_name: 'GamesPlayer' - has_many :players_venues, class_name: 'PlayersVenue' + has_many :games_players, class_name: 'GamesPlayer' + has_many :players_venues, class_name: 'PlayersVenue' has_many :players_regions, class_name: 'PlayersRegion' has_many :players_seasons, class_name: 'PlayersSeason' + has_many :comments + has_many :actions end - has_many :games, through: :games_players has_many :referees, dependent: :nullify - has_many :games, through: :referees - has_many :venues, through: :players_venues - has_many :regions, through: :players_regions - has_many :seasons, through: :players_seasons - has_many :achievements, dependent: :destroy + has_many :games, through: :games_players + has_many :games, through: :referees + has_many :venues, through: :players_venues + has_many :regions, through: :players_regions + has_many :seasons, through: :players_seasons + + with_options dependent: :destroy do + has_many :achievements + has_many :notifications + end attr_writer :login + # Validation with_options presence: true do validates :username, uniqueness: { case_sensitive: false }, length: { minimum: 2 }, format: { with: /\A[a-z0-9-]*\z/, - message: 'may use numbers, letters, underscores (_), and hyphens (-)' - } - validates :first_name, :last_name, - length: { maximum: 64 }, - format: { - with: /\A[a-zA-Z][a-zA-Z-]*[a-zA-Z]\z/, - message: 'may use letters and hyphens (-)' + message: 'may use numbers, letters, underscores (_), and '\ + 'hyphens (-)' } validates :notify_promotional, :notify_events end - validates :score, :games_played, :games_won, - numericality: { - only_integer: true, - greater_than_or_equal_to: 0 - } + with_options length: { maximum: 64 }, + format: { + with: /\A[A-Z][a-zA-Z-]*[a-z]\z/, + message: 'may use letters and hyphens (-), must start with an'\ + ' uppercase letter' + } do + validates :first_name, :last_name, presence: true + validates :nickname, allow_nil: true, allow_blank: true + end + + validates :score, :games_played, :games_won, :tickets, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } - validates :email, - format: { with: URI::MailTo::EMAIL_REGEXP }, - allow_nil: true, - allow_blank: true, - uniqueness: true + validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }, + allow_nil: true, allow_blank: true, uniqueness: true + + # Since validation by presence doesn't work for booleans + validates :password_changed, :developer, :admin, + inclusion: { in: [true, false] } def to_param username end + # Defines searchkick data def search_data { full_name: full_name, username: "@#{username}", - is_admin: admin?, - is_developer: developer? + is_admin: admin, + is_developer: developer } end + # Returns a human readable form of the players full name def full_name - "#{first_name} #{last_name}" + if nickname.nil? + "#{first_name} #{last_name}" + else + "#{first_name} '#{nickname}' #{last_name}" + end end def login @login || username || email end + # noinspection RubyClassMethodNamingConvention def self.find_for_database_authentication(warden_conditions) conditions = warden_conditions.dup if (login = conditions.delete(:login)) - where(conditions.to_h).where(['lower(username) = :value OR lower(email) = :value', { value: login.downcase }]).first + where(conditions.to_h).find_by([ + 'lower(username) = :value OR lower(email) = :value', + { value: login.downcase } + ]) elsif conditions.key?(:username) || conditions.key?(:email) - where(conditions.to_h).first + find_by(conditions.to_h) end end @@ -130,7 +153,7 @@ def self.to_csv def recent_games GamesPlayer.includes(game: [:venue]) .where(player: self) - .reorder(created_at: :desc).limit(25) + .reorder(created_at: :desc) end def season_player @@ -144,4 +167,34 @@ def self.recent(days = 30) def self.admins Player.where(admin: true).or(Player.where(developer: true)) end + + def unread_notifications + notifications.where(read: false) + end + + # Define custom setters which make blank attributes nil + %w[nickname email].each do |attribute| + define_method "#{attribute}=" do |value| + value = value.presence unless value.nil? + super(value) + end + end + + # Define custom setters which automatically titleize + %w[first_name last_name].each do |attribute| + define_method "#{attribute}=" do |value| + # We need to be sure we only capitalize the first char + if value.instance_of?(String) && value.present? + value = value[0].capitalize + value.slice(1..-1) + end + + super(value) + end + end + + def tickets=(value) + value = 0 if value.negative? + super(value) + end end +# rubocop:enable Metrics/ClassLength diff --git a/app/models/venue.rb b/app/models/venue.rb index 28b5600..af8ed12 100644 --- a/app/models/venue.rb +++ b/app/models/venue.rb @@ -14,6 +14,7 @@ class Venue < ApplicationRecord inverse_of: :venue has_many :players, through: :players_venues belongs_to :region + has_many :games, dependent: :nullify enum state: %i[ACT NSW NT QLD SA TAS VIC WA] diff --git a/app/views/achievements/_achievement.html.erb b/app/views/achievements/_achievement.html.erb new file mode 100644 index 0000000..094f920 --- /dev/null +++ b/app/views/achievements/_achievement.html.erb @@ -0,0 +1,12 @@ + + + <%= achievement.title %> + + + <%= achievement.description %> + + + <% if achievement.proof.present? -%> + <%= link_to 'View proof', achievement.proof.url(:full) %> + <% end -%> + diff --git a/app/views/achievements/index.html.erb b/app/views/achievements/index.html.erb index 898a221..2ae2b6d 100644 --- a/app/views/achievements/index.html.erb +++ b/app/views/achievements/index.html.erb @@ -5,4 +5,15 @@
Achievement List

<%= t('achievements.index.title') % { player: @player.full_name } %>

-
\ No newline at end of file + +
+ + <%= render collection: @achievements, partial: 'achievement' %> +
+
+ + + diff --git a/app/views/admin/_navigation.html.erb b/app/views/admin/_navigation.html.erb index 2c79099..b7be31d 100644 --- a/app/views/admin/_navigation.html.erb +++ b/app/views/admin/_navigation.html.erb @@ -50,17 +50,9 @@

Actions

<% if current_player.developer? -%> @@ -81,11 +81,11 @@ <% end -%> - \ No newline at end of file + diff --git a/app/views/admin/achievements/_form.html.erb b/app/views/admin/achievements/_form.html.erb index b6e1976..93d82f7 100644 --- a/app/views/admin/achievements/_form.html.erb +++ b/app/views/admin/achievements/_form.html.erb @@ -1,4 +1,7 @@ -<%= form_with model: [:admin, @achievement.becomes(Achievement)], html: { multipart: true }, class: 'modal-form' do |form| %> +<%= form_with model: @achievement.becomes(Achievement), + url: admin_player_achievements_path(@player), + html: { multipart: true }, + class: 'modal-form' do |form| %>
<%= form.label :type, 'Achievement' %>
@@ -12,16 +15,6 @@ ) %>
- -
- <%= form.label :player_id %>
- <%= field_errors @achievement, :player_id %> - <%= form.hidden_field :player_id %> -
- <%= form.text_field :player, class: 'player-input', name: nil, placeholder: 'Name or username', value: @player_name %> -
-
-
<%= form.label :proof %>
@@ -29,5 +22,7 @@ <%= form.file_field :proof %>
- <%= form.submit 'Award Achievement', class: 'button wide', data: { disable_with: 'Please wait...' } %> -<% end %> \ No newline at end of file + <%= form.submit 'Award Achievement', + class: 'button wide', + data: { disable_with: 'Please wait...' } %> +<% end %> diff --git a/app/views/admin/actions/_action.html.erb b/app/views/admin/actions/_action.html.erb new file mode 100644 index 0000000..842d584 --- /dev/null +++ b/app/views/admin/actions/_action.html.erb @@ -0,0 +1,32 @@ +<% player = action.player -%> + + + + <% case action.action -%> + <% when 'player' -%> + + <% when 'game' -%> + + <% when 'event' -%> + + <% when 'venue' -%> + + <% when 'region' -%> + + <% when 'achievement' -%> + + <% when 'ticket' -%> + + <% when 'comment' -%> + Comment + <% end -%> + + + + + <%= link_to player.full_name, edit_admin_player_path(player) %> <%= action.description %> + + + + <%= local_time_ago(action.created_at) %> + diff --git a/app/views/admin/actions/index.html.erb b/app/views/admin/actions/index.html.erb new file mode 100644 index 0000000..0830222 --- /dev/null +++ b/app/views/admin/actions/index.html.erb @@ -0,0 +1,15 @@ +<% content_for :title, t('admin.actions.index.title') -%> + +Admin Tools +

<%= t('admin.actions.index.title') %>

+ +
+ + <%= render @actions %> +
+
+ + diff --git a/app/views/admin/games/_form.html.erb b/app/views/admin/games/_form.html.erb index 974dc3a..c5e3e4c 100644 --- a/app/views/admin/games/_form.html.erb +++ b/app/views/admin/games/_form.html.erb @@ -1,5 +1,4 @@ <%= form_with model: [:admin, @game], class: 'modal-form game-form' do |form| %> -
<%= form.label :played_on %>
@@ -18,12 +17,24 @@
<%= form.label :season_id %>
<%= field_errors @game, :season_id %> - <%= form.collection_select :season_id, Season.all.order(start_at: :desc).limit(10), :id, :name %> + <%= form.collection_select :season_id, + Season.all.order(start_at: :desc).limit(10), + :id, :name %>
+ <% unless @game.persisted? %> + +
+ <%= form.label :tickets %>
+ <%= field_errors @game, :tickets %> + <%= form.number_field :tickets, value: 0, min: 0 %> +
+ <% end -%> +

<%= t('admin.games.new.add_players') %>

- <%= link_to 'Create player', new_admin_player_path, class: 'anchor', remote: true, data: { expects_modal: true } %> + <%= link_to 'Create player (opens in tab)', new_admin_player_path, class: 'anchor', + target: '_blank' %>
<%= field_errors @game, :games_players %> @@ -38,7 +49,7 @@
diff --git a/app/views/games/show.html.erb b/app/views/games/show.html.erb index ee17b29..86bf81b 100644 --- a/app/views/games/show.html.erb +++ b/app/views/games/show.html.erb @@ -1,18 +1,32 @@ -<%- content_for :title, t('games.show.title') % { game: @game.name, venue: @game.venue.name } %> -<%- content_for :description, t('games.show.description') % { game: @game.name } %> -<%- content_for :canonical_url, games_url %> +<% content_for :title, t('games.show.title') % { game: @game.name, venue: @game.venue.name } -%> +<% content_for :description, t('games.show.description') % { game: @game.name } -%> +<% content_for :canonical_url, games_url -%>
+ <%= render 'application/password_banner' %> + Game Log

<%= t('games.show.title') % { game: @game.name, venue: @game.venue.name } %>

<%- players = @game.paginated_players(params[:page]) %>
-
- - <%= render collection: players, partial: 'player' %> -
+
+
+ + <%= render collection: players, partial: 'player' %> +
+
+ + + + +
+ <%= render 'game_comments' %> +
@@ -56,4 +70,4 @@

-
\ No newline at end of file + diff --git a/app/views/landing/release_notes.html.erb b/app/views/landing/release_notes.html.erb index eca7312..1c7407c 100644 --- a/app/views/landing/release_notes.html.erb +++ b/app/views/landing/release_notes.html.erb @@ -3,17 +3,70 @@ <% content_for :canonical_url, release_notes_url -%>
+ <%= render 'application/password_banner' %> + Version History

<%= t('landing.release_notes.title') %>

+ +
+

1.2.0Decemeber 1st, 2018

+
    +
  • +
    new
    +

    A banner prompting players to change their password if it was automatically generated. See <%= link_to '#139', 'https://github.com/sean0x42/riverrats.com.au/pull/139', class: 'anchor' %>.

    +
  • +
  • +
    new
    +

    Tickets! Players can now be awarded tickets by administrators. See <%= link_to '#126', 'https://github.com/sean0x42/riverrats.com.au/pull/126', class: 'anchor' %>.

    +
  • +
  • +
    new
    +

    Admin actions are now recorded, and can be reviewed by other admins. See <%= link_to '#126', 'https://github.com/sean0x42/riverrats.com.au/pull/126', class: 'anchor' %>.

    +
  • +
  • +
    new
    +

    Players can now comment on games. See <%= link_to '#112', 'https://github.com/sean0x42/riverrats.com.au/pull/112', class: 'anchor' %>.

    +
  • +
  • +
    new
    +

    You'll now receive notifications for various things, such as playing in a game. See <%= link_to '#101', 'https://github.com/sean0x42/riverrats.com.au/pull/101', class: 'anchor' %>.

    +
  • +
  • +
    new
    +

    Admins can now give players nicknames. See <%= link_to '#100', 'https://github.com/sean0x42/riverrats.com.au/pull/100', class: 'anchor' %>.

    +
  • +
  • +
    new
    +

    Improved achievement views. See <%= link_to '#137', 'https://github.com/sean0x42/riverrats.com.au/pull/137', class: 'anchor' %>.

    +
  • +
  • +
    improved
    +

    Error pages (<%= link_to '404', '/404', class: 'anchor', target: '_blank' %>, <%= link_to '422', '/422', class: 'anchor', target: '_blank' %>, <%= link_to '500', '/500', class: 'anchor', target: '_blank' %>) have had a facelift. They should be more meaningful now. See <%= link_to '#138', 'https://github.com/sean0x42/riverrats.com.au/pull/138', class: 'anchor' %>.

    +
  • +
  • +
    improved
    +

    Some emails have had an aesthetic rework. See <%= link_to '#136', 'https://github.com/sean0x42/riverrats.com.au/pull/136', class: 'anchor' %>.

    +
  • +
  • +
    fixed
    +

    Fixed a bug with logging in, signing up, and editing account settings. See <%= link_to '#133', 'https://github.com/sean0x42/riverrats.com.au/pull/133', class: 'anchor' %> and <%= link_to '#139', 'https://github.com/sean0x42/riverrats.com.au/pull/139', class: 'anchor' %>.

    +
  • +
  • +
    fixed
    +

    Player's names are now capitalised appropriately. See <%= link_to '#15', 'https://github.com/sean0x42/riverrats.com.au/issues/15', class: 'anchor' %>.

    +
  • +
+
+

1.1.3October 27th, 2018

  • fixed
    -

    Fixed an issue preventing players from resetting their passwords #105.

    +

    Fixed an issue preventing players from resetting their passwords <%= link_to '#105', 'https://github.com/sean0x42/riverrats.com.au/issues/105', class: 'anchor' %>.

diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 461d951..fd28d57 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -6,6 +6,9 @@ + <% if content_for?(:no_turbolinks_cache) -%> + + <% end -%> <%= render 'favicons' %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb index b27f680..02375cd 100644 --- a/app/views/layouts/mailer.html.erb +++ b/app/views/layouts/mailer.html.erb @@ -2,11 +2,7 @@ - - <%= yield %> diff --git a/app/views/notifications/_notification.html.erb b/app/views/notifications/_notification.html.erb new file mode 100644 index 0000000..12d7a37 --- /dev/null +++ b/app/views/notifications/_notification.html.erb @@ -0,0 +1,38 @@ + + + + <% case notification.icon -%> + <% when 'game' -%> + + <% when 'comment' -%> + + <% else -%> + Other: <%= notification.icon.inspect %> + <% end -%> + + + + + <% if notification.url.nil? -%> + <%= notification.message %> + <% else -%> + <%= link_to notification.message, notification.url %> + <% end -%> + + + + Sent <%= local_time_ago(notification.created_at) %> + + + + <%= link_to notification_mark_read_path(notification), remote: true, + method: :patch, class: 'read' do %> + <% if notification.read -%> + + <% else -%> + + <% end -%> + <%= notification.read ? 'Mark unread' : 'Mark read' %> + <% end -%> + + diff --git a/app/views/notifications/_notifications.html.erb b/app/views/notifications/_notifications.html.erb new file mode 100644 index 0000000..a93c07e --- /dev/null +++ b/app/views/notifications/_notifications.html.erb @@ -0,0 +1,10 @@ +<% if @notifications.empty? -%> +
+ +

Nice! You're all caught up.

+
+<% else -%> + +<% end -%> diff --git a/app/views/notifications/_short_notification.html.erb b/app/views/notifications/_short_notification.html.erb new file mode 100644 index 0000000..ba30ecf --- /dev/null +++ b/app/views/notifications/_short_notification.html.erb @@ -0,0 +1,21 @@ +<% notification = short_notification -%> +
  • +
    + <% case notification.icon -%> + <% when 'game' -%> + + <% when 'comment' -%> + + <% else -%> + Other: <%= notification.icon.inspect %> + <% end -%> + <%= notification.icon.titleize %> +
    + + <% if notification.url.nil? -%> + <%= short_notification.message %> + <% else -%> + <%= link_to notification.message, notification.url, + class: 'notification-link' %> + <% end -%> +
  • diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb new file mode 100644 index 0000000..4d5646f --- /dev/null +++ b/app/views/notifications/index.html.erb @@ -0,0 +1,22 @@ +<% content_for :title, t('notifications.title') -%> +<% content_for :description, t('notifications.description') -%> +<% content_for :canonical_url, notifications_url -%> +<% content_for :no_turblinks_cache, true -%> + +
    + <%= render 'application/password_banner' %> + + Account Tools +

    <%= t('notifications.title') -%>

    + +
    + + <%= render @notifications %> +
    +
    + + +
    diff --git a/app/views/notifications/index.js.erb b/app/views/notifications/index.js.erb new file mode 100644 index 0000000..865d8cd --- /dev/null +++ b/app/views/notifications/index.js.erb @@ -0,0 +1 @@ +document.querySelector(".js-notifications").innerHTML = "<%= escape_javascript(render 'notifications') %>"; diff --git a/app/views/notifications/success.js.erb b/app/views/notifications/success.js.erb new file mode 100644 index 0000000..6a72ed3 --- /dev/null +++ b/app/views/notifications/success.js.erb @@ -0,0 +1 @@ +Turbolinks.visit(location.toString()); diff --git a/app/views/player_mailer/welcome.html.erb b/app/views/player_mailer/welcome.html.erb index c228b17..96ee762 100644 --- a/app/views/player_mailer/welcome.html.erb +++ b/app/views/player_mailer/welcome.html.erb @@ -1,19 +1,12 @@ -
    -
    -

    Welcome <%= @player.first_name %>,

    -

    We've just created an account for you over - at <%= link_to 'riverrats.com.au', root_url, style: 'color: #9154ab;' %>. Logging in is easy. simply use the - following username and password:

    +

    Welcome <%= @player.first_name %>,

    +

    Someone just created an account for you at <%= link_to 'riverrats.com.au', root_url %>.

    -

    Your username: -  <%= @player.username %>  -

    -

    Your password: -  <%= @password %>  -

    +

    Your username: + <%= @player.username %>  +

    - <%= link_to 'Click here to log in', new_player_session_url, style: 'color: #fdfcfd; background: linear-gradient(90deg, #9154ab, #ab54a8);line-height: 1.1; border-radius: 3px;text-decoration: none;padding: 16px 26px;margin: 0;display: inline-block;' %> +

    Your password: + <%= @password %>  +

    -

    We strongly recommend you <%= link_to 'update your password, by clicking this link', edit_player_registration_url, style: 'color: #ab7dbf;' %>.

    -
    -
    \ No newline at end of file +<%= link_to 'Click here to log in', new_player_session_url %> diff --git a/app/views/player_mailer/welcome.text.erb b/app/views/player_mailer/welcome.text.erb index 5c559ed..8723c7d 100644 --- a/app/views/player_mailer/welcome.text.erb +++ b/app/views/player_mailer/welcome.text.erb @@ -9,8 +9,4 @@ Your username is: Your password is: <%= @password %> -You may update this password when you log in for the -first time. - -To login to the website, follow this link: -<%= new_player_session_url %> \ No newline at end of file +<%= new_player_session_url %> diff --git a/app/views/players/index.html.erb b/app/views/players/index.html.erb index 133974a..acfd049 100644 --- a/app/views/players/index.html.erb +++ b/app/views/players/index.html.erb @@ -1,8 +1,10 @@ -<% content_for :title, t('players.index.title') %> -<% content_for :description, t('players.index.description') %> -<% content_for :canonical_url, players_url %> +<% content_for :title, t('players.index.title') -%> +<% content_for :description, t('players.index.description') -%> +<% content_for :canonical_url, players_url -%>
    + <%= render 'application/password_banner' %> + Leaderboard

    <%= t('players.index.title') %>

    @@ -16,4 +18,4 @@ <%= paginate @players %>

    <%= page_entries_info @players %>

    -
    \ No newline at end of file +
    diff --git a/app/views/players/registrations/_register_form.html.erb b/app/views/players/registrations/_register_form.html.erb index 83cd968..d688172 100644 --- a/app/views/players/registrations/_register_form.html.erb +++ b/app/views/players/registrations/_register_form.html.erb @@ -1,36 +1,51 @@ -<%= form_with model: resource, class: 'modal-form' do |form| %> - <%= devise_error_messages! %> - +<%= form_with model: resource, local: true, class: 'modal-form' do |form| %> +
    <%= form.label :first_name %>
    - <%= form.text_field :first_name, autofocus: true, spellcheck: false, placeholder: 'Given name...' %> + <%= field_errors resource, :first_name %> + <%= form.text_field :first_name, autofocus: true, spellcheck: false, + placeholder: 'Given name...' %>
    +
    <%= form.label :last_name %>
    - <%= form.text_field :last_name, spellcheck: false, placeholder: 'Surname, family name...' %> + <%= field_errors resource, :last_name %> + <%= form.text_field :last_name, spellcheck: false, + placeholder: 'Surname, family name...' %>
    -
    - +
    <%= form.label :email %>
    + <%= field_errors resource, :email %> <%= form.email_field :email, placeholder: 'someone@example.com' %>
    +
    <%= form.label :password %>
    - <%= form.password_field :password, autocomplete: 'off', placeholder: '6 characters minimum' %> + <%= field_errors resource, :password %> + <%= form.password_field :password, autocomplete: 'off', + placeholder: '6 characters minimum' %>
    +
    <%= form.label :password_confirmation %>
    - <%= form.password_field :password_confirmation, autocomplete: 'off', placeholder: 'Retype your password' %> + <%= field_errors resource, :password_confirmation %> + <%= form.password_field :password_confirmation, autocomplete: 'off', + placeholder: 'Retype your password' %>
    - <%= form.submit 'Create my account', class: 'button wide', name: nil, data: { disable_with: 'Please wait...' } %> + <%= form.submit 'Create my account', class: 'button wide', name: nil, + data: { disable_with: 'Please wait...' } %> -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/players/registrations/_settings_form.html.erb b/app/views/players/registrations/_settings_form.html.erb index 15f3a24..8b887dc 100644 --- a/app/views/players/registrations/_settings_form.html.erb +++ b/app/views/players/registrations/_settings_form.html.erb @@ -1,35 +1,37 @@ -<%= form_with model: resource, url: player_registration_path, class: 'modal-form' do |form| %> - - <%= devise_error_messages! %> - +<%= form_with model: resource, url: player_registration_path, local: true, + class: 'modal-form' do |form| %>
    <%= form.label :email %>
    - <%= form.email_field :email, autofocus: true, placeholder: 'someone@example.com' %> + <%= field_errors resource, :email %> + <%= form.email_field :email, autofocus: true, + placeholder: 'someone@example.com' %>
    <%= form.label :password %> (optional)
    + <%= field_errors resource, :password %> <%= form.password_field :password, autocomplete: 'off' %>
    <%= form.label :password_confirmation %>
    + <%= field_errors resource, :password_confirmation %> <%= form.password_field :password_confirmation, autocomplete: 'off' %>
    <%= form.label :current_password %> (required)
    + <%= field_errors resource, :current_password %> <%= form.password_field :current_password, autocomplete: 'off' %>

    Notification Settings

    -
    <%= form.check_box :notify_promotional %> @@ -41,9 +43,8 @@ <%= form.check_box :notify_events %> <%= form.label :notify_events, 'Receive upcoming event emails' %>
    -
    - <%= form.submit 'Save settings', class: 'button wide', remote: true, data: { disable_with: 'Please wait...' } %> - -<% end %> \ No newline at end of file + <%= form.submit 'Save settings', class: 'button wide', remote: true, + data: { disable_with: 'Please wait...' } %> +<% end %> diff --git a/app/views/players/registrations/create.js.erb b/app/views/players/registrations/create.js.erb new file mode 100644 index 0000000..8658aac --- /dev/null +++ b/app/views/players/registrations/create.js.erb @@ -0,0 +1 @@ +document.querySelector(".modal-form").innerHTML = "<%= escape_javascript(render 'register_form') %>"; diff --git a/app/views/players/registrations/edit.html.erb b/app/views/players/registrations/edit.html.erb index 4b2a7fb..fc25a2a 100644 --- a/app/views/players/registrations/edit.html.erb +++ b/app/views/players/registrations/edit.html.erb @@ -1,17 +1,19 @@ -<% content_for :title, 'Preferences' -%> +<% content_for :title, t('players.registrations.edit.title') -%> +<% content_for :description, t('players.registrations.edit.description') -%> +<% content_for :canonical_url, edit_player_registration_url -%> -Account Tools -

    Settings

    +
    + Account Tools +

    <%= t('players.registrations.edit.title') %>

    -<%= render 'settings_form' %> + <%= render 'settings_form' %>
    -

    Delete account

    Warning! This action cannot be undone. Unhappy? Cancel your account here. We'll be sad to see - you go.

    + you go.

    <%= link_to 'Delete account', @@ -21,5 +23,5 @@ class: 'button danger' %>
    -
    +
    diff --git a/app/views/players/registrations/new.html.erb b/app/views/players/registrations/new.html.erb index 3082566..edaf941 100644 --- a/app/views/players/registrations/new.html.erb +++ b/app/views/players/registrations/new.html.erb @@ -1,8 +1,10 @@ -<% content_for :title, 'Create an account' %> -<% content_for :description, t('description.players.registrations.new') %> -<% content_for :canonical_url, new_player_registration_url %> +<% content_for :title, t('players.registrations.new.title') -%> +<% content_for :description, t('players.registrations.new.description') -%> +<% content_for :canonical_url, new_player_registration_url -%> -Create an account -

    Register

    +
    + Create an account +

    <%= t('players.registrations.new.title') %>

    -<%= render 'register_form' %> \ No newline at end of file + <%= render 'register_form' %> +
    diff --git a/app/views/players/sessions/_form.html.erb b/app/views/players/sessions/_form.html.erb index 6f55a52..ce5c1cb 100644 --- a/app/views/players/sessions/_form.html.erb +++ b/app/views/players/sessions/_form.html.erb @@ -1,5 +1,4 @@ <%= form_with model: resource, url: player_session_path, class: 'modal-form', local: true do |form| %> -
    <%= form.label :login, 'Email or Username' %>
    <%= form.text_field :login, autofocus: true %> @@ -25,5 +24,4 @@ - -<% end %> \ No newline at end of file +<% end -%> diff --git a/app/views/players/sessions/new.html.erb b/app/views/players/sessions/new.html.erb index 490155f..d07f17e 100644 --- a/app/views/players/sessions/new.html.erb +++ b/app/views/players/sessions/new.html.erb @@ -1,8 +1,10 @@ -<% content_for :title, 'Secure Login' %> -<% content_for :description, t('description.players.sessions.new') %> -<% content_for :canonical_url, new_player_session_url %> +<% content_for :title, 'Secure Login' -%> +<% content_for :description, t('description.players.sessions.new') -%> +<% content_for :canonical_url, new_player_session_url -%> -Welcome Back -

    Secure Login

    +
    + Welcome Back +

    Secure Login

    -<%= render 'form' %> \ No newline at end of file + <%= render 'form' %> +
    diff --git a/app/views/players/show.html.erb b/app/views/players/show.html.erb index 4ce426b..bce33ab 100644 --- a/app/views/players/show.html.erb +++ b/app/views/players/show.html.erb @@ -1,6 +1,6 @@ -<%- content_for :title, t('players.show.title') % { name: @player.full_name, username: "@#{@player.username}" } %> -<%- content_for :description, t('players.show.description') % { name: @player.full_name } %> -<%- content_for :canonical_url, players_url %> +<% content_for :title, t('players.show.title') % { name: @player.full_name, username: "@#{@player.username}" } -%> +<% content_for :description, t('players.show.description') % { name: @player.full_name } -%> +<% content_for :canonical_url, players_url -%>
    @@ -13,7 +13,7 @@
    - <%- season_player = @player.season_player %> + <% season_player = @player.season_player -%>

    All Time Score

    @@ -40,22 +40,31 @@
    + <% games = @player.recent_games.page(params[:page]).per(25) -%>
    - <%= render collection: @player.recent_games, partial: 'game_player' %> + <%= render collection: games, partial: 'game_player' %>
    + +

    Recent Achievements

    - -

    Sorry, this section is under construction. Please check back later.

    + <%= link_to 'View all achievements', player_achievements_path(@player), class: 'button-tertiary' %> + +
    +

    + + <%= number_format @player.tickets %> <%= 'ticket'.pluralize(@player.tickets) %> +

    +
    -
    \ No newline at end of file +
    diff --git a/app/views/regions/show.html.erb b/app/views/regions/show.html.erb index 3d96b25..3a2231a 100644 --- a/app/views/regions/show.html.erb +++ b/app/views/regions/show.html.erb @@ -3,6 +3,8 @@ <% content_for :canonical_url, region_url(@region) -%>
    + <%= render 'application/password_banner' %> + Region

    <%= t('region.show.title') % { region: @region.name } %>

    @@ -51,4 +53,4 @@

    - \ No newline at end of file + diff --git a/app/views/venues/show.html.erb b/app/views/venues/show.html.erb index 0d3dbff..49457c4 100644 --- a/app/views/venues/show.html.erb +++ b/app/views/venues/show.html.erb @@ -3,6 +3,8 @@ <%- content_for :canonical_url, venue_url(@venue) %>
    + <%= render 'application/password_banner' %> + Venue

    <%= t('venues.show.title') % { venue: @venue.name } %>

    @@ -89,4 +91,4 @@

    - \ No newline at end of file + diff --git a/app/workers/comment_notification_worker.rb b/app/workers/comment_notification_worker.rb new file mode 100644 index 0000000..b657582 --- /dev/null +++ b/app/workers/comment_notification_worker.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# Sends notifications out to player for commentss +class CommentNotificationWorker + include Sidekiq::Worker + include Rails.application.routes.url_helpers + + def perform(id, player_id) + comment = Comment.find(id) + Notification.create( + player_id: player_id, + icon: :comment, + message: message(comment), + url: game_path(comment.game_id) + ) + end + + private + + def message(comment) + venue = Venue.joins(:games).find_by(games: { id: comment.game_id }) + format(I18n.t('notification.comment'), + sender: comment.player.username, + game: comment.game_id.to_s.rjust(2, '0'), + venue: venue.name) + end +end diff --git a/app/workers/comment_notifications_worker.rb b/app/workers/comment_notifications_worker.rb new file mode 100644 index 0000000..c875508 --- /dev/null +++ b/app/workers/comment_notifications_worker.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# Sends notifications out to players for comments +class CommentNotificationsWorker + include Sidekiq::Worker + + def perform(id) + comment = Comment.find(id) + players = Player.joins(:games_players) + .where(games_players: { game_id: comment.game_id }) + .pluck(:id) + players.each do |player_id| + next if player_id == comment.player_id + + CommentNotificationWorker.perform_async(id, player_id) + end + end +end diff --git a/app/workers/daily_action_worker.rb b/app/workers/daily_action_worker.rb new file mode 100644 index 0000000..ddc29ff --- /dev/null +++ b/app/workers/daily_action_worker.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# A worker which runs every day, deleting old actions +class DailyActionWorker + include Sidekiq::Worker + + def perform + Action.where('created_at > ?', Time.zone.today - 3.months).delete_all + + # Schedule next worker + DailyActionWorker.perform_at(1.day.from_now.at_end_of_day) + end +end diff --git a/app/workers/daily_notification_worker.rb b/app/workers/daily_notification_worker.rb new file mode 100644 index 0000000..d440fd8 --- /dev/null +++ b/app/workers/daily_notification_worker.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# Deletes old notifications +class DailyNotificationWorker + include Sidekiq::Worker + + def perform + Notification.where('created_at < ?', Time.zone.today - 1.week).destroy_all + + DailyNotificationWorker.perform_at(1.day.from_now.at_beginning_of_day) + end +end diff --git a/app/workers/game_notification_worker.rb b/app/workers/game_notification_worker.rb new file mode 100644 index 0000000..c077cc6 --- /dev/null +++ b/app/workers/game_notification_worker.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# A worker for creating and sending notifications +class GameNotificationWorker + include Sidekiq::Worker + include Rails.application.routes.url_helpers + sidekiq_options queue: 'low_priority' + + def perform(id, player_id) + player = GamesPlayer.find(id) + Notification.create( + player_id: player_id, + icon: :game, + message: message(player), + url: game_path(player.game_id) + ) + end + + private + + def message(player) + venue = Venue.joins(:games).find_by(games: { id: player.game_id }) + format(I18n.t('notification.game'), + position: (player.position + 1).ordinalize, + game: player.game_id.to_s.rjust(2, '0'), + venue: venue.name) + end +end diff --git a/app/workers/game_ticket_worker.rb b/app/workers/game_ticket_worker.rb new file mode 100644 index 0000000..c067d7b --- /dev/null +++ b/app/workers/game_ticket_worker.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# Awards a ticket to a single player from a game +class GameTicketWorker + include Sidekiq::Worker + + def perform(player_id) + player = Player.find(player_id) + player.tickets += 1 + player.save + end +end diff --git a/app/workers/game_tickets_worker.rb b/app/workers/game_tickets_worker.rb new file mode 100644 index 0000000..704a588 --- /dev/null +++ b/app/workers/game_tickets_worker.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# A worker which awards the given number of tickets to players in a game +class GameTicketsWorker + include Sidekiq::Worker + + def perform(game_id, tickets) + game = Game.find(game_id) + game.game_played_by.limit(tickets).pluck(:id).each do |id| + GameTicketWorker.perform_async(id) + end + end +end diff --git a/config/database.yml b/config/database.yml index ae421de..79170ed 100644 --- a/config/database.yml +++ b/config/database.yml @@ -90,4 +90,4 @@ production: <<: *default database: river_rats username: rails - password: '' \ No newline at end of file + password: '' diff --git a/config/deploy.rb b/config/deploy.rb index f37aa93..56d1dc8 100644 --- a/config/deploy.rb +++ b/config/deploy.rb @@ -4,7 +4,7 @@ lock '~> 3.11.0' set :application, 'river_rats' -set :repo_url, 'git@github.com:LuckehPickle/riverrats.com.au.git' +set :repo_url, 'git@github.com:sean0x42/riverrats.com.au.git' set :branch, 'master' set :domain, 'riverrats.com.au' set :rvm_ruby_version, '2.4.4' @@ -39,7 +39,7 @@ # set :local_user, -> { `git config user.name`.chomp } # Default value for keep_releases is 5 -set :keep_releases, 5 +set :keep_releases, 5 # Uncomment the following to require manually verifying the host key before # first deploy. diff --git a/config/locales/en.yml b/config/locales/en.yml index e554fe1..e8debd0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -6,7 +6,10 @@ # available at http://guides.rubyonrails.org/i18n.html. en: - version: "version 1.1.3" + version: "version 1.2.0" + notification: + game: "You placed %{position} in Game #%{game} at the %{venue}." + comment: "@%{sender} just commented on Game #%{game} at the %{venue}." achievement: wins: title: "Winner Winner Chicken Dinner %{level}" @@ -19,34 +22,55 @@ en: description: "Play %{count} %{game}." royal_flush: title: "Get Flushed!" - description: "Get a Royal Flush" + description: "Get dealt a royal flush." straight_flush: title: "Semi Flushable" - description: "Get a Straight Flush" + description: "Get dealt a straight flush." king_of_the_world: title: "King Of The World" description: "Win a regional championship." born_leader: title: "Born Leader" - description: "Regional Point Winner" + description: "Regional Point Winner." hand_me_the_deed: title: "Hand Me The Deed" - description: "Venue Point Winner" + description: "Venue Point Winner." the_bridesmaid: title: "The Bridesmaid" - description: "Place second in %{games}" + description: "Place second in %{games}." the_prodigy: title: "The Prodigy" - description: "Most games won by any player in a season" + description: "Most games won by any player in a season." the_wooden_spoon: title: "The Wooden Spoon %{level}" - description: "Place 10th in %{games}" + description: "Place 10th in %{games}." + comments: + edit: + title: "Edit Comment" + destroy: "Successfully deleted this comment." + players: + index: + title: "All Time Leaderboard" + description: "View the best players that the River Rats Poker League has to offer!" + search: + title: "Search Results" + description: "Search all players" + show: + title: "%{name} (%{username})" + description: "View %{name}'s player profile, including a brief game history, achievements, and rankings!" + registrations: + new: + title: "Register" + description: "Join the River Rats Poker League official website. Start tracking your scores, games, achievements and more!" + edit: + title: "Settings" + description: "Adjust your account settings, to get the most out of the River Rats website." + sessions: + new: + title: "Login" + description: "Login to access your River Rats Poker League account. You can use either your email address or your username." description: players: - registrations: - new: "Join the River Rats Poker League official website. Start tracking your scores, games, achievements and more!" - sessions: - new: "Login to access your River Rats Poker League account. You can use either your email address or your username." passwords: new: "If you forgot your account password, then this page allows you to send a password reset email." events: @@ -62,16 +86,19 @@ en: title: "Edit Player" create: flash: "You have successfully created a player." + action: "created player @%{player}." update: flash: "You have successfully updated a player." + action: "edited player @%{player}." destroy: flash: "You have successfully deleted a player." + action: "deleted player @%{player}." confirm_delete: "Are you sure you want to delete this player? This action cannot be undone." games: index: title: "Games" new: - title: "Create Game" + title: "Record Game" add_players: "Add Players" add_tournament_directors: "Add Tournament Directors" game_details: "Edit Details" @@ -85,10 +112,13 @@ en: title: "Edit Game" create: flash: "You have successfully created a game." + action: "recorded Game #%{game}." update: flash: "You have successfully updated a game." + action: "edited Game #%{game}." destroy: flash: "You have successfully deleted a game." + action: "deleted Game #%{game}." confirm_delete: "Are you sure you want to delete this game? This action cannot be undone." events: index: @@ -99,10 +129,13 @@ en: title: "Edit Event" create: flash: "You have successfully created an event." + action: "created event %{event}." update: flash: "You have successfully updated an event." + action: "edited event %{event}." destroy: flash: "You have successfully deleted an event." + action: "deleted event %{event}." confirm_delete: "Are you sure you want to delete this event? This action cannot be undone." recurring: confirm_delete: "Are you sure you want to delete this event, and all following events? This action cannot be undone." @@ -118,10 +151,13 @@ en: title: "Edit Venue" create: flash: "You have successfully created a venue." + action: "created venue %{venue}." update: flash: "You have successfully updated a venue." + action: "edited venue %{venue}." destroy: flash: "You have successfully deleted a venue." + action: "deleted venue %{venue}." confirm_delete: "Are you sure you want to delete this venue? This action cannot be undone." regions: index: @@ -132,35 +168,38 @@ en: title: "Edit Region" create: flash: "You have successfully created a region." + action: "created region %{region}." update: flash: "You have successfully updated a region." + action: "edited region %{region}." destroy: flash: "You have successfully deleted a region." + action: "deleted region %{region}." confirm_delete: "Are you sure you want to delete this region? This action cannot be undone." achievements: create: - title: "Successfully awarded an achievement" - body: "You awarded an achievement." + flash: "Successfully awarded an achievement" + action: "awarded achievement %{achievement} to @%{player}." new: title: "Award Achievement" mail: new: title: "Generate Mailing List" + tickets: + edit: + title: "Award & Revoke Tickets" + label: "Tickets to Award (can be negative)" + update: + flash: "Successfully updated player tickets." + action: "awarded %{tickets} ticket(s) to @%{player}." + actions: + index: + title: "Review Admin Actions" danger_zone: "Careful! Actions performed here cannot be undone." seasons: show: title: "%{season}" description: "This page contains complete leaderboards, start and end dates, and some other metadata for %{season}." - players: - index: - title: "All Time Leaderboard" - description: "View the best players that the River Rats Poker League has to offer!" - search: - title: "Search Results" - description: "Search all players" - show: - title: "%{name} (%{username})" - description: "View %{name}'s player profile, including a brief game history, achievements, and rankings!" games: show: title: "Game %{game} at the %{venue}" @@ -202,6 +241,13 @@ en: index: title: "%{player}'s Achievements" description: "View a list of all achievements unlocked by %{player}" + notifications: + title: 'Notifications' + description: 'View your full notificatio history.' log: term_trap: "Unicorn worker intercepting TERM and doing nothing. Wait for master to send QUIT" intercept_term: "Unicorn master intercepting TERM and sending myself QUIT instead" + password: + banner: + title: "Please change your password" + body: "You're still using the default password. Please update it now to keep your account secure." diff --git a/config/routes.rb b/config/routes.rb index cb71799..65215fa 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,23 +1,38 @@ # frozen_string_literal: true +# rubocop:disable Metrics/BlockLength Rails.application.routes.draw do require 'sidekiq/web' + + # These routes only apply to logged in players authenticated :player do mount Sidekiq::Web => '/sidekiq' + + # Routes for notifications + resources :notifications, only: %i[index destroy] do + match 'mark-read', via: %i[patch put] + collection { match 'clear', via: %i[patch put] } + end end devise_for :players, - path_names: { sign_in: 'login', sign_out: 'logout', sign_up: 'register' }, + path_names: { + sign_in: 'login', + sign_out: 'logout', + sign_up: 'register' + }, controllers: { registrations: 'players/registrations', sessions: 'players/sessions', passwords: 'players/passwords' } + # Landing page routes root 'landing#index' get '/privacy-policy', to: 'landing#privacy_policy' get '/release-notes', to: 'landing#release_notes' + # Player specific routes resources :players, only: %i[index show], param: :username do collection do get 'search' @@ -27,20 +42,39 @@ resources :achievements, only: %i[index show] end + resources :games, only: %i[index show] do + resources :comments, except: %i[index show new] + end + resources :events, only: :show - resources :games, :seasons, only: %i[index show] + resources :seasons, only: %i[index show] get '/calendar(/:year/:month)', to: 'events#index', as: 'events' resources :regions, :venues, only: :show, param: :slug + # Routes that are only accessible to administrators namespace :admin do root to: redirect('/admin/players') - resources :players, except: :show, param: :username + # Player specific routes + resources :players, except: :show, param: :username do + get 'tickets', to: 'tickets#edit' + match 'tickets', to: 'tickets#update', via: %i[patch put] + resources :achievements, only: %i[new create] + end + + # Generic routes resources :games, :events, :regions, :venues, except: :show - resources :achievements, only: %i[new create] + resources :actions, only: :index + + # Mail + resources :mail, only: :index do + collection do + post 'players', to: 'mail#generate', defaults: { format: 'csv' } + end + end - get 'mail', to: 'mail#index' - post 'mail/players', to: 'mail#show', defaults: { format: 'csv' } + # Generic objects get 'scores', to: 'scores#index' end end +# rubocop:enable Metrics/BlockLength diff --git a/db/migrate/20181002020607_add_nickname_to_players.rb b/db/migrate/20181002020607_add_nickname_to_players.rb new file mode 100644 index 0000000..2d48816 --- /dev/null +++ b/db/migrate/20181002020607_add_nickname_to_players.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# Adds a nickname field to each player's name +class AddNicknameToPlayers < ActiveRecord::Migration[5.2] + def change + add_column :players, :nickname, :string, null: true, default: nil + end +end diff --git a/db/migrate/20181003033751_create_notifications.rb b/db/migrate/20181003033751_create_notifications.rb new file mode 100644 index 0000000..b976e44 --- /dev/null +++ b/db/migrate/20181003033751_create_notifications.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class CreateNotifications < ActiveRecord::Migration[5.2] + def change + create_table :notifications do |t| + t.belongs_to :player, null: false, index: true + t.integer :icon, default: 0, null: false + t.string :message, null: false + t.string :url + + t.timestamps + end + end +end diff --git a/db/migrate/20181024092215_create_comments.rb b/db/migrate/20181024092215_create_comments.rb new file mode 100644 index 0000000..9f2b31c --- /dev/null +++ b/db/migrate/20181024092215_create_comments.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class CreateComments < ActiveRecord::Migration[5.2] + def change + create_table :comments do |t| + t.belongs_to :game, index: true, null: false + t.belongs_to :player, index: true, null: false + t.text :body + t.timestamps + end + end +end diff --git a/db/migrate/20181029025504_add_read_to_notifications.rb b/db/migrate/20181029025504_add_read_to_notifications.rb new file mode 100644 index 0000000..6ecf01c --- /dev/null +++ b/db/migrate/20181029025504_add_read_to_notifications.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddReadToNotifications < ActiveRecord::Migration[5.2] + def change + add_column :notifications, :read, :boolean, default: false, null: false + end +end diff --git a/db/migrate/20181115045216_add_tickets_to_players.rb b/db/migrate/20181115045216_add_tickets_to_players.rb new file mode 100644 index 0000000..e07f4d4 --- /dev/null +++ b/db/migrate/20181115045216_add_tickets_to_players.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddTicketsToPlayers < ActiveRecord::Migration[5.2] + def change + add_column :players, :tickets, :int, null: false, default: 0 + end +end diff --git a/db/migrate/20181115054721_create_actions.rb b/db/migrate/20181115054721_create_actions.rb new file mode 100644 index 0000000..490a838 --- /dev/null +++ b/db/migrate/20181115054721_create_actions.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class CreateActions < ActiveRecord::Migration[5.2] + def change + create_table :actions do |t| + t.references :player, index: true + t.integer :action + t.string :description + t.timestamps + end + end +end diff --git a/db/migrate/20181130230556_add_password_changed_to_players.rb b/db/migrate/20181130230556_add_password_changed_to_players.rb new file mode 100644 index 0000000..7733622 --- /dev/null +++ b/db/migrate/20181130230556_add_password_changed_to_players.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddPasswordChangedToPlayers < ActiveRecord::Migration[5.2] + def change + add_column :players, :password_changed, :boolean, null: false, default: true + end +end diff --git a/db/migrate/20181201022853_add_deleted_to_comments.rb b/db/migrate/20181201022853_add_deleted_to_comments.rb new file mode 100644 index 0000000..e75b346 --- /dev/null +++ b/db/migrate/20181201022853_add_deleted_to_comments.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddDeletedToComments < ActiveRecord::Migration[5.2] + def change + add_column :comments, :deleted, :boolean, null: false, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 8f250ff..b816a0e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2018_09_22_043403) do +ActiveRecord::Schema.define(version: 2018_12_01_022853) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -22,12 +22,21 @@ t.datetime "updated_at", null: false t.string "proof_file_name" t.string "proof_content_type" - t.integer "proof_file_size" + t.bigint "proof_file_size" t.datetime "proof_updated_at" t.integer "level", default: 0, null: false t.index ["player_id"], name: "index_achievements_on_player_id" end + create_table "actions", force: :cascade do |t| + t.bigint "player_id" + t.integer "action" + t.string "description" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["player_id"], name: "index_actions_on_player_id" + end + create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false @@ -49,6 +58,25 @@ t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true end + create_table "articles", force: :cascade do |t| + t.string "title" + t.text "body" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "draft", default: true, null: false + end + + create_table "comments", force: :cascade do |t| + t.bigint "game_id", null: false + t.bigint "player_id", null: false + t.text "body" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "deleted", default: false, null: false + t.index ["game_id"], name: "index_comments_on_game_id" + t.index ["player_id"], name: "index_comments_on_player_id" + end + create_table "events", force: :cascade do |t| t.string "title" t.bigint "venue_id", null: false @@ -60,7 +88,7 @@ t.text "days" t.text "description" t.bigint "recurring_event_id" - t.string "type", null: false + t.string "type", default: "", null: false t.index ["recurring_event_id"], name: "index_events_on_recurring_event_id" t.index ["venue_id"], name: "index_events_on_venue_id" end @@ -99,6 +127,17 @@ t.index ["player_id"], name: "index_games_players_on_player_id" end + create_table "notifications", force: :cascade do |t| + t.bigint "player_id", null: false + t.integer "icon", default: 0, null: false + t.string "message", null: false + t.string "url" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "read", default: false, null: false + t.index ["player_id"], name: "index_notifications_on_player_id" + end + create_table "players", force: :cascade do |t| t.string "username", null: false t.string "first_name", null: false @@ -125,6 +164,9 @@ t.integer "second_places", default: 0, null: false t.integer "wooden_spoons", default: 0, null: false t.boolean "developer", default: false + t.string "nickname" + t.integer "tickets", default: 0, null: false + t.boolean "password_changed", default: true, null: false t.index ["email"], name: "index_players_on_email" t.index ["reset_password_token"], name: "index_players_on_reset_password_token", unique: true t.index ["username"], name: "index_players_on_username", unique: true @@ -191,6 +233,8 @@ create_table "seasons", force: :cascade do |t| t.date "start_at", null: false t.date "end_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "venues", force: :cascade do |t| @@ -209,7 +253,7 @@ t.integer "state", limit: 2, default: 1 t.string "image_file_name" t.string "image_content_type" - t.integer "image_file_size" + t.bigint "image_file_size" t.datetime "image_updated_at" t.index ["region_id"], name: "index_venues_on_region_id" t.index ["slug"], name: "index_venues_on_slug", unique: true diff --git a/db/seeds.rb b/db/seeds.rb index 1c51eb1..be84bbf 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -6,7 +6,7 @@ # # Examples: # -# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) -# Character.create(name: 'Luke', movie: movies.first) +# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) +# Character.create(name: 'Luke', movie: movies.first) SeasonGeneratorJob.perform_now diff --git a/lib/username_lib.rb b/lib/username_lib.rb index b356fcb..54297f2 100644 --- a/lib/username_lib.rb +++ b/lib/username_lib.rb @@ -6,7 +6,7 @@ def self.generate_username(first_name, last_name) standard = "#{first_name}#{last_name}".downcase username = standard - # Keep adding a random digit to the end of the players name until it's unique + # Keep adding random digits to players name until unique while Player.exists?(username: username) username = "#{standard}#{Random.rand(1..99)}" end diff --git a/public/404.html b/public/404.html index a1d7b78..85e6178 100644 --- a/public/404.html +++ b/public/404.html @@ -29,161 +29,132 @@ - - +
    + HTTP Exception 404 +

    Page Not Found

    -
    -

    404

    -

    Sorry, but we couldn't find that page. You may have mistyped the address, or the page may have been moved.

    -
    - - - home - Return home - - - - arrow_back - Go back - - +

    We couldn't find a page at this address. You may have mistyped the address, or the page may have been moved.

    + Go to home page
    -
    - - - diff --git a/public/422.html b/public/422.html index 38275e0..aa19335 100644 --- a/public/422.html +++ b/public/422.html @@ -29,161 +29,117 @@ - - - -
    -

    422

    -

    The change you wanted was rejected. Maybe you tried to change something you didn't have access to.

    -
    - - - home - Return home - - - - arrow_back - Go back - - +
    + HTTP Exception 422 +

    Change Rejected

    + +

    The change you wanted was rejected. Maybe you tried to change something you didn't have access to.

    + Go to home page
    -
    - - - diff --git a/public/500.html b/public/500.html index bcd9dae..eaa6a7c 100644 --- a/public/500.html +++ b/public/500.html @@ -29,161 +29,117 @@ - - - -
    -

    500

    -

    We're sorry, but something went wrong with our website. This error has been logged.

    -
    - - - home - Return home - - - - arrow_back - Go back - - +
    + HTTP Exception 500 +

    Server Error

    + +

    We're sorry, but something went wrong with our website. If you encounter this message, please let us know what you were doing when it happened by sending an email to sean@seanbailey.io.

    + Go to home page
    -
    - - - - \ No newline at end of file + diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb index 652febb..40ca24e 100644 --- a/test/application_system_test_case.rb +++ b/test/application_system_test_case.rb @@ -2,6 +2,7 @@ require 'test_helper' +# System test case class ApplicationSystemTestCase < ActionDispatch::SystemTestCase driven_by :selenium, using: :chrome, screen_size: [1400, 1400] end diff --git a/test/controllers/admin/actions_controller_test.rb b/test/controllers/admin/actions_controller_test.rb new file mode 100644 index 0000000..f41ed4d --- /dev/null +++ b/test/controllers/admin/actions_controller_test.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'test_helper' + +class Admin::ActionsControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/admin/tickets_controller_test.rb b/test/controllers/admin/tickets_controller_test.rb new file mode 100644 index 0000000..e0929e4 --- /dev/null +++ b/test/controllers/admin/tickets_controller_test.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'test_helper' + +class Admin::TicketsControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/comments_controller_test.rb b/test/controllers/comments_controller_test.rb new file mode 100644 index 0000000..4c6f928 --- /dev/null +++ b/test/controllers/comments_controller_test.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'test_helper' + +class CommentsControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/notifications_controller_test.rb b/test/controllers/notifications_controller_test.rb new file mode 100644 index 0000000..4bafae7 --- /dev/null +++ b/test/controllers/notifications_controller_test.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'test_helper' + +# Tests the notification controller +class NotificationsControllerTest < ActionDispatch::IntegrationTest + test '' do + assert true + end +end diff --git a/test/fixtures/actions.yml b/test/fixtures/actions.yml new file mode 100644 index 0000000..80aed36 --- /dev/null +++ b/test/fixtures/actions.yml @@ -0,0 +1,11 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +# This model initially had no columns defined. If you add columns to the +# model remove the '{}' from the fixture names and add the columns immediately +# below each fixture, per the syntax in the comments below +# +one: {} +# column: value +# +two: {} +# column: value diff --git a/test/fixtures/comments.yml b/test/fixtures/comments.yml new file mode 100644 index 0000000..c76d847 --- /dev/null +++ b/test/fixtures/comments.yml @@ -0,0 +1,5 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +# This model initially had no columns defined. If you add columns to the +# model remove the '{}' from the fixture names and add the columns immediately +# below each fixture, per the syntax in the comments below diff --git a/test/fixtures/notifications.yml b/test/fixtures/notifications.yml new file mode 100644 index 0000000..8608e18 --- /dev/null +++ b/test/fixtures/notifications.yml @@ -0,0 +1,2 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + diff --git a/test/fixtures/players.yml b/test/fixtures/players.yml index 37960bc..7dbfee3 100644 --- a/test/fixtures/players.yml +++ b/test/fixtures/players.yml @@ -38,15 +38,3 @@ eve: developer: false email: eve@stephens.com encrypted_password: <%= Devise::Encryptor.digest Player, '_evenstephens' %> - -felicity: - username: '' - first_name: Felicity - last_name: Jones - encrypted_password: <%= Devise::Encryptor.digest Player, 'aLpHaBeTsOuP' %> - -greg: - username: gregorydavis - first_name: '' - last_name: Davis - encrypted_password: <%= Devise::Encryptor.digest Player, '_3287hbdsfk' %> \ No newline at end of file diff --git a/test/models/action_test.rb b/test/models/action_test.rb new file mode 100644 index 0000000..fd55076 --- /dev/null +++ b/test/models/action_test.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'test_helper' + +# Tests actions +class ActionTest < ActiveSupport::TestCase + test 'description must exist' do + action = Action.new(description: nil) + assert_not action.valid?, 'Action is valid without description' + assert_not_empty action.errors[:description], + 'No validation error raised with nil description' + end + + test 'description must not be short' do + len = 2 + action = Action.new(description: 'a' * len) + assert_not action.valid?, + "Action is valid with short description (#{len} chars)" + assert_not_empty action.errors[:description], + 'No validation error raised for short description '\ + "(#{len} chars)" + end + + test 'action must not be nil' do + action = Action.new(action: nil) + assert_not action.valid?, 'Action is valid with nil action' + assert_not_empty action.errors[:action], + 'No validation error raised for nil action' + end +end diff --git a/test/models/comment_test.rb b/test/models/comment_test.rb new file mode 100644 index 0000000..117da5c --- /dev/null +++ b/test/models/comment_test.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'test_helper' + +# Test comments +class CommentTest < ActiveSupport::TestCase + test 'invalid with blank body' do + comment = Comment.new(body: '') + assert_not comment.valid?, 'Comment is valid with blank body' + assert_not_empty comment.errors[:body], + 'No validation error present for comment with blank body' + end + + test 'invalid with nil body' do + comment = Comment.new(body: nil) + assert_not comment.valid?, 'Comment is valid with nil body' + assert_not_empty comment.errors[:body], + 'No validation error present for comment with nil body' + end + + test 'invalid with long body' do + comment = Comment.new(body: 'h' * 500) + assert_not comment.valid?, 'Comment is valid with long body (500 chars)' + assert_not_empty comment.errors[:body], + 'No validation error present for comment with long body '\ + '(500 chars)' + end + + test 'invalid without player' do + comment = Comment.new(player: nil) + assert_not comment.valid?, 'Comment is valid with nil player' + assert_not_empty comment.errors[:player], + 'No validation error present for comment with nil player' + end + + test 'invalid without game' do + comment = Comment.new(game: nil) + assert_not comment.valid?, 'Comment is valid with nil game' + assert_not_empty comment.errors[:game], + 'No validation error present for comment with nil game' + end + + test 'invalid with nil deleted' do + comment = Comment.new(deleted: nil) + assert_not comment.valid?, 'Comment is valid with nil deleted' + assert_not_empty comment.errors[:deleted], + 'No validation error present for comment with nil deleted' + end +end diff --git a/test/models/game_player_test.rb b/test/models/game_player_test.rb index 1be8efb..f18922d 100644 --- a/test/models/game_player_test.rb +++ b/test/models/game_player_test.rb @@ -2,6 +2,7 @@ require 'test_helper' +# Test games players class GamePlayerTest < ActiveSupport::TestCase test 'position should be positive' do player = games_players(:negative_position) diff --git a/test/models/game_test.rb b/test/models/game_test.rb index cb328ce..02fbd8e 100644 --- a/test/models/game_test.rb +++ b/test/models/game_test.rb @@ -6,27 +6,38 @@ class GameTest < ActiveSupport::TestCase test 'should have at least two players' do game = games(:no_players) - assert_not game.valid?, 'Game should not have less than two players.' + assert_not game.valid?, 'Game should not have less than two players' + assert_not_empty game.errors[:games_players], + 'No validation error given for game with less than two '\ + 'players' end test 'should have at least one referee' do game = games(:no_referees) - assert_not game.valid?, 'Game should not be valid with no referees.' - # puts game.errors.messages + assert_not game.valid?, 'Game should not be valid with no referees' + assert_not_empty game.errors[:referees], + 'No validation given for game with no referees' end test 'should not have duplicate players' do game = games(:duplicate_players) - assert_not game.valid?, 'Players should not appear multiple times.' + assert_not game.valid?, 'Players should not appear multiple times' + assert_not_empty game.errors[:games_players], + 'No validation error given for game with duplicate players' end test 'should not have duplicate referees' do game = games(:duplicate_referees) - assert_not game.valid?, 'Referees should not appear multiple times.' + assert_not game.valid?, 'Referees should not appear multiple times' + assert_not_empty game.errors[:referees], + 'No validation error given for game with duplicate '\ + 'referees' end test 'should be within season' do game = games(:out_of_season) - assert_not game.valid?, 'Game should be played within it\'s season.' + assert_not game.valid?, 'Game should be played within it\'s season' + assert_not_empty game.errors[:played_on], + 'No validation error given for game out of season' end end diff --git a/test/models/notification_test.rb b/test/models/notification_test.rb new file mode 100644 index 0000000..010b7ed --- /dev/null +++ b/test/models/notification_test.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'test_helper' + +# Tests the notification model +class NotificationTest < ActiveSupport::TestCase + test 'invalid with blank message' do + notification = Notification.new(message: '') + assert_not notification.valid?, 'Notification was valid with blank message' + assert_not_empty notification.errors[:message], + 'No validation error given for blank message' + end + + test 'invalid with nil message' do + notification = Notification.new(message: nil) + assert_not notification.valid?, 'Notification was valid with nil message' + assert_not_empty notification.errors[:message], + 'No validation error given for nil message' + end + + test 'invalid with long message' do + notification = Notification.new(message: 'o' * 240) + assert_not notification.valid?, + 'Notification was valid with long message (240 chars)' + assert_not_empty notification.errors[:message], + 'No validation error given for long message (240 chars)' + end + + test 'invalid with short message' do + notification = Notification.new(message: 'o') + assert_not notification.valid?, + 'Notification was valid with short message (1 char)' + assert_not_empty notification.errors[:message], + 'No validation error given for short message (1 char)' + end + + test 'invalid with no player' do + notification = Notification.new(player: nil) + assert_not notification.valid?, 'Notification valid without player' + assert_not_empty notification.errors[:player], + 'No validation error given without player' + end + + test 'valid without url' do + notification = Notification.new(url: nil) + assert_empty notification.errors[:url], 'Validation error given with url' + end +end diff --git a/test/models/player_test.rb b/test/models/player_test.rb index 668626a..06d6757 100644 --- a/test/models/player_test.rb +++ b/test/models/player_test.rb @@ -4,18 +4,107 @@ # Tests players class PlayerTest < ActiveSupport::TestCase - test 'email should be optional' do + test 'valid player' do player = players(:carter) - assert player.valid?, player.errors + assert player.valid?, player.errors.messages end - test 'username should be present' do - blank_username = players(:felicity) - assert_not blank_username.valid?, 'Player should not be valid with a blank username.' + test 'invalid with blank username' do + player = Player.new(username: '') + assert_not player.valid?, 'Player is valid without a username' + assert_not_empty player.errors[:username], + 'No validation error present for player without a username' end - test 'first name should be present' do - blank_first_name = players(:greg) - assert_not blank_first_name.valid?, 'Player should not be valid with a blank first name.' + test 'invalid with blank first name' do + player = Player.new(first_name: '') + assert_not player.valid?, 'Player is valid without a first name' + assert_not_empty player.errors[:first_name], + 'No validation error present for player without a first '\ + 'name' + end + + test 'invalid with blank last name' do + player = Player.new(last_name: '') + assert_not player.valid?, 'Player is valid without a last name' + assert_not_empty player.errors[:last_name], + 'No validation error present for player without a last '\ + 'name' + end + + test 'invalid with long first name' do + player = Player.new(first_name: 'a' * 100) + assert_not player.valid?, 'Player is valid with long first name' + assert_not_empty player.errors[:first_name], + 'No validation error present for player with long first '\ + 'name' + end + + test 'invalid with long last name' do + player = Player.new(last_name: 'a' * 100) + assert_not player.valid?, 'Player is valid with long last name' + assert_not_empty player.errors[:last_name], + 'No validation error present for player with long last '\ + 'name' + end + + test 'invalid with long nickname' do + player = Player.new(nickname: 'a' * 100) + assert_not player.valid?, 'Player is valid with long nickname' + assert_not_empty player.errors[:nickname], + 'No validation error present for player with long '\ + 'nickname' + end + + test 'blank email should be nil' do + player = Player.new(email: '') + assert player.email.nil?, 'Email is not made nil when blank' + end + + test 'blank nickname should be nil' do + player = Player.new(nickname: '') + assert player.nickname.nil?, 'Nickname is not made nil when blank' + end + + test 'First name should be capitalised' do + error = 'First name was not appropriately capitalised' + + player = Player.new(first_name: 'harry') + assert player.first_name == 'Harry', error + + player.first_name = 'McKenzie' + assert player.first_name == 'McKenzie', error + end + + test 'Last name should be capitalised' do + error = 'Last name was not appropriately capitalised' + + player = Player.new(last_name: 'smith') + assert player.last_name == 'Smith', error + + player.last_name = 'McDonald' + assert player.last_name == 'McDonald', error + end + + test 'tickets should be clamped above 0' do + player = Player.new + player.tickets = -1 + assert player.tickets.zero?, 'Ticket count was not clamped above zero' + end + + test 'tickets should be an integer' do + player = Player.new(tickets: 10.5) + assert_not player.valid?, 'Player is valid with non-integer tickets' + assert_not_empty player.errors[:tickets], + 'No validation error present for player with non-integer'\ + ' tickets' + end + + test 'password changed should exist' do + player = Player.new(password_changed: nil) + assert_not player.valid?, 'Player is valid with nil password changed' + assert_not_empty player.errors[:password_changed], + 'No validation error present for player with nil password'\ + ' changed' end end diff --git a/test/models/referee_test.rb b/test/models/referee_test.rb index 7a4bf0c..8503495 100644 --- a/test/models/referee_test.rb +++ b/test/models/referee_test.rb @@ -2,6 +2,7 @@ require 'test_helper' +# Tests referees class RefereeTest < ActiveSupport::TestCase test 'game should be present' do referee = Referee.new(player: players(:eve)) diff --git a/test/models/region_test.rb b/test/models/region_test.rb index 944c1ff..d2022db 100644 --- a/test/models/region_test.rb +++ b/test/models/region_test.rb @@ -2,14 +2,33 @@ require 'test_helper' +# Test the region model class RegionTest < ActiveSupport::TestCase test 'name should not be blank' do region = Region.new(name: '') - assert_not region.valid?, 'Region should not be valid with a blank name.' + assert_not region.valid?, 'Region should not be valid with a blank name' + assert_not_empty region.errors[:name], + 'No validation error given for region with blank name' end test 'slug should not be blank' do - region = Region.new(name: 'Test', slug: '') - assert_not region.valid?, 'Region should not be valid with a blank slug.' + region = Region.new(slug: '') + assert_not region.valid?, 'Region should not be valid with a blank slug' + assert_not_empty region.errors[:slug], + 'No validation error given for region with blank slug' + end + + test 'name should not be nil' do + region = Region.new(name: nil) + assert_not region.valid?, 'Region should not be valid with a nil name' + assert_not_empty region.errors[:name], + 'No validation error given for region with nil name' + end + + test 'slug should not be nil' do + region = Region.new(slug: nil) + assert_not region.valid?, 'Region should not be valid with a nil slug' + assert_not_empty region.errors[:slug], + 'No validation error given for region with nil slug' end end diff --git a/test/models/season_test.rb b/test/models/season_test.rb index 08df0d6..d252173 100644 --- a/test/models/season_test.rb +++ b/test/models/season_test.rb @@ -2,12 +2,27 @@ require 'test_helper' +# Tests seasons class SeasonTest < ActiveSupport::TestCase test 'start date should be before end date' do season = Season.new( start_at: Time.zone.today, end_at: Time.zone.today - 1.day ) - assert_not season.valid?, 'start_at should be before end_at.' + assert_not season.valid?, 'start_at should be before end_at' + end + + test 'start date should exist' do + season = Season.new(start_at: nil) + assert_not season.valid?, 'Season is valid with nil start date' + assert_not_empty season.errors[:start_at], + 'No validation error present for season without start date' + end + + test 'end date should exist' do + season = Season.new(end_at: nil) + assert_not season.valid?, 'Season is valid with nil end date' + assert_not_empty season.errors[:end_at], + 'No validation error present for season without end date' end end diff --git a/test/models/venue_test.rb b/test/models/venue_test.rb index a8ea423..41dc721 100644 --- a/test/models/venue_test.rb +++ b/test/models/venue_test.rb @@ -2,14 +2,69 @@ require 'test_helper' +# Test the venue model class VenueTest < ActiveSupport::TestCase - test 'name should be present' do - venue = Venue.new(name: '', region: regions(:nabiac)) - assert_not venue.valid?, 'Venue should not be valid with a blank name.' + test 'name should not be blank' do + venue = Venue.new(name: '') + assert_not venue.valid?, 'Venue should not be valid with a blank name' + assert_not_empty venue.errors[:name], + 'No validation error for venue with blank name' + end + + test 'name should not be nil' do + venue = Venue.new(name: nil) + assert_not venue.valid?, 'Venue is valid with nil name' + assert_not_empty venue.errors[:name], + 'No validation error for venue with nil name' + end + + test 'name should be unique' do + # There is already a venue with this name in fixtures + venue = Venue.new(name: 'Nabiac Hotel') + assert_not venue.valid?, 'Venue is valid with non-unique name' + assert_not_empty venue.errors[:name], + 'No validation error for venue with non-unique name' + end + + test 'name should not be long' do + venue = Venue.new(name: 'a') + assert_not venue.valid?, 'Venue is valid with short name (1 char)' + assert_not_empty venue.errors[:name], + 'No validation error for venue with short name (1 char)' + end + + test 'name should not be short' do + venue = Venue.new(name: 'a' * 200) + assert_not venue.valid?, 'Venue is valid with long name (200 chars)' + assert_not_empty venue.errors[:name], + 'No validation error for venue with long name (200 chars)' end test 'slug should be present' do - venue = Venue.new(name: 'Alphabet', slug: '', region: regions(:nabiac)) - assert_not venue.valid?, 'Venue should not be valid with a blank slug.' + venue = Venue.new(slug: '') + assert_not venue.valid?, 'Venue should not be valid with a blank slug' + assert_not_empty venue.errors[:slug], + 'No validation error for venue with blank slug' + end + + test 'slug should not be nil' do + venue = Venue.new(slug: nil) + assert_not venue.valid?, 'Venue is valid with nil slug' + assert_not_empty venue.errors[:slug], + 'No validation error for venue with nil slug' + end + + test 'region should not be nil' do + venue = Venue.new(region: nil) + assert_not venue.valid?, 'Venue is valid without region' + assert_not_empty venue.errors[:region], + 'No validation error for venue without region' + end + + test 'website should be valid url' do + venue = Venue.new(website: 'a') + assert_not venue.valid?, 'Venue is valid with invalid URL' + assert_not_empty venue.errors[:website], + 'No validation error for invalid URL' end end diff --git a/test/test_helper.rb b/test/test_helper.rb index d17f685..474bb54 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,12 +1,19 @@ # frozen_string_literal: true +# Coveralls +require 'coveralls' +Coveralls.wear! + require File.expand_path('../config/environment', __dir__) require 'rails/test_help' + +# Sidekiq require 'sidekiq/testing' +Sidekiq::Testing.fake! +# Generic test case, extended by all other cases class ActiveSupport::TestCase - # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. + # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical + # order. fixtures :all - - Sidekiq::Testing.fake! end diff --git a/test/workers/comment_notification_worker_test.rb b/test/workers/comment_notification_worker_test.rb new file mode 100644 index 0000000..8149e9d --- /dev/null +++ b/test/workers/comment_notification_worker_test.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'test_helper' + +# Tests comment notificaiton worker +class CommentNotificationWorkerTest < MiniTest::Test +end diff --git a/test/workers/comment_notifications_worker_test.rb b/test/workers/comment_notifications_worker_test.rb new file mode 100644 index 0000000..7a1a5b9 --- /dev/null +++ b/test/workers/comment_notifications_worker_test.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'test_helper' + +# Tests comment notifications worker +class CommentNotificationsWorkerTest < MiniTest::Test +end diff --git a/test/workers/daily_action_worker_test.rb b/test/workers/daily_action_worker_test.rb new file mode 100644 index 0000000..ab1bdff --- /dev/null +++ b/test/workers/daily_action_worker_test.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'test_helper' + +# Tests daily action worker +class DailyActionWorkerTest < MiniTest::Test +end diff --git a/test/workers/daily_notification_worker_test.rb b/test/workers/daily_notification_worker_test.rb new file mode 100644 index 0000000..25e9487 --- /dev/null +++ b/test/workers/daily_notification_worker_test.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'test_helper' + +# Tests the daily notification worker +class DailyNotificationWorkerTest < Minitest::Test + def test_example + skip "add some examples to (or delete) #{__FILE__}" + end +end diff --git a/test/workers/game_notification_worker_test.rb b/test/workers/game_notification_worker_test.rb new file mode 100644 index 0000000..90c4fd0 --- /dev/null +++ b/test/workers/game_notification_worker_test.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'test_helper' + +# Tests sending game notifications +class GameNotificationWorkerTest < Minitest::Test + def test_example + skip "add some examples to (or delete) #{__FILE__}" + end +end diff --git a/test/workers/game_ticket_worker_test.rb b/test/workers/game_ticket_worker_test.rb new file mode 100644 index 0000000..522789d --- /dev/null +++ b/test/workers/game_ticket_worker_test.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'test_helper' + +# Tests game ticket workers +class GameTicketWorkerTest < MiniTest::Test +end diff --git a/test/workers/game_tickets_worker_test.rb b/test/workers/game_tickets_worker_test.rb new file mode 100644 index 0000000..c4a7bf7 --- /dev/null +++ b/test/workers/game_tickets_worker_test.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'test_helper' + +# Tests awarding game tickets +class GameTicketsWorkerTest < MiniTest::Test +end