From 8b129bbbf4bb03dbba9aa44dc5a529a5e7d56742 Mon Sep 17 00:00:00 2001 From: Georg Gadinger Date: Sun, 19 Apr 2020 18:33:20 +0200 Subject: [PATCH 01/27] Fix moderation panel. --- app/controllers/moderation_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/moderation_controller.rb b/app/controllers/moderation_controller.rb index dd23ec67..792fee95 100644 --- a/app/controllers/moderation_controller.rb +++ b/app/controllers/moderation_controller.rb @@ -57,7 +57,7 @@ class ModerationController < ApplicationController @host = User.find(@user_id) @users = [] return if @host.nil? - @users = User.where('(current_sign_in_ip = ? OR last_sign_in_ip = ?) AND id != ?', @host.current_sign_in_ip, @host.last_sign_in_ip, @user_id) + @users = User.where('(current_sign_in_ip = ? OR last_sign_in_ip = ?) AND id != ?', @host.current_sign_in_ip, @host.last_sign_in_ip, @user_id).to_a @users.unshift @host render template: 'moderation/priority' From 2237287472ce876f8782bd3b5354e0ab79ec4554 Mon Sep 17 00:00:00 2001 From: Georg Gadinger Date: Sun, 19 Apr 2020 19:43:55 +0200 Subject: [PATCH 02/27] Remove Capistrano --- Capfile | 18 ------------- Gemfile | 11 -------- Gemfile.lock | 30 --------------------- config/deploy.rb | 43 ------------------------------- config/deploy/production.rb | 39 ---------------------------- lib/capistrano/tasks/i18n.rake | 11 -------- lib/capistrano/tasks/restart.rake | 38 --------------------------- 7 files changed, 190 deletions(-) delete mode 100644 Capfile delete mode 100644 config/deploy.rb delete mode 100644 config/deploy/production.rb delete mode 100644 lib/capistrano/tasks/i18n.rake delete mode 100644 lib/capistrano/tasks/restart.rake diff --git a/Capfile b/Capfile deleted file mode 100644 index cfef1975..00000000 --- a/Capfile +++ /dev/null @@ -1,18 +0,0 @@ -# Load DSL and set up stages -require "capistrano/setup" - -# Include default deployment tasks -require "capistrano/deploy" - -# Load the SCM plugin -require "capistrano/scm/git" -install_plugin Capistrano::SCM::Git - -# Include tasks from other gems included in your Gemfile -#require "rvm1/capistrano3" -require "capistrano/bundler" -require "capistrano/rails/assets" -require "capistrano/rails/migrations" - -# Load custom tasks from `lib/capistrano/tasks` if you have any defined -Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r } diff --git a/Gemfile b/Gemfile index 88ed7c93..42a75408 100644 --- a/Gemfile +++ b/Gemfile @@ -82,17 +82,6 @@ group :development do gem 'web-console', '< 4.0.0' end -# Deployment -group :development do - gem 'capistrano', '~> 3.8', require: false - gem 'capistrano-rails', require: false - - gem 'rbnacl', '>= 3.2', '< 5.0', require: false - gem 'rbnacl-libsodium', require: false - gem 'bcrypt_pbkdf', '>= 1.0', '< 2.0', require: false - gem 'ed25519', require: false -end - group :production do gem 'unicorn', group: :production end diff --git a/Gemfile.lock b/Gemfile.lock index b21f3cef..44319795 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -67,13 +67,10 @@ GEM tzinfo (~> 1.1) addressable (2.7.0) public_suffix (>= 2.0.2, < 5.0) - airbrussh (1.4.0) - sshkit (>= 1.6.1, != 1.7.0) arel (9.0.0) autoprefixer-rails (9.7.6) execjs bcrypt (3.1.13) - bcrypt_pbkdf (1.0.1) better_errors (2.6.0) coderay (>= 1.0.0) erubi (>= 1.0.0) @@ -93,16 +90,6 @@ GEM buftok (0.2.0) builder (3.2.4) byebug (11.1.2) - capistrano (3.13.0) - airbrussh (>= 1.0.0) - i18n - rake (>= 10.0.0) - sshkit (>= 1.9.0) - capistrano-bundler (1.6.0) - capistrano (~> 3.1) - capistrano-rails (1.4.0) - capistrano (~> 3.1) - capistrano-bundler (~> 1.1) capybara (3.32.1) addressable mini_mime (>= 0.1.3) @@ -144,7 +131,6 @@ GEM docile (1.3.2) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - ed25519 (1.2.4) equalizer (0.0.11) erubi (1.9.0) excon (0.73.0) @@ -291,9 +277,6 @@ GEM naught (1.1.0) nenv (0.3.0) nested_form (0.3.2) - net-scp (2.0.0) - net-ssh (>= 2.6.5, < 6.0.0) - net-ssh (5.2.0) newrelic_rpm (6.10.0.364) nio4r (2.5.2) nokogiri (1.10.9) @@ -392,10 +375,6 @@ GEM rb-fsevent (0.10.3) rb-inotify (0.10.1) ffi (~> 1.0) - rbnacl (4.0.2) - ffi - rbnacl-libsodium (1.0.16) - rbnacl (>= 3.0.1) redcarpet (3.5.0) redis (4.1.3) regexp_parser (1.7.0) @@ -468,9 +447,6 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - sshkit (1.21.0) - net-scp (>= 1.1.2) - net-ssh (>= 2.8.0) sweetalert-rails (1.1.3) railties (>= 3.1.0) temple (0.8.2) @@ -525,7 +501,6 @@ PLATFORMS DEPENDENCIES bcrypt (~> 3.1.7) - bcrypt_pbkdf (>= 1.0, < 2.0) better_errors bootstrap-sass (~> 3.4.0) bootstrap3-datetimepicker-rails (~> 4.7.14) @@ -533,8 +508,6 @@ DEPENDENCIES bootswatch-rails brakeman byebug - capistrano (~> 3.8) - capistrano-rails capybara coffee-rails (~> 4.1) colorize @@ -543,7 +516,6 @@ DEPENDENCIES devise (~> 4.0) devise-async devise-i18n - ed25519 factory_bot_rails fake_email_validator faker @@ -580,8 +552,6 @@ DEPENDENCIES rails-i18n (~> 5.0) rails_admin rake - rbnacl (>= 3.2, < 5.0) - rbnacl-libsodium redcarpet redis rspec-rails (~> 3.9) diff --git a/config/deploy.rb b/config/deploy.rb deleted file mode 100644 index e84e4b49..00000000 --- a/config/deploy.rb +++ /dev/null @@ -1,43 +0,0 @@ -# config valid only for current version of Capistrano -lock "3.13.0" - -set :application, "retrospring" -set :repo_url, "git@git.rrerr.net:nilsding/retrospring.git" - -# Default branch is :master -ask :branch, `git rev-parse --abbrev-ref HEAD`.chomp - -# Default deploy_to directory is /var/www/my_app_name -set :deploy_to, "/home/justask/apps/retrospring" - -# Default value for :format is :airbrussh. -# set :format, :airbrussh - -# You can configure the Airbrussh format using :format_options. -# These are the defaults. -# set :format_options, command_output: true, log_file: "log/capistrano.log", color: :auto, truncate: :auto - -# Default value for :pty is false -# set :pty, true - -# Default value for :linked_files is [] -append :linked_files, "config/database.yml", "config/justask.yml", "config/secrets.yml", "config/unicorn.rb", - "config/newrelic.yml", "config/initializers/devise.rb" - -# Default value for linked_dirs is [] -append :linked_dirs, "log", "tmp/pids", "tmp/cache", "tmp/sockets", "public/uploads", "public/system", "public/exports" - -# Default value for default_env is {} -# set :default_env, { path: "/opt/ruby/bin:$PATH" } - -# Default value for keep_releases is 5 -# set :keep_releases, 5 - -# Ruby version / RVM -set :rvm1_ruby_version, '2.3.3@retrospring' - -# Create JS i18n files before precompiling assets -before 'deploy:assets:precompile', 'deploy:i18n_js' - -# Restart the app server after successful deploy -after 'deploy:cleanup', 'deploy:restart' diff --git a/config/deploy/production.rb b/config/deploy/production.rb deleted file mode 100644 index d2de3084..00000000 --- a/config/deploy/production.rb +++ /dev/null @@ -1,39 +0,0 @@ -# Defines a single server with a list of roles and multiple properties. -server "retrospring-001.aws.infra.retrospring.net", user: "justask", roles: %w{app db web} - -# Configuration -# ============= -# You can set any configuration variable like in config/deploy.rb -# These variables are then only loaded and set in this stage. -# For available Capistrano configuration variables see the documentation page. -# http://capistranorb.com/documentation/getting-started/configuration/ -# Feel free to add new variables to customise your setup. - -set :rails_env, :production - -# Custom SSH Options -# ================== -# You may pass any option but keep in mind that net/ssh understands a -# limited set of options, consult the Net::SSH documentation. -# http://net-ssh.github.io/net-ssh/classes/Net/SSH.html#method-c-start -# -# Global options -# -------------- -# set :ssh_options, { -# keys: %w(/home/rlisowski/.ssh/id_rsa), -# forward_agent: false, -# auth_methods: %w(password) -# } -# -# The server-based syntax can be used to override options: -# ------------------------------------ -# server "example.com", -# user: "user_name", -# roles: %w{web app}, -# ssh_options: { -# user: "user_name", # overrides user setting above -# keys: %w(/home/user_name/.ssh/id_rsa), -# forward_agent: false, -# auth_methods: %w(publickey password) -# # password: "please use keys" -# } diff --git a/lib/capistrano/tasks/i18n.rake b/lib/capistrano/tasks/i18n.rake deleted file mode 100644 index 89e64264..00000000 --- a/lib/capistrano/tasks/i18n.rake +++ /dev/null @@ -1,11 +0,0 @@ -namespace :deploy do - task :i18n_js do - on roles(:all) do - within release_path do - with rails_env: fetch(:rails_env), rails_groups: fetch(:rails_assets_groups) do - execute :rake, "i18n:js:export" - end - end - end - end -end diff --git a/lib/capistrano/tasks/restart.rake b/lib/capistrano/tasks/restart.rake deleted file mode 100644 index de18b8b1..00000000 --- a/lib/capistrano/tasks/restart.rake +++ /dev/null @@ -1,38 +0,0 @@ -namespace :deploy do - task :start do - on roles(:all) do -puts "------- skip start" -next - rvm_prefix = "#{fetch(:rvm1_auto_script_path)}/rvm-auto.sh #{fetch(:rvm1_ruby_version)}" - execute :tmux, 'new-session', - '-d', - '-s', 'retrospring', - '-n', 'retrospring', - '-c', '/usr/home/justask/apps/retrospring/current', - "'#{rvm_prefix} bundle exec foreman start'" - end - end - - task :stop do - on roles(:all) do -puts "------- skip stop" -next - execute :sh, '-c', '\'tmux list-panes -t retrospring -F "#{pane_pid}" | xargs kill\'' - end - end - - desc 'Restart the server' - task :restart do - on roles(:all) do -puts "------- skip restart" -next - info 'Restarting application server' - invoke('deploy:stop') - - info 'Waiting 5 seconds' - sleep 5 - - invoke('deploy:start') - end - end -end From 6999695ff8bb2c6bab8dc1d5d8eeffde0da50672 Mon Sep 17 00:00:00 2001 From: Georg Gadinger Date: Sun, 19 Apr 2020 19:49:47 +0200 Subject: [PATCH 03/27] Cleanup --- CODE_OF_CONDUCT.md | 128 ++++++++++++++++++++++++++++++ CONTRIBUTING.md | 23 ++++-- CONTRIBUTORS.txt | 10 --- app/views/shared/_links.html.haml | 2 +- 4 files changed, 146 insertions(+), 17 deletions(-) create mode 100644 CODE_OF_CONDUCT.md delete mode 100644 CONTRIBUTORS.txt diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..2363c157 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +team@retrospring.net. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b0bb2114..7ee0bfac 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,12 +1,18 @@ # Contributing -Thank you for reading this! This document has some notes on contributing to the development of Retrospring. +Thank you for reading this! This document has some notes on contributing to the +development of Retrospring. ## Reporting bugs -Before submitting an issue please check if there is already an existing issue. If there is, please add any additional information or give it a "+1" in the comments. +Before submitting an issue please check if there is already an existing issue. +If there is, please add any additional information or give it a "+1" in the +comments. -When submitting an issue please describe the issue clearly, including how to reproduce the bug, which situations it appears in, what you expect to happen, what actually happens, and what platform (browser and operating system) you are using. We find screenshots (for front-end issues) very helpful. +When submitting an issue please describe the issue clearly, including how to +reproduce the bug, which situations it appears in, what you expect to happen, +what actually happens, and what platform (browser and operating system) you are +using. We find screenshots (for front-end issues) very helpful. ## Pull Requests @@ -16,13 +22,18 @@ When submitting an issue please describe the issue clearly, including how to rep 4. Push to the branch (`git push origin feature-new`) 5. Create a new Pull Request -We love pull requests! We are very happy to work with you to get your changes merged in, however please keep the following in mind. +We love pull requests! We are very happy to work with you to get your changes +merged in, however please keep the following in mind. * Please use the core team standard of `feature-*` branch naming. * Adhere to the coding conventions you see in the surrounding code. * If you include a new feature also include tests, and make sure they'll pass. -* Before submitting a pull-request, clean up the history by going over your commits and squashing together minor changes and fixes into the corresponding commits. You can do this using the interactive rebase command. +* Before submitting a pull-request, clean up the history by going over your + commits and squashing together minor changes and fixes into the corresponding + commits. You can do this using the interactive rebase command. ## What to work on -If you want to know what you can help us with, like new features, check the Github issues or the [TODO](https://github.com/Retrospring/retrospring/blob/master/TODO). +If you want to know what you can help us with, like new features, check the +GitHub issues or the +[TODO](https://github.com/Retrospring/retrospring/blob/master/TODO). diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt deleted file mode 100644 index ec45db72..00000000 --- a/CONTRIBUTORS.txt +++ /dev/null @@ -1,10 +0,0 @@ -List of contributors, format is "Name @github " - -Andreas N. @pixeldesu -Georg G. @nilsding -Yuki @yukimono -Adel D.-K. -Howl -Iain D. -Jona H. @schisma @ix -Robin B. diff --git a/app/views/shared/_links.html.haml b/app/views/shared/_links.html.haml index 66245fbe..ed2f92b4 100644 --- a/app/views/shared/_links.html.haml +++ b/app/views/shared/_links.html.haml @@ -5,7 +5,7 @@ · = link_to t('views.general.about'), about_path · - = link_to "Github", 'https://github.com/retrospring/retrospring' + = link_to "GitHub", 'https://github.com/retrospring/retrospring' · = link_to t('views.general.terms'), terms_path · From 79f45ee20ec93aacec39f7cb3936f8d6e14dea76 Mon Sep 17 00:00:00 2001 From: Georg Gadinger Date: Sun, 19 Apr 2020 19:49:53 +0200 Subject: [PATCH 04/27] Surprise! --- README.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d1797620..dbd63a31 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,24 @@ -# Retrospring [![Build Status](https://travis-ci.org/Retrospring/retrospring.svg)](https://travis-ci.org/Retrospring/retrospring) +# Retrospring -This is the source code that powers Retrospring. This is a detached fork of [nilsding/justask](https://github.com/nilsding/justask), where we continue development. +This is the source code that powers Retrospring. This is a detached fork of +[nilsding/justask](https://github.com/nilsding/justask), where we continue +development. -Retrospring shut down on June 12, 2016. The source code will continue to be hosted here, as though no further updates will be made to it (there weren't any to begin with, though). +Retrospring shut down on June 12, 2016, but it's somehow still alive. You're +welcome. ## Installation -You can find all the installation instructions needed for a local/production setup of Retrospring in the [Wiki](https://github.com/Retrospring/retrospring/wiki/Setup)! +You can find all the installation instructions needed for a local/production +setup of Retrospring in the +[Wiki](https://github.com/Retrospring/retrospring/wiki/Setup). ## Contributing -Guidelines for Pull Requests and general information about how you can help us improving Retrospring can be found [here](https://github.com/Retrospring/retrospring/blob/master/CONTRIBUTING.md)! +Guidelines for Pull Requests and general information about how you can help us +improving Retrospring can be found +[here](https://github.com/Retrospring/retrospring/blob/master/CONTRIBUTING.md). ## License -[AGPLv3](https://github.com/Retrospring/retrospring/blob/master/LICENSE). +[AGPLv3](https://github.com/Retrospring/retrospring/blob/master/LICENSE). From 34604ae06f8d1ff1498d3214b8d2de918a216e0d Mon Sep 17 00:00:00 2001 From: Georg Gadinger Date: Sun, 19 Apr 2020 19:56:01 +0200 Subject: [PATCH 05/27] Remove Travis config --- .travis.yml | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 3dbed2e1..00000000 --- a/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -language: ruby -cache: bundler -rvm: 2.1 -services: -- redis-server -before_script: -- cp config/database.yml.postgres config/database.yml -- cp config/justask.yml.example config/justask.yml -- bundle exec rake db:drop db:create db:migrate db:test:prepare -notifications: - slack: - secure: "p+LieMEgRpQQimqT6+Up4cHpbFSyFWoEAzt3gl1yg+cECT2btJR0xkL10Z74gRhNcmcyrhP3IU10u+/RVclqvKAx39bG6r/+8kbyhCA1yXAJRScHwJMKRUqFwT8FV7UWZNyHEbpJmuC2M+VwGAf6PYZcNbfcjZzR8xPdM6+Mjq6mxNTMiJuGCTaL/+FPUOmiTTjl0ZNQqsbyScAtTWh8EMCUIrJ6fif2tESVY1g2DuknOhRAqs6SnhGXmOE+tmmi2bwx8dGULMbFKO9seeqGqHgRO/SJ7+o+qaPXC5pZmdl7gIjLubEQI8pZnLyYnm4mT7LE+LQzuhreHUiEOIvJ43Gxnpq9Dt48spQNIh3oViyw2JVbYE+RprFjfw6Dk1EtgfZ7qQEbUZwkguXZ98kz9ps6w/m3/fKWTOEHheHcCeuN35MnLkZfdUiodTsQuOnwSpMGsIWnOQP0XwiZ7bMshRET/Mg2amZhp6xcIt52ZZbC/ojDRoq+bakmjKWnq5mkZK5EqzTaHm9xo//nMNA0UIPYJyotgmauGc5xkSUHbyy/uf5xMRmcwuiss+VBCMI8z5a1uHkLGd805JU6Gr3OXp8fuiu8zWr80k4+w7vojsS84vvd2ntVktJswXRVrj/e1TdxrYCNY/32bKu23E24ygxtKt//fx0Jif0sCdSHXf4=" \ No newline at end of file From 8a632a09cd09310318d181b29c5b2f25013c4f29 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 19 Apr 2020 20:12:22 +0100 Subject: [PATCH 06/27] Create Announcement model & controller --- app/assets/javascripts/announcement.coffee | 3 +++ app/assets/stylesheets/announcement.scss | 3 +++ app/controllers/announcement_controller.rb | 20 +++++++++++++++++++ app/helpers/announcement_helper.rb | 2 ++ app/models/announcement.rb | 2 ++ app/views/announcement/edit.html.haml | 2 ++ app/views/announcement/index.html.haml | 8 ++++++++ app/views/announcement/new.html.haml | 2 ++ config/routes.rb | 7 +++++++ .../20200419183714_create_announcements.rb | 14 +++++++++++++ db/schema.rb | 14 ++++++++++++- spec/helpers/announcement_helper_spec.rb | 15 ++++++++++++++ spec/models/announcement_spec.rb | 5 +++++ 13 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/announcement.coffee create mode 100644 app/assets/stylesheets/announcement.scss create mode 100644 app/controllers/announcement_controller.rb create mode 100644 app/helpers/announcement_helper.rb create mode 100644 app/models/announcement.rb create mode 100644 app/views/announcement/edit.html.haml create mode 100644 app/views/announcement/index.html.haml create mode 100644 app/views/announcement/new.html.haml create mode 100644 db/migrate/20200419183714_create_announcements.rb create mode 100644 spec/helpers/announcement_helper_spec.rb create mode 100644 spec/models/announcement_spec.rb diff --git a/app/assets/javascripts/announcement.coffee b/app/assets/javascripts/announcement.coffee new file mode 100644 index 00000000..24f83d18 --- /dev/null +++ b/app/assets/javascripts/announcement.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/stylesheets/announcement.scss b/app/assets/stylesheets/announcement.scss new file mode 100644 index 00000000..e922d541 --- /dev/null +++ b/app/assets/stylesheets/announcement.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the announcement controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/controllers/announcement_controller.rb b/app/controllers/announcement_controller.rb new file mode 100644 index 00000000..1d52ff1e --- /dev/null +++ b/app/controllers/announcement_controller.rb @@ -0,0 +1,20 @@ +class AnnouncementController < ApplicationController + def index + @announcements = Announcement.all + end + + def new + end + + def create + end + + def edit + end + + def update + end + + def destroy + end +end diff --git a/app/helpers/announcement_helper.rb b/app/helpers/announcement_helper.rb new file mode 100644 index 00000000..b71ddc14 --- /dev/null +++ b/app/helpers/announcement_helper.rb @@ -0,0 +1,2 @@ +module AnnouncementHelper +end diff --git a/app/models/announcement.rb b/app/models/announcement.rb new file mode 100644 index 00000000..aa98a0e5 --- /dev/null +++ b/app/models/announcement.rb @@ -0,0 +1,2 @@ +class Announcement < ApplicationRecord +end diff --git a/app/views/announcement/edit.html.haml b/app/views/announcement/edit.html.haml new file mode 100644 index 00000000..26015ad5 --- /dev/null +++ b/app/views/announcement/edit.html.haml @@ -0,0 +1,2 @@ +

Announcement#edit

+

Find me in app/views/announcement/edit.html.erb

diff --git a/app/views/announcement/index.html.haml b/app/views/announcement/index.html.haml new file mode 100644 index 00000000..b7a759d4 --- /dev/null +++ b/app/views/announcement/index.html.haml @@ -0,0 +1,8 @@ +- provide(:title, generate_title("Announcements")) +.container.j2-page + .row + .col-md-12 + = link_to "Add new", :announcement_new, class: "btn btn-default" + - @announcements.each do |announcement| + .panel.panel-default + = announcement.content diff --git a/app/views/announcement/new.html.haml b/app/views/announcement/new.html.haml new file mode 100644 index 00000000..a14230c1 --- /dev/null +++ b/app/views/announcement/new.html.haml @@ -0,0 +1,2 @@ +

Announcement#new

+

Find me in app/views/announcement/new.html.erb

diff --git a/config/routes.rb b/config/routes.rb index 8176f058..bdc9a8c7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -127,5 +127,12 @@ Rails.application.routes.draw do match '/:username/groups(/p/:page)', to: 'user#groups', via: 'get', as: :show_user_groups, defaults: {page: 1} match '/:username/questions(/p/:page)', to: 'user#questions', via: 'get', as: :show_user_questions, defaults: {page: 1} + match "/admin/announcements", to: "announcement#index", via: :get, as: :announcement_index + match "/admin/announcements", to: "announcement#create", via: :post, as: :announcement_create + match "/admin/announcements/new", to: "announcement#new", via: :get, as: :announcement_new + match "/admin/announcements/:id/edit", to: "announcement#edit", via: :get, as: :announcement_edit + match "/admin/announcements/:id", to: "announcement#update", via: :patch, as: :announcement_update + match "/admin/announcements/:id", to: "announcement#destroy", via: :delete, as: :announcement_destroy + puts 'processing time of routes.rb: ' + "#{(Time.now - start).round(3).to_s.ljust(5, '0')}s".light_green end diff --git a/db/migrate/20200419183714_create_announcements.rb b/db/migrate/20200419183714_create_announcements.rb new file mode 100644 index 00000000..1aedd713 --- /dev/null +++ b/db/migrate/20200419183714_create_announcements.rb @@ -0,0 +1,14 @@ +class CreateAnnouncements < ActiveRecord::Migration[5.2] + def change + create_table :announcements do |t| + t.text :content, null: false + t.string :link_text + t.string :link_href + t.datetime :starts_at, null: false + t.datetime :ends_at, null: false + t.belongs_to :user, null: false + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 439625e6..215202c3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,11 +10,23 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2016_01_05_165913) do +ActiveRecord::Schema.define(version: 2020_04_19_183714) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "announcements", force: :cascade do |t| + t.text "content", null: false + t.string "link_text" + t.string "link_href" + t.datetime "starts_at", null: false + t.datetime "ends_at", null: false + t.bigint "user_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_announcements_on_user_id" + end + create_table "answers", id: :serial, force: :cascade do |t| t.text "content" t.integer "question_id" diff --git a/spec/helpers/announcement_helper_spec.rb b/spec/helpers/announcement_helper_spec.rb new file mode 100644 index 00000000..04b6d9e2 --- /dev/null +++ b/spec/helpers/announcement_helper_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the AnnouncementHelper. For example: +# +# describe AnnouncementHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe AnnouncementHelper, type: :helper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/announcement_spec.rb b/spec/models/announcement_spec.rb new file mode 100644 index 00000000..0bfc218b --- /dev/null +++ b/spec/models/announcement_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Announcement, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end From e3b89f734626484e8d2552517e8c51f6b2c26fca Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 19 Apr 2020 20:34:48 +0100 Subject: [PATCH 07/27] Implement creation of announcements --- app/controllers/announcement_controller.rb | 15 +++++++++++++++ app/models/announcement.rb | 1 + app/views/announcement/new.html.haml | 21 +++++++++++++++++++-- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/app/controllers/announcement_controller.rb b/app/controllers/announcement_controller.rb index 1d52ff1e..4ab15b7a 100644 --- a/app/controllers/announcement_controller.rb +++ b/app/controllers/announcement_controller.rb @@ -4,9 +4,18 @@ class AnnouncementController < ApplicationController end def new + @announcement = Announcement.new end def create + @announcement = Announcement.new(announcement_params) + @announcement.user = current_user + if @announcement.save + flash[:success] = "Announcement created successfully." + redirect_to action: :index + else + render 'announcement/new' + end end def edit @@ -17,4 +26,10 @@ class AnnouncementController < ApplicationController def destroy end + + private + + def announcement_params + params.require(:announcement).permit(:content, :link_text, :link_href, :starts_at, :ends_at) + end end diff --git a/app/models/announcement.rb b/app/models/announcement.rb index aa98a0e5..03010a0c 100644 --- a/app/models/announcement.rb +++ b/app/models/announcement.rb @@ -1,2 +1,3 @@ class Announcement < ApplicationRecord + belongs_to :user end diff --git a/app/views/announcement/new.html.haml b/app/views/announcement/new.html.haml index a14230c1..d41b08f1 100644 --- a/app/views/announcement/new.html.haml +++ b/app/views/announcement/new.html.haml @@ -1,2 +1,19 @@ -

Announcement#new

-

Find me in app/views/announcement/new.html.erb

+- provide(:title, generate_title("Add new announcement")) +.container.j2-page + = bootstrap_form_for(@announcement, url: {action: "create"}) do |f| + .row + .col-md-12 + = f.text_area :content, label: "Content" + .row + .col-md-6 + = f.url_field :link_href, label: "Link URL" + .col-md-6 + = f.datetime_field :link_text, label: "Link text" + .row + .col-md-6 + = f.datetime_field :starts_at, label: "Start time" + .col-md-6 + = f.datetime_field :ends_at, label: "End time" + .row + .col-md-12.text-right + = f.submit class: "btn btn-primary" From f14a168bcef2fd815fec7c6c7ea0afcc6e8a62fc Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 19 Apr 2020 20:50:33 +0100 Subject: [PATCH 08/27] Implement deletion of announcements --- app/controllers/announcement_controller.rb | 7 +++++++ app/views/announcement/index.html.haml | 8 +++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/controllers/announcement_controller.rb b/app/controllers/announcement_controller.rb index 4ab15b7a..f7219a4b 100644 --- a/app/controllers/announcement_controller.rb +++ b/app/controllers/announcement_controller.rb @@ -19,12 +19,19 @@ class AnnouncementController < ApplicationController end def edit + @announcement = Announcement.find(params[:id]) end def update end def destroy + if Announcement.destroy(params[:id]) + flash[:success] = "Announcement deleted successfully." + else + flash[:error] = "Failed to delete announcement." + end + redirect_to announcement_index_path end private diff --git a/app/views/announcement/index.html.haml b/app/views/announcement/index.html.haml index b7a759d4..a82a9066 100644 --- a/app/views/announcement/index.html.haml +++ b/app/views/announcement/index.html.haml @@ -5,4 +5,10 @@ = link_to "Add new", :announcement_new, class: "btn btn-default" - @announcements.each do |announcement| .panel.panel-default - = announcement.content + .panel-heading + = announcement.starts_at + .panel-body + = announcement.content + .panel-footer + = button_to "Edit", announcement_edit_path(id: announcement.id), class: 'btn btn-link' + = button_to "Delete", announcement_destroy_path(id: announcement.id), method: :delete, class: 'btn btn-link', confirm: 'Are you sure you want to delete this announcement?' \ No newline at end of file From 6187cb0b6c8b3a410f7c3fa122c31abeec225f2f Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 19 Apr 2020 20:58:57 +0100 Subject: [PATCH 09/27] Add the ability to edit announcements --- app/controllers/announcement_controller.rb | 6 ++++++ app/views/announcement/edit.html.haml | 21 +++++++++++++++++++-- app/views/announcement/index.html.haml | 2 +- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/app/controllers/announcement_controller.rb b/app/controllers/announcement_controller.rb index f7219a4b..7c4a9b0a 100644 --- a/app/controllers/announcement_controller.rb +++ b/app/controllers/announcement_controller.rb @@ -23,6 +23,12 @@ class AnnouncementController < ApplicationController end def update + if Announcement.update(params[:id], announcement_params) + flash[:success] = "Announcement updated successfully." + redirect_to announcement_index_path + else + render 'announcement/edit' + end end def destroy diff --git a/app/views/announcement/edit.html.haml b/app/views/announcement/edit.html.haml index 26015ad5..387ff707 100644 --- a/app/views/announcement/edit.html.haml +++ b/app/views/announcement/edit.html.haml @@ -1,2 +1,19 @@ -

Announcement#edit

-

Find me in app/views/announcement/edit.html.erb

+- provide(:title, generate_title("Edit announcement")) +.container.j2-page + = bootstrap_form_for(@announcement, url: {action: "update"}, method: "PATCH") do |f| + .row + .col-md-12 + = f.text_area :content, label: "Content" + .row + .col-md-6 + = f.url_field :link_href, label: "Link URL" + .col-md-6 + = f.datetime_field :link_text, label: "Link text" + .row + .col-md-6 + = f.datetime_field :starts_at, label: "Start time" + .col-md-6 + = f.datetime_field :ends_at, label: "End time" + .row + .col-md-12.text-right + = f.submit class: "btn btn-primary" diff --git a/app/views/announcement/index.html.haml b/app/views/announcement/index.html.haml index a82a9066..0d117c83 100644 --- a/app/views/announcement/index.html.haml +++ b/app/views/announcement/index.html.haml @@ -10,5 +10,5 @@ .panel-body = announcement.content .panel-footer - = button_to "Edit", announcement_edit_path(id: announcement.id), class: 'btn btn-link' + = button_to "Edit", announcement_edit_path(id: announcement.id), method: :get, class: 'btn btn-link' = button_to "Delete", announcement_destroy_path(id: announcement.id), method: :delete, class: 'btn btn-link', confirm: 'Are you sure you want to delete this announcement?' \ No newline at end of file From 2ecc746e235d91168b56984ef16755e7e54eb738 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 19 Apr 2020 21:11:22 +0100 Subject: [PATCH 10/27] Implement validation for announcements --- app/models/announcement.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/models/announcement.rb b/app/models/announcement.rb index 03010a0c..f2a7684f 100644 --- a/app/models/announcement.rb +++ b/app/models/announcement.rb @@ -1,3 +1,18 @@ class Announcement < ApplicationRecord belongs_to :user + + validates :content, presence: true + validates :starts_at, presence: true + validates :link_href, presence: true, if: -> { link_text.present? } + validate :starts_at, :validate_date_range + + def link_present? + link_text.present? + end + + def validate_date_range + if starts_at > ends_at + errors.add(:starts_at, "Start date must be before end date") + end + end end From 473f2cdcc52d81328fa5fc94608ec01dde2417b9 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 19 Apr 2020 21:26:55 +0100 Subject: [PATCH 11/27] Show announcement validation errors on the frontend --- app/controllers/announcement_controller.rb | 4 +++- app/views/announcement/edit.html.haml | 10 ++++++++++ app/views/announcement/new.html.haml | 10 ++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/app/controllers/announcement_controller.rb b/app/controllers/announcement_controller.rb index 7c4a9b0a..93083c1e 100644 --- a/app/controllers/announcement_controller.rb +++ b/app/controllers/announcement_controller.rb @@ -23,7 +23,9 @@ class AnnouncementController < ApplicationController end def update - if Announcement.update(params[:id], announcement_params) + @announcement = Announcement.find(params[:id]) + @announcement.update(announcement_params) + if @announcement.save flash[:success] = "Announcement updated successfully." redirect_to announcement_index_path else diff --git a/app/views/announcement/edit.html.haml b/app/views/announcement/edit.html.haml index 387ff707..ee7eb3ba 100644 --- a/app/views/announcement/edit.html.haml +++ b/app/views/announcement/edit.html.haml @@ -1,6 +1,16 @@ - provide(:title, generate_title("Edit announcement")) .container.j2-page = bootstrap_form_for(@announcement, url: {action: "update"}, method: "PATCH") do |f| + - if @announcement.errors.any? + .row + .col-md-12 + .alert.alert-danger + %strong + = pluralize(@announcement.errors.count, "error") + prohibited this announcement from being saved: + %ul + - @announcement.errors.full_messages.each do |err| + %li= err .row .col-md-12 = f.text_area :content, label: "Content" diff --git a/app/views/announcement/new.html.haml b/app/views/announcement/new.html.haml index d41b08f1..57ed3eaf 100644 --- a/app/views/announcement/new.html.haml +++ b/app/views/announcement/new.html.haml @@ -1,6 +1,16 @@ - provide(:title, generate_title("Add new announcement")) .container.j2-page = bootstrap_form_for(@announcement, url: {action: "create"}) do |f| + - if @announcement.errors.any? + .row + .col-md-12 + .alert.alert-danger + %strong + = pluralize(@announcement.errors.count, "error") + prohibited this announcement from being saved: + %ul + - @announcement.errors.full_messages.each do |err| + %li= err .row .col-md-12 = f.text_area :content, label: "Content" From a0c9641994ef74969e86d451f888456e7b95b448 Mon Sep 17 00:00:00 2001 From: Georg Gadinger Date: Sun, 19 Apr 2020 21:47:19 +0200 Subject: [PATCH 12/27] Remove useless specs --- .gitignore | 2 + .rubocop.yml | 8 +- spec/controllers/user_controller_spec.rb | 13 -- spec/factories/10_users.rb | 9 -- spec/factories/answers.rb | 6 - spec/factories/notifications.rb | 8 - spec/factories/questions.rb | 6 - spec/factories/users.rb | 21 +++ spec/features/users/banned_spec.rb | 45 ------ spec/features/users/follow_user_spec.rb | 28 ---- spec/features/users/inbox_spec.rb | 144 ----------------- spec/features/users/sign_in_spec.rb | 45 ------ spec/features/users/sign_out_spec.rb | 19 --- spec/features/users/user_show_spec.rb | 66 -------- spec/helpers/subscribe_helper_spec.rb | 15 -- spec/models/answer_spec.rb | 19 --- spec/models/notification_spec.rb | 5 - spec/rails_helper.rb | 51 +++++- spec/spec_helper.rb | 147 +++++++++--------- spec/support/devise.rb | 3 - .../{factory_girl.rb => factory_bot.rb} | 4 + spec/support/helpers.rb | 5 - spec/support/helpers/session_helpers.rb | 19 --- spec/support/simplecov.rb | 6 + spec/support/wait_for_ajax.rb | 15 -- 25 files changed, 155 insertions(+), 554 deletions(-) delete mode 100644 spec/controllers/user_controller_spec.rb delete mode 100644 spec/factories/10_users.rb delete mode 100644 spec/factories/answers.rb delete mode 100644 spec/factories/notifications.rb delete mode 100644 spec/factories/questions.rb create mode 100644 spec/factories/users.rb delete mode 100644 spec/features/users/banned_spec.rb delete mode 100644 spec/features/users/follow_user_spec.rb delete mode 100644 spec/features/users/inbox_spec.rb delete mode 100644 spec/features/users/sign_in_spec.rb delete mode 100644 spec/features/users/sign_out_spec.rb delete mode 100644 spec/features/users/user_show_spec.rb delete mode 100644 spec/helpers/subscribe_helper_spec.rb delete mode 100644 spec/models/answer_spec.rb delete mode 100644 spec/models/notification_spec.rb delete mode 100644 spec/support/devise.rb rename spec/support/{factory_girl.rb => factory_bot.rb} (56%) delete mode 100644 spec/support/helpers.rb delete mode 100644 spec/support/helpers/session_helpers.rb create mode 100644 spec/support/simplecov.rb delete mode 100644 spec/support/wait_for_ajax.rb diff --git a/.gitignore b/.gitignore index 2f31a86f..ccd5a704 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ desktop.ini # ignore exports /public/export + +/spec/examples.txt diff --git a/.rubocop.yml b/.rubocop.yml index e5740e5d..47316f61 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,5 +1,6 @@ AllCops: - RunRailsCops: true + Rails: + Enabled: true Exclude: - 'vendor/**/*' - 'db/schema.rb' @@ -7,7 +8,7 @@ AllCops: Metrics/ClassLength: Enabled: false -Metrics/LineLength: +Layout/LineLength: Enabled: false Metrics/MethodLength: @@ -16,9 +17,6 @@ Metrics/MethodLength: Metrics/ModuleLength: Enabled: false -Style/BracesAroundHashParameters: - Enabled: false - Style/ClassAndModuleChildren: Enabled: false diff --git a/spec/controllers/user_controller_spec.rb b/spec/controllers/user_controller_spec.rb deleted file mode 100644 index cfd08f08..00000000 --- a/spec/controllers/user_controller_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'rails_helper' - -RSpec.describe UserController, :type => :controller do - before do - @user = create(:user) - end - - it 'responds successfully with a HTTP 200 status code' do - get :show, username: @user.screen_name, page: 1 - expect(response).to be_success - expect(response).to have_http_status(200) - end -end diff --git a/spec/factories/10_users.rb b/spec/factories/10_users.rb deleted file mode 100644 index dc6400f4..00000000 --- a/spec/factories/10_users.rb +++ /dev/null @@ -1,9 +0,0 @@ -FactoryBot.define do - factory :user do |u| - u.sequence(:screen_name) { |n| "#{Faker::Internet.user_name 0..12, %w(_)}#{n}" } - u.sequence(:email) { |n| "#{n}#{Faker::Internet.email}" } - u.password { "P4s5w0rD" } - u.sequence(:confirmed_at) { Time.zone.now } - u.display_name { Faker::Name.name } - end -end diff --git a/spec/factories/answers.rb b/spec/factories/answers.rb deleted file mode 100644 index 64d7bd0c..00000000 --- a/spec/factories/answers.rb +++ /dev/null @@ -1,6 +0,0 @@ -FactoryBot.define do - factory :answer do |u| - u.sequence(:content) { |n| "This is an answer. I'm number #{n}!" } - u.user { FactoryBot.create(:user) } - end -end diff --git a/spec/factories/notifications.rb b/spec/factories/notifications.rb deleted file mode 100644 index 05dcc2fb..00000000 --- a/spec/factories/notifications.rb +++ /dev/null @@ -1,8 +0,0 @@ -FactoryBot.define do - factory :notification do - target_type { "MyString" } - target_id { 1 } - recipient_id { 1 } - new { false } - end -end diff --git a/spec/factories/questions.rb b/spec/factories/questions.rb deleted file mode 100644 index d35c40ca..00000000 --- a/spec/factories/questions.rb +++ /dev/null @@ -1,6 +0,0 @@ -FactoryBot.define do - factory :question do |u| - u.sequence(:content) { |n| "#{QuestionGenerator.generate}#{n}" } - u.author_is_anonymous { true } - end -end diff --git a/spec/factories/users.rb b/spec/factories/users.rb new file mode 100644 index 00000000..488ff726 --- /dev/null +++ b/spec/factories/users.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :user do + sequence(:screen_name) { |i| "#{Faker::Internet.username(specifier: 0..12, separators: %w(_))}#{i}" } + sequence(:email) { |i| "#{i}#{Faker::Internet.email}" } + password { "P4s5w0rD" } + confirmed_at { Time.now.utc } + display_name { Faker::Name.name } + + transient do + roles { [] } + end + + after(:create) do |user, evaluator| + evaluator.roles.each do |role| + user.add_role role + end + end + end +end diff --git a/spec/features/users/banned_spec.rb b/spec/features/users/banned_spec.rb deleted file mode 100644 index 4d041bb0..00000000 --- a/spec/features/users/banned_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -include Warden::Test::Helpers -Warden.test_mode! - -# Feature: Ban users -# As a user -# I want to get banned -# So I can't sign in anymore -feature "Ban users", :devise do - - after :each do - Warden.test_reset! - end - - # Scenario: User gets banned - # Given I am signed in - # When I visit another page - # And I am banned - # Then I see the sign in page - scenario "user gets banned", js: true do - me = FactoryBot.create :user - - login_as me, scope: :user - visit root_path - expect(page).to have_text("Timeline") - page.driver.render Rails.root.join("tmp/ban_#{Time.now.to_i}_1.png"), full: true - - me.permanently_banned = true - me.save - - visit "/inbox" - - expect(current_path).to eq(new_user_session_path) - page.driver.render Rails.root.join("tmp/ban_#{Time.now.to_i}_2.png"), full: true - end - - scenario 'user visits banned user profiles', js: true do - evil_user = FactoryBot.create :user - evil_user.permanently_banned = true - evil_user.save - - visit show_user_profile_path(evil_user.screen_name) - expect(page).to have_text('BANNED') - page.driver.render Rails.root.join("tmp/ban_#{Time.now.to_i}_3.png"), full: true - end -end diff --git a/spec/features/users/follow_user_spec.rb b/spec/features/users/follow_user_spec.rb deleted file mode 100644 index d028f751..00000000 --- a/spec/features/users/follow_user_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -include Warden::Test::Helpers -Warden.test_mode! - -feature "User profile page", :devise do - - after :each do - Warden.test_reset! - end - - scenario "user gets followed", js: true do - me = FactoryBot.create(:user) - other = FactoryBot.create(:user) - - login_as me, scope: :user - visit show_user_profile_path(other.screen_name) - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_1.png"), full: true - - click_button "Follow" - wait_for_ajax - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_2.png"), full: true - - expect(page).to have_text("FOLLOWING") - - click_link 'Follower' - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_3.png"), full: true - expect(page).to have_text(me.screen_name) - end -end diff --git a/spec/features/users/inbox_spec.rb b/spec/features/users/inbox_spec.rb deleted file mode 100644 index 12f38e38..00000000 --- a/spec/features/users/inbox_spec.rb +++ /dev/null @@ -1,144 +0,0 @@ -include Warden::Test::Helpers -Warden.test_mode! - -# Feature: Answer questions -# As a user -# I want to go to the inbox -# So I can answer and get new questions -feature "Inbox", :devise do - - after :each do - Warden.test_reset! - end - - # Scenario: User answers a question - # Given I am signed in - # When I visit the inbox - # And I have a question in my inbox - # Then I can answer my question - # And see the answer on my user profile - scenario "user answers a question", js: true do - me = FactoryBot.create :user - question = FactoryBot.create :question - Inbox.create question: question, user: me, new: true - - login_as me, scope: :user - visit root_path - - click_link "Inbox" - expect(page).to have_text(question.content) - fill_in "ib-answer", with: Faker::Lorem.sentence - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_2.png"), full: true - - click_button "Answer" - wait_for_ajax - expect(page).not_to have_text(question.content) - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_3.png"), full: true - - visit show_user_profile_path(me.screen_name) - expect(page).to have_text(question.content) - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_4.png"), full: true - end - - # Scenario: User generates new question - # Given I am signed in - # When I visit the inbox - # And I click "Get new question" - # Then I get a new question - scenario 'user generates new question', js: true do - me = FactoryBot.create :user - - login_as me, scope: :user - visit inbox_path - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_1.png"), full: true - - click_button "Get new question" - wait_for_ajax - expect(page).to have_text('Answer') - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_2.png"), full: true - end - - # Scenario: User with privacy options generates new question - # Given I am signed in - # When I visit the inbox - # And I click "Get new question" - # And I don't want to receive questions by anonymous users - # Then I get a new question - scenario 'user with privacy options generates new question', js: true do - me = FactoryBot.create :user - me.privacy_allow_anonymous_questions = false - me.save - - login_as me, scope: :user - visit inbox_path - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_1.png"), full: true - - click_button "Get new question" - wait_for_ajax - expect(page).to have_text('Answer') - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_2.png"), full: true - end - -=begin - # Scenario: User deletes a question - # Given I am signed in - # When I visit the inbox - # And I have a question in my inbox - # And I delete the question - # Then don't see it anymore in my inbox - scenario "user deletes a question", js: true do - me = FactoryBot.create :user - question = FactoryBot.create :question - Inbox.create question: question, user: me - - login_as me, scope: :user - visit inbox_path - expect(page).to have_text(question.content) - - click_button "Delete" - expect(page).to have_text('Really delete?') - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_1.png"), full: true - - # this apparently doesn't get triggered :( - page.find('.sweet-alert').click_button 'Delete' - wait_for_ajax - - login_as me, scope: :user - visit inbox_path - expect(page).not_to have_text(question.content) - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_2.png"), full: true - end - - # Scenario: User deletes all questions - # Given I am signed in - # When I visit the inbox - # And I have a few questions in my inbox - # And I click on "Delete all questions" - # Then don't see them anymore in my inbox - scenario "user deletes all questions", js: true do - me = FactoryBot.create :user - 5.times do - question = FactoryBot.create :question - Inbox.create question: question, user: me - end - - login_as me, scope: :user - visit inbox_path - expect(page).to have_text('Answer'.upcase) - - click_button "Delete all questions" - expect(page).to have_text('Really delete 5 questions?') - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_1.png"), full: true - - page.find('.sweet-alert').click_button 'Delete' - wait_for_ajax - - puts me.inbox.all - - login_as me, scope: :user - visit inbox_path - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_2.png"), full: true - expect(page).not_to have_text('Answer'.upcase) - end -=end -end diff --git a/spec/features/users/sign_in_spec.rb b/spec/features/users/sign_in_spec.rb deleted file mode 100644 index d9945eb8..00000000 --- a/spec/features/users/sign_in_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -# Feature: Sign in -# As a user -# I want to sign in -# So I can visit protected areas of the site -feature 'Sign in', :devise do - - scenario 'user cannot sign in if not registered', js: true do - user = FactoryBot.build(:user) - signin(user.screen_name, user.password) - expect(page).to have_content I18n.t 'devise.failure.not_found_in_database', authentication_keys: 'login' - end - - # Scenario: User can sign in with valid credentials - # Given I exist as a user - # And I am not signed in - # When I sign in with valid credentials - # Then I see a success message - scenario 'user can sign in with valid credentials', js: true do - user = FactoryBot.create(:user) - signin(user.email, user.password) - expect(page).to have_content I18n.t 'devise.sessions.signed_in' - end - - # Scenario: User cannot sign in with wrong email - # Given I exist as a user - # And I am not signed in - # When I sign in with a wrong email - # Then I see an invalid email message - scenario 'user cannot sign in with wrong email', js: true do - user = FactoryBot.create(:user) - signin('invalid@email.com', user.password) - expect(page).to have_content I18n.t 'devise.failure.not_found_in_database', authentication_keys: 'login' - end - - # Scenario: User cannot sign in with wrong password - # Given I exist as a user - # And I am not signed in - # When I sign in with a wrong password - # Then I see an invalid password message - scenario 'user cannot sign in with wrong password', js: true do - user = FactoryBot.create(:user) - signin(user.email, 'what the fuck is my p4s5w0rD again?') - expect(page).to have_content I18n.t 'devise.failure.invalid', authentication_keys: 'login' - end -end \ No newline at end of file diff --git a/spec/features/users/sign_out_spec.rb b/spec/features/users/sign_out_spec.rb deleted file mode 100644 index 2e027e9b..00000000 --- a/spec/features/users/sign_out_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -# Feature: Sign out -# As a user -# I want to sign out -# So I can protect my account from unauthorized access -feature 'Sign out', :devise do - - # Scenario: User signs out successfully - # Given I am signed in - # When I sign out - # Then I see a signed out message - scenario 'user signs out successfully', js: true do - user = FactoryBot.create(:user) - signin(user.email, user.password) - expect(page).to have_content I18n.t 'devise.sessions.signed_in' - click_link user.screen_name - click_link 'Logout' - expect(page).to have_content I18n.t 'devise.sessions.signed_out' - end -end \ No newline at end of file diff --git a/spec/features/users/user_show_spec.rb b/spec/features/users/user_show_spec.rb deleted file mode 100644 index ef7a5834..00000000 --- a/spec/features/users/user_show_spec.rb +++ /dev/null @@ -1,66 +0,0 @@ -include Warden::Test::Helpers -Warden.test_mode! - -# Feature: User profile page -# As a user -# I want to visit my user profile page -# So I can see my personal account data -feature "User profile page", :devise do - - after :each do - Warden.test_reset! - end - - # Scenario: User sees own profile - # Given I am signed in - # When I visit the user profile page - # Then I see my own screen name and follower count - scenario 'user sees own profile', js: true do - user = FactoryBot.create(:user) - - login_as(user, :scope => :user) - - visit show_user_profile_path(user.screen_name) - - expect(page).to have_content user.screen_name - expect(page).to have_content user.follower_count - end - - # Scenario: User sees another user's profile - # Given I am signed in - # When I visit another user's profile - # Then I see that user's screen name and follower count - scenario "user sees another user's profile", js: true do - me = FactoryBot.create(:user) - other = FactoryBot.create(:user) - - login_as me, scope: :user - - visit show_user_profile_path(other.screen_name) - - expect(page).to have_content other.screen_name - expect(page).to have_content other.follower_count - end - - # Scenario: User gets asked a question - # Given I am signed in - # When I visit another user's profile - # And I fill something in the question box - # And I click on "Ask" - # Then I see "Question asked successfully." - scenario "user gets asked a question", js: true do - me = FactoryBot.create(:user) - other = FactoryBot.create(:user) - - login_as me, scope: :user - visit show_user_profile_path(other.screen_name) - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_1.png"), full: true - - fill_in "qb-question", with: Faker::Lorem.sentence - click_button "Ask" - wait_for_ajax - page.driver.render Rails.root.join("tmp/#{Time.now.to_i}_2.png"), full: true - - expect(page).to have_text("Question asked successfully.") - end -end diff --git a/spec/helpers/subscribe_helper_spec.rb b/spec/helpers/subscribe_helper_spec.rb deleted file mode 100644 index cb8dc23c..00000000 --- a/spec/helpers/subscribe_helper_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'rails_helper' - -# Specs in this file have access to a helper object that includes -# the SubscribeHelper. For example: -# -# describe SubscribeHelper do -# describe "string concat" do -# it "concats two strings with spaces" do -# expect(helper.concat_strings("this","that")).to eq("this that") -# end -# end -# end -RSpec.describe SubscribeHelper, :type => :helper do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/models/answer_spec.rb b/spec/models/answer_spec.rb deleted file mode 100644 index 44a3f12b..00000000 --- a/spec/models/answer_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'rails_helper' - -RSpec.describe Answer, :type => :model do - before :each do - @answer = Answer.new( - content: 'This is an answer.', - user: FactoryBot.create(:user), - question: FactoryBot.create(:question) - ) - end - - subject { @answer } - - it { should respond_to(:content) } - - it '#content returns a string' do - expect(@answer.content).to match 'This is an answer.' - end -end diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb deleted file mode 100644 index f79213f9..00000000 --- a/spec/models/notification_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -RSpec.describe Notification, :type => :model do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 5bbadc73..7517cd89 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -1,8 +1,16 @@ +# frozen_string_literal: true + # This file is copied to spec/ when you run 'rails generate rspec:install' -ENV["RAILS_ENV"] ||= 'test' -require 'spec_helper' +ENV["RAILS_ENV"] ||= "test" require File.expand_path("../../config/environment", __FILE__) -require 'rspec/rails' +# Prevent database truncation if the environment is production +abort("The Rails environment is running in production mode!") if Rails.env.production? +require "spec_helper" +require "rspec/rails" +# Add additional requires below this line. Rails is not loaded until this point! +require "devise" +require "capybara/rails" +require "capybara/rspec" # Requires supporting ruby files with custom matchers and macros, etc, in # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are @@ -11,20 +19,40 @@ require 'rspec/rails' # run twice. It is recommended that you do not name files matching this glob to # end with _spec.rb. You can configure this pattern with the --pattern # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. -Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } -# Checks for pending migrations before tests are run. +# Checks for pending migration and applies them before tests are run. # If you are not using ActiveRecord, you can remove this line. ActiveRecord::Migration.maintain_test_schema! RSpec.configure do |config| + config.before :suite do + # Run webpack compilation before suite, so assets exists in public/packs + # It would be better to run the webpack compilation only if at least one :js spec + # should be executed, but `when_first_matching_example_defined` + # does not work with `config.infer_spec_type_from_file_location!` + # see https://github.com/rspec/rspec-core/issues/2366 + + unless ENV.key?("DISABLE_WEBPACK_IN_TESTS") + start = Time.now # rubocop:disable all + `bin/webpack` + puts "processing time of webpack: \033[32;1m#{(Time.now - start).round(3).to_s.ljust(5, '0')}s\033[0m" # rubocop:disable all + end + end + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures - # config.fixture_path = "#{::Rails.root}/spec/fixtures" + config.fixture_path = "#{::Rails.root}/spec/fixtures" # If you're not using ActiveRecord, or you'd prefer not to run each of your # examples within a transaction, remove the following line or assign false # instead of true. - config.use_transactional_fixtures = false + config.use_transactional_fixtures = true # RSpec Rails can automatically mix in different behaviours to your tests # based on their file location, for example enabling you to call `get` and @@ -40,4 +68,13 @@ RSpec.configure do |config| # The different available types are documented in the features, such as in # https://relishapp.com/rspec/rspec-rails/docs config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") + + config.include Devise::Test::ControllerHelpers, type: :controller end + +Dir[Rails.root.join "spec", "shared_examples", "*.rb"].sort.each { |f| require f } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 10b74ba3..77ae7ed2 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,99 +1,102 @@ -require 'simplecov' -require 'simplecov-json' -require 'simplecov-rcov' +# frozen_string_literal: true -SimpleCov.formatters = [ - SimpleCov::Formatter::HTMLFormatter, - SimpleCov::Formatter::JSONFormatter, - SimpleCov::Formatter::RcovFormatter -] -SimpleCov.start - -require 'capybara/poltergeist' -Capybara.register_driver :poltergeist do |app| - Capybara::Poltergeist::Driver.new app, - js_errors: false, - timeout: 180 -end -Capybara.javascript_driver = :poltergeist - -require 'factory_bot_rails' +require "support/simplecov" +require "support/factory_bot" # This file was generated by the `rails generate rspec:install` command. Conventionally, all # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. -# The generated `.rspec` file contains `--require spec_helper` which will cause this -# file to always be loaded, without a need to explicitly require it in any files. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. # # Given that it is always loaded, you are encouraged to keep this file as # light-weight as possible. Requiring heavyweight dependencies from this file # will add to the boot time of your test suite on EVERY test run, even for an -# individual file that may not need all of that loaded. Instead, make a -# separate helper file that requires this one and then use it only in the specs -# that actually need it. +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. # # The `.rspec` file also contains a few flags that are not defaults but that # users commonly want. # # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration RSpec.configure do |config| -# The settings below are suggested to provide a good initial experience -# with RSpec, but feel free to customize to your heart's content. -=begin - # These two settings work together to allow you to limit a spec run - # to individual examples or groups you care about by tagging them with - # `:focus` metadata. When nothing is tagged with `:focus`, all examples - # get run. - config.filter_run :focus - config.run_all_when_everything_filtered = true - - # Many RSpec users commonly either run the entire suite or an individual - # file, and it's useful to allow more verbose output when running an - # individual spec file. - if config.files_to_run.one? - # Use the documentation formatter for detailed output, - # unless a formatter has already been configured - # (e.g. via a command-line flag). - config.default_formatter = 'doc' - end - - # Print the 10 slowest examples and example groups at the - # end of the spec run, to help surface which specs are running - # particularly slow. - config.profile_examples = 10 - - # Run specs in random order to surface order dependencies. If you find an - # order dependency and want to debug it, you can fix the order by providing - # the seed, which is printed after each run. - # --seed 1234 - config.order = :random - - # Seed global randomization in this process using the `--seed` CLI option. - # Setting this allows you to use `--seed` to deterministically reproduce - # test failures related to randomization by passing the same `--seed` value - # as the one that triggered the failure. - Kernel.srand config.seed - # rspec-expectations config goes here. You can use an alternate # assertion/expectation library such as wrong or the stdlib/minitest # assertions if you prefer. config.expect_with :rspec do |expectations| - # Enable only the newer, non-monkey-patching expect syntax. - # For more details, see: - # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax - expectations.syntax = :expect + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true end # rspec-mocks config goes here. You can use an alternate test double # library (such as bogus or mocha) by changing the `mock_with` option here. config.mock_with :rspec do |mocks| - # Enable only the newer, non-monkey-patching expect syntax. - # For more details, see: - # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ - mocks.syntax = :expect - # Prevents you from mocking or stubbing a method that does not exist on - # a real object. This is generally recommended. + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. mocks.verify_partial_doubles = true end -=end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + + # The settings below are suggested to provide a good initial experience + # with RSpec, but feel free to customize to your heart's content. + # # This allows you to limit a spec run to individual examples or groups + # # you care about by tagging them with `:focus` metadata. When nothing + # # is tagged with `:focus`, all examples get run. RSpec also provides + # # aliases for `it`, `describe`, and `context` that include `:focus` + # # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + # config.filter_run_when_matching :focus + # + # # Allows RSpec to persist some state between runs in order to support + # # the `--only-failures` and `--next-failure` CLI options. We recommend + # # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + # + # # Limits the available syntax to the non-monkey patched syntax that is + # # recommended. For more details, see: + # # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + # config.disable_monkey_patching! + # + # # Many RSpec users commonly either run the entire suite or an individual + # # file, and it's useful to allow more verbose output when running an + # # individual spec file. + # if config.files_to_run.one? + # # Use the documentation formatter for detailed output, + # # unless a formatter has already been configured + # # (e.g. via a command-line flag). + # config.default_formatter = 'doc' + # end + # + # # Print the 10 slowest examples and example groups at the + # # end of the spec run, to help surface which specs are running + # # particularly slow. + # config.profile_examples = 10 + # + # # Run specs in random order to surface order dependencies. If you find an + # # order dependency and want to debug it, you can fix the order by providing + # # the seed, which is printed after each run. + # # --seed 1234 + # config.order = :random + # + # # Seed global randomization in this process using the `--seed` CLI option. + # # Setting this allows you to use `--seed` to deterministically reproduce + # # test failures related to randomization by passing the same `--seed` value + # # as the one that triggered the failure. + # Kernel.srand config.seed end diff --git a/spec/support/devise.rb b/spec/support/devise.rb deleted file mode 100644 index 21dd1e8b..00000000 --- a/spec/support/devise.rb +++ /dev/null @@ -1,3 +0,0 @@ -RSpec.configure do |config| - config.include Devise::TestHelpers, type: :controller -end \ No newline at end of file diff --git a/spec/support/factory_girl.rb b/spec/support/factory_bot.rb similarity index 56% rename from spec/support/factory_girl.rb rename to spec/support/factory_bot.rb index c7890e49..31663a9d 100644 --- a/spec/support/factory_girl.rb +++ b/spec/support/factory_bot.rb @@ -1,3 +1,7 @@ +# frozen_string_literal: true + +require "factory_bot_rails" + RSpec.configure do |config| config.include FactoryBot::Syntax::Methods end diff --git a/spec/support/helpers.rb b/spec/support/helpers.rb deleted file mode 100644 index 5730fdb5..00000000 --- a/spec/support/helpers.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'support/helpers/session_helpers' - -RSpec.configure do |config| - config.include Features::SessionHelpers, type: :feature -end \ No newline at end of file diff --git a/spec/support/helpers/session_helpers.rb b/spec/support/helpers/session_helpers.rb deleted file mode 100644 index 6ee1933e..00000000 --- a/spec/support/helpers/session_helpers.rb +++ /dev/null @@ -1,19 +0,0 @@ -module Features - module SessionHelpers - def sign_up_with(screen_name, email, password, confirmation) - visit new_user_registration_path - fill_in 'User name', with: screen_name - fill_in 'Email address', with: email - fill_in 'Password', with: password - fill_in 'Password confirmation', :with => confirmation - click_button 'Sign up' - end - - def signin(email, password) - visit new_user_session_path - fill_in 'User name', with: email - fill_in 'Password', with: password - click_button 'Sign in' - end - end -end \ No newline at end of file diff --git a/spec/support/simplecov.rb b/spec/support/simplecov.rb new file mode 100644 index 00000000..2cc94286 --- /dev/null +++ b/spec/support/simplecov.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require "simplecov" +# require "simplecov-rcov" +# SimpleCov.formatter = SimpleCov::Formatter::RcovFormatter +SimpleCov.start "rails" diff --git a/spec/support/wait_for_ajax.rb b/spec/support/wait_for_ajax.rb deleted file mode 100644 index 9d772404..00000000 --- a/spec/support/wait_for_ajax.rb +++ /dev/null @@ -1,15 +0,0 @@ -module WaitForAjax - def wait_for_ajax - Timeout.timeout(Capybara.default_wait_time) do - loop until finished_all_ajax_requests? - end - end - - def finished_all_ajax_requests? - page.evaluate_script('jQuery.active').zero? - end -end - -RSpec.configure do |config| - config.include WaitForAjax, type: :feature -end From 946bb3ae9d8714550a009869355dd02cf7c2f89d Mon Sep 17 00:00:00 2001 From: Georg Gadinger Date: Sun, 19 Apr 2020 22:35:58 +0200 Subject: [PATCH 13/27] Use Rolify for admin and moderator roles --- Gemfile | 2 + Gemfile.lock | 2 + Rakefile | 52 ++++++++++--------- app/controllers/ajax/moderation_controller.rb | 20 ++++--- app/models/role.rb | 15 ++++++ app/models/user.rb | 10 +++- app/views/layouts/_profile.html.haml | 2 +- app/views/user/_actions.html.haml | 2 +- app/views/user/_modal_privileges.html.haml | 2 +- .../user/_modal_privileges_item.html.haml | 5 +- app/views/user/_profile_info.html.haml | 6 +-- app/views/user/data.html.haml | 2 +- config/initializers/rails_admin.rb | 5 +- config/initializers/rolify.rb | 12 +++++ config/routes.rb | 14 ++--- .../20200419184442_rolify_create_roles.rb | 20 +++++++ .../20200419185535_create_initial_roles.rb | 34 ++++++++++++ db/schema.rb | 20 ++++++- db/seeds.rb | 4 ++ lib/exporter.rb | 21 ++++++-- .../role_constrained_routes_spec.rb | 51 ++++++++++++++++++ 21 files changed, 246 insertions(+), 55 deletions(-) create mode 100644 app/models/role.rb create mode 100644 config/initializers/rolify.rb create mode 100644 db/migrate/20200419184442_rolify_create_roles.rb create mode 100644 db/migrate/20200419185535_create_initial_roles.rb create mode 100644 spec/integration/role_constrained_routes_spec.rb diff --git a/Gemfile b/Gemfile index 42a75408..e20ff191 100644 --- a/Gemfile +++ b/Gemfile @@ -42,6 +42,8 @@ gem 'tiny-color-rails' gem 'jquery-minicolors-rails' gem 'colorize' +gem "rolify", "~> 5.2" + source "https://rails-assets.org" do gem 'rails-assets-growl' gem 'rails-assets-jquery', '~> 2.2.0' diff --git a/Gemfile.lock b/Gemfile.lock index 44319795..c70c9ee6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -382,6 +382,7 @@ GEM responders (3.0.0) actionpack (>= 5.0) railties (>= 5.0) + rolify (5.2.0) rspec-core (3.9.1) rspec-support (~> 3.9.1) rspec-expectations (3.9.1) @@ -554,6 +555,7 @@ DEPENDENCIES rake redcarpet redis + rolify (~> 5.2) rspec-rails (~> 3.9) ruby-progressbar sanitize diff --git a/Rakefile b/Rakefile index 6ce0e0dc..ab71e59f 100644 --- a/Rakefile +++ b/Rakefile @@ -105,44 +105,48 @@ namespace :justask do end end - desc "Gives admin status to an user." + desc "Gives admin status to a user." task :admin, [:screen_name] => :environment do |t, args| - fail "screen name required" if args[:screen_name].nil? + abort "screen name required" if args[:screen_name].nil? + user = User.find_by_screen_name(args[:screen_name]) - fail "user #{args[:screen_name]} not found" if user.nil? - user.admin = true - user.save! - puts "#{user.screen_name} is now an admin." + abort "user #{args[:screen_name]} not found" if user.nil? + + user.add_role :administrator + puts "#{user.screen_name} is now an administrator." end - desc "Removes admin status from an user." + desc "Removes admin status from a user." task :deadmin, [:screen_name] => :environment do |t, args| - fail "screen name required" if args[:screen_name].nil? + abort "screen name required" if args[:screen_name].nil? + user = User.find_by_screen_name(args[:screen_name]) - fail "user #{args[:screen_name]} not found" if user.nil? - user.admin = false - user.save! - puts "#{user.screen_name} is no longer an admin." + abort "user #{args[:screen_name]} not found" if user.nil? + + user.remove_role :administrator + puts "#{user.screen_name} is no longer an administrator." end - desc "Gives moderator status to an user." + desc "Gives moderator status to a user." task :mod, [:screen_name] => :environment do |t, args| - fail "screen name required" if args[:screen_name].nil? + abort "screen name required" if args[:screen_name].nil? + user = User.find_by_screen_name(args[:screen_name]) - fail "user #{args[:screen_name]} not found" if user.nil? - user.moderator = true - user.save! - puts "#{user.screen_name} is now an moderator." + abort "user #{args[:screen_name]} not found" if user.nil? + + user.add_role :moderator + puts "#{user.screen_name} is now a moderator." end - desc "Removes moderator status from an user." + desc "Removes moderator status from a user." task :demod, [:screen_name] => :environment do |t, args| - fail "screen name required" if args[:screen_name].nil? + abort "screen name required" if args[:screen_name].nil? + user = User.find_by_screen_name(args[:screen_name]) - fail "user #{args[:screen_name]} not found" if user.nil? - user.moderator = false - user.save! - puts "#{user.screen_name} is no longer an moderator." + abort "user #{args[:screen_name]} not found" if user.nil? + + user.remove_role :moderator + puts "#{user.screen_name} is no longer a moderator." end desc "Hits an user with the banhammer." diff --git a/app/controllers/ajax/moderation_controller.rb b/app/controllers/ajax/moderation_controller.rb index 26889361..9ec82abb 100644 --- a/app/controllers/ajax/moderation_controller.rb +++ b/app/controllers/ajax/moderation_controller.rb @@ -125,9 +125,9 @@ class Ajax::ModerationController < ApplicationController unban = params[:ban] == "0" perma = params[:permaban] == "1" - buntil = DateTime.strptime params[:until], "%m/%d/%Y %I:%M %p" unless unban or perma + buntil = DateTime.strptime params[:until], "%m/%d/%Y %I:%M %p" unless unban || perma - if not unban and target.admin? + if !unban && target.has_role?(:administrator) @status = :nopriv @message = I18n.t('messages.moderation.ban.nopriv') @success = false @@ -166,7 +166,7 @@ class Ajax::ModerationController < ApplicationController @message = I18n.t('messages.moderation.privilege.nope') return unless %w(blogger supporter moderator admin contributor translator).include? params[:type].downcase - if %w(supporter moderator admin).include?(params[:type].downcase) and !current_user.admin? + if %w(supporter moderator admin).include?(params[:type].downcase) && !current_user.has_role?(:administrator) @status = :nopriv @message = I18n.t('messages.moderation.privilege.nopriv') @success = false @@ -174,7 +174,9 @@ class Ajax::ModerationController < ApplicationController end @checked = status - case params[:type].downcase + type = params[:type].downcase + target_role = {"admin" => "administrator"}.fetch(type, type).to_sym + case type when 'blogger' target_user.blogger = status when 'contributor' @@ -183,10 +185,12 @@ class Ajax::ModerationController < ApplicationController target_user.translator = status when 'supporter' target_user.supporter = status - when 'moderator' - target_user.moderator = status - when 'admin' - target_user.admin = status + when 'moderator', 'admin' + if status + target_user.add_role target_role + else + target_user.remove_role target_role + end end target_user.save! diff --git a/app/models/role.rb b/app/models/role.rb new file mode 100644 index 00000000..5db08032 --- /dev/null +++ b/app/models/role.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class Role < ApplicationRecord + has_and_belongs_to_many :users, join_table: :users_roles + + belongs_to :resource, + polymorphic: true, + optional: true + + validates :resource_type, + inclusion: { in: Rolify.resource_types }, + allow_nil: true + + scopify +end diff --git a/app/models/user.rb b/app/models/user.rb index 43e88e4f..119f8d81 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -5,6 +5,8 @@ class User < ApplicationRecord :recoverable, :rememberable, :trackable, :validatable, :confirmable, :authentication_keys => [:login] + rolify + # attr_accessor :login has_many :questions, dependent: :destroy @@ -183,7 +185,7 @@ class User < ApplicationRecord # @return [Boolean] is the user a moderator? def mod? - self.moderator? || self.admin? + has_role?(:moderator) || has_role?(:administrator) end # region stuff used for reporting/moderation @@ -258,4 +260,10 @@ class User < ApplicationRecord end !self.export_processing end + + # %w[admin moderator].each do |m| + # define_method(m) { raise "not allowed: #{m}" } + # define_method(m+??) { raise "not allowed: #{m}?"} + # define_method(m+?=) { |*a| raise "not allowed: #{m}="} + # end end diff --git a/app/views/layouts/_profile.html.haml b/app/views/layouts/_profile.html.haml index 9983862f..d01c7293 100644 --- a/app/views/layouts/_profile.html.haml +++ b/app/views/layouts/_profile.html.haml @@ -15,7 +15,7 @@ %i.fa.fa-fw.fa-cog = t('views.navigation.settings') %li.divider - - if current_user.admin? + - if current_user.has_role?(:administrator) %li %a{href: rails_admin_path} %i.fa.fa-fw.fa-cogs diff --git a/app/views/user/_actions.html.haml b/app/views/user/_actions.html.haml index ec19de32..7f3ae676 100644 --- a/app/views/user/_actions.html.haml +++ b/app/views/user/_actions.html.haml @@ -33,7 +33,7 @@ %a{href: '#', data: { target: "#modal-privileges", toggle: :modal }} %i.fa.fa-wrench = raw t('views.actions.privilege', user: user.screen_name) - - unless user.admin? + - unless user.has_role?(:administrator) %li %a{href: '#', data: { target: "#modal-ban", toggle: :modal }} %i.fa.fa-ban diff --git a/app/views/user/_modal_privileges.html.haml b/app/views/user/_modal_privileges.html.haml index 351e030c..f54ec705 100644 --- a/app/views/user/_modal_privileges.html.haml +++ b/app/views/user/_modal_privileges.html.haml @@ -11,7 +11,7 @@ = render 'user/modal_privileges_item', privilege: 'blogger', description: t('views.modal.privilege.blogger'), user: @user = render 'user/modal_privileges_item', privilege: 'contributor', description: t('views.modal.privilege.contributor'), user: @user = render 'user/modal_privileges_item', privilege: 'translator', description: t('views.modal.privilege.translator'), user: @user - - if current_user.admin? + - if current_user.has_role?(:administrator) = render 'user/modal_privileges_item', privilege: 'supporter', description: t('views.modal.privilege.supporter'), user: @user = render 'user/modal_privileges_item', privilege: 'moderator', description: t('views.modal.privilege.moderator'),user: @user = render 'user/modal_privileges_item', privilege: 'admin', description: t('views.modal.privilege.admin'), user: @user diff --git a/app/views/user/_modal_privileges_item.html.haml b/app/views/user/_modal_privileges_item.html.haml index 92d498dd..5164a049 100644 --- a/app/views/user/_modal_privileges_item.html.haml +++ b/app/views/user/_modal_privileges_item.html.haml @@ -1,8 +1,11 @@ - description ||= '' +- role_mapping = {"admin" => "administrator"} +- requires_role = %w[admin moderator].include?(privilege) +- checked = requires_role ? user.has_role?(role_mapping.fetch(privilege, privilege).to_sym) : user.public_send("#{privilege}?") %li.list-group-item{id: "privilege-#{privilege}"} .media .pull-left.j2-table - %input.input--center{type: :checkbox, name: 'check-your-privileges', data: { type: privilege, user: user.screen_name }, checked: user.send("#{privilege}?"), autocomplete: 'off'} + %input.input--center{type: :checkbox, name: 'check-your-privileges', data: { type: privilege, user: user.screen_name }, checked: checked, autocomplete: 'off'} .media-body .list-group-item-heading= privilege.capitalize - unless description.blank? diff --git a/app/views/user/_profile_info.html.haml b/app/views/user/_profile_info.html.haml index 57404894..0403a7e0 100644 --- a/app/views/user/_profile_info.html.haml +++ b/app/views/user/_profile_info.html.haml @@ -1,11 +1,11 @@ .panel.panel-default#profile %img.profile--avatar{src: @user.profile_picture.url(:large)} - - if user_signed_in? && current_user.admin? - - if @user.admin? + - if user_signed_in? && current_user.has_role?(:administrator) + - if @user.has_role?(:administrator) .profile--panel-badge.panel-badge-danger %i.fa.fa-flask = t 'views.user.title.admin' - - if @user.moderator? + - if @user.has_role?(:moderator) .profile--panel-badge.panel-badge-success %i.fa.fa-users = t 'views.user.title.moderator' diff --git a/app/views/user/data.html.haml b/app/views/user/data.html.haml index 9cff91a2..8dae340a 100644 --- a/app/views/user/data.html.haml +++ b/app/views/user/data.html.haml @@ -95,7 +95,7 @@ %p.data-heading Admin %p - - if current_user.admin? + - if current_user.has_role?(:administrator) %span.label.label-success %i.fa.fa-fw.fa-check - else diff --git a/config/initializers/rails_admin.rb b/config/initializers/rails_admin.rb index 2c804526..e89e849a 100644 --- a/config/initializers/rails_admin.rb +++ b/config/initializers/rails_admin.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # workaround to get pagination right if defined? WillPaginate Kaminari.configure do |config| @@ -6,12 +8,11 @@ if defined? WillPaginate end RailsAdmin.config do |config| - config.main_app_name = ['justask', 'Kontrollzentrum'] ## == Authentication == config.authenticate_with do - redirect_to main_app.root_path unless current_user.try :admin? + redirect_to main_app.root_path unless current_user&.has_role?(:administrator) end config.current_user_method(&:current_user) diff --git a/config/initializers/rolify.rb b/config/initializers/rolify.rb new file mode 100644 index 00000000..d6d8dffd --- /dev/null +++ b/config/initializers/rolify.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +Rolify.configure do |config| + # By default ORM adapter is ActiveRecord. uncomment to use mongoid + # config.use_mongoid + + # Dynamic shortcuts for User class (user.is_admin? like methods). Default is: false + # config.use_dynamic_shortcuts + + # Configuration to remove roles from database once the last resource is removed. Default is: true + config.remove_role_if_empty = false +end diff --git a/config/routes.rb b/config/routes.rb index 8176f058..e5ac3191 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,19 +2,19 @@ require 'sidekiq/web' Rails.application.routes.draw do start = Time.now - # Admin panel - mount RailsAdmin::Engine => '/justask_admin', as: 'rails_admin' - # Sidekiq constraints ->(req) { req.env["warden"].authenticate?(scope: :user) && - req.env['warden'].user.admin? } do + req.env["warden"].user.has_role?(:administrator) } do + # Admin panel + mount RailsAdmin::Engine => "/justask_admin", as: "rails_admin" + mount Sidekiq::Web, at: "/sidekiq" - mount PgHero::Engine, at: "/pghero", as: 'pghero' + mount PgHero::Engine, at: "/pghero", as: "pghero" end # Moderation panel - constraints ->(req) { req.env['warden'].authenticate?(scope: :user) && - (req.env['warden'].user.mod?) } do + constraints ->(req) { req.env["warden"].authenticate?(scope: :user) && + req.env["warden"].user.mod? } do match '/moderation/priority(/:user_id)', to: 'moderation#priority', via: :get, as: :moderation_priority match '/moderation/ip/:user_id', to: 'moderation#ip', via: :get, as: :moderation_ip match '/moderation(/:type)', to: 'moderation#index', via: :get, as: :moderation, defaults: {type: 'all'} diff --git a/db/migrate/20200419184442_rolify_create_roles.rb b/db/migrate/20200419184442_rolify_create_roles.rb new file mode 100644 index 00000000..23ab21f9 --- /dev/null +++ b/db/migrate/20200419184442_rolify_create_roles.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class RolifyCreateRoles < ActiveRecord::Migration[5.2] + def change + create_table(:roles) do |t| + t.string :name + t.references :resource, polymorphic: true + + t.timestamps + end + + create_table(:users_roles, id: false) do |t| + t.references :user + t.references :role + end + + add_index(:roles, %i[name resource_type resource_id]) + add_index(:users_roles, %i[user_id role_id]) + end +end diff --git a/db/migrate/20200419185535_create_initial_roles.rb b/db/migrate/20200419185535_create_initial_roles.rb new file mode 100644 index 00000000..f3f0788c --- /dev/null +++ b/db/migrate/20200419185535_create_initial_roles.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class CreateInitialRoles < ActiveRecord::Migration[5.2] + def up + %w[Administrator Moderator].each do |role| + Role.where(name: role.parameterize).first_or_create + end + + { + admin: :administrator, + moderator: :moderator + }.each do |legacy_role, new_role| + User.where(legacy_role => true).each do |u| + puts "-- migrating #{u.screen_name} (#{u.id}) from field:#{legacy_role} to role:#{new_role}" + u.add_role new_role + u.public_send("#{legacy_role}=", false) + u.save! + end + end + end + + def down + { + administrator: :admin, + moderator: :moderator + }.each do |new_role, legacy_role| + User.with_role(new_role).each do |u| + puts "-- migrating #{u.screen_name} (#{u.id}) from role:#{new_role} to field:#{legacy_role}" + u.public_send("#{legacy_role}=", true) + u.save! + end + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 439625e6..02d34dfe 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: 2016_01_05_165913) do +ActiveRecord::Schema.define(version: 2020_04_19_185535) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -137,6 +137,16 @@ ActiveRecord::Schema.define(version: 2016_01_05_165913) do t.string "reason" end + create_table "roles", force: :cascade do |t| + t.string "name" + t.string "resource_type" + t.bigint "resource_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id" + t.index ["resource_type", "resource_id"], name: "index_roles_on_resource_type_and_resource_id" + end + create_table "services", id: :serial, force: :cascade do |t| t.string "type", null: false t.integer "user_id", null: false @@ -270,4 +280,12 @@ ActiveRecord::Schema.define(version: 2016_01_05_165913) do t.index ["screen_name"], name: "index_users_on_screen_name", unique: true end + create_table "users_roles", id: false, force: :cascade do |t| + t.bigint "user_id" + t.bigint "role_id" + t.index ["role_id"], name: "index_users_roles_on_role_id" + t.index ["user_id", "role_id"], name: "index_users_roles_on_user_id_and_role_id" + t.index ["user_id"], name: "index_users_roles_on_user_id" + end + end diff --git a/db/seeds.rb b/db/seeds.rb index 4edb1e85..9874789c 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -5,3 +5,7 @@ # # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) # Mayor.create(name: 'Emanuel', city: cities.first) + +%w[Administrator Moderator].each do |role| + Role.where(name: role.parameterize).first_or_create +end diff --git a/lib/exporter.rb b/lib/exporter.rb index 08f3e3c9..1086708f 100644 --- a/lib/exporter.rb +++ b/lib/exporter.rb @@ -1,9 +1,13 @@ +# frozen_string_literal: true + require 'json' require 'yaml' require 'httparty' require 'securerandom' class Exporter + EXPORT_ROLES = [:administrator, :moderator].freeze + def initialize(user) @user = user @obj = {} @@ -30,10 +34,10 @@ class Exporter private def collect_user_info - %i(admin answered_count asked_count ban_reason banned_until bio blogger comment_smiled_count commented_count + %i(answered_count asked_count ban_reason banned_until bio blogger comment_smiled_count commented_count confirmation_sent_at confirmed_at contributor created_at crop_h crop_h_h crop_h_w crop_h_x crop_h_y crop_w crop_x crop_y current_sign_in_at current_sign_in_ip display_name email follower_count friend_count - id last_sign_in_at last_sign_in_ip locale location moderator motivation_header permanently_banned + id last_sign_in_at last_sign_in_ip locale location motivation_header permanently_banned privacy_allow_anonymous_questions privacy_allow_public_timeline privacy_allow_stranger_answers privacy_show_in_search profile_header_content_type profile_header_file_name profile_header_file_size profile_header_updated_at profile_picture_content_type profile_picture_file_name profile_picture_file_size @@ -41,6 +45,10 @@ class Exporter updated_at website).each do |f| @obj[f] = @user.send f end + + EXPORT_ROLES.each do |role| + @obj[role] = @user.has_role?(role) + end end def collect_questions @@ -221,11 +229,16 @@ class Exporter def user_stub(user) uobj = {} - %i(admin answered_count asked_count bio blogger comment_smiled_count commented_count contributor created_at - display_name follower_count friend_count id location moderator motivation_header permanently_banned screen_name + %i(answered_count asked_count bio blogger comment_smiled_count commented_count contributor created_at + display_name follower_count friend_count id location motivation_header permanently_banned screen_name smiled_count supporter translator website).each do |f| uobj[f] = user.send f end + + EXPORT_ROLES.each do |role| + uobj[role] = user.has_role?(role) + end + uobj end end diff --git a/spec/integration/role_constrained_routes_spec.rb b/spec/integration/role_constrained_routes_spec.rb new file mode 100644 index 00000000..4b093876 --- /dev/null +++ b/spec/integration/role_constrained_routes_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'role-constrained routes', type: :request do + shared_examples_for 'fails to access route' do + it 'fails to access route' do + # 404 = no user found -- we have a fallback route if something could not be matched + expect(subject).to eq 404 + end + end + + shared_examples_for 'routes for' do |roles, subject_block, skip_reason: nil| + before { skip(skip_reason) } if skip_reason + + subject(&subject_block) + + context 'not signed in' do + include_examples 'fails to access route' + end + + roles.each do |role| + context "signed in user without #{role} role" do + let(:user) { FactoryBot.create(:user, password: 'test1234') } + + before(:each) do + post '/sign_in', params: { user: { login: user.email, password: user.password } } + end + + include_examples 'fails to access route' + end + + context "signed in user with #{role} role" do + let(:user) { FactoryBot.create(:user, password: 'test1234', roles: [role]) } + + before(:each) do + post '/sign_in', params: { user: { login: user.email, password: user.password } } + end + + it 'can access route' do + expect(subject).to be_in 200..299 + end + end + end + end + + it_behaves_like('routes for', [:administrator], -> { get('/justask_admin') }) + it_behaves_like('routes for', [:administrator], -> { get('/sidekiq') }) + it_behaves_like('routes for', [:administrator], -> { get('/pghero') }, skip_reason: 'PG::InFailedSqlTransaction due to 5.1 upgrade, works fine outside specs though') + it_behaves_like('routes for', %i[administrator moderator], -> { get('/moderation') }) +end From b6d6c1fded439aae8ef69b993516debaeb5ec12c Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 19 Apr 2020 21:38:21 +0100 Subject: [PATCH 14/27] Add announcements to the top of the application template --- app/controllers/application_controller.rb | 5 +++++ app/models/announcement.rb | 10 ++++++++++ app/views/layouts/application.html.haml | 1 + app/views/shared/_announcements.haml | 6 ++++++ 4 files changed, 22 insertions(+) create mode 100644 app/views/shared/_announcements.haml diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ccc1e3f9..e5c9919b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -6,6 +6,7 @@ class ApplicationController < ActionController::Base before_action :configure_permitted_parameters, if: :devise_controller? before_action :check_locale before_action :banned? + before_action :find_active_announcements # check if user wants to read def check_locale @@ -50,6 +51,10 @@ class ApplicationController < ActionController::Base end end + def find_active_announcements + @active_announcements ||= Announcement.find_active + end + include ApplicationHelper protected diff --git a/app/models/announcement.rb b/app/models/announcement.rb index f2a7684f..3014a117 100644 --- a/app/models/announcement.rb +++ b/app/models/announcement.rb @@ -6,6 +6,16 @@ class Announcement < ApplicationRecord validates :link_href, presence: true, if: -> { link_text.present? } validate :starts_at, :validate_date_range + def self.find_active + Rails.cache.fetch "announcement_active", expires_in: 1.minute do + where "starts_at <= :now AND ends_at > :now", now: Time.current + end + end + + def active? + Time.now.utc >= starts_at && Time.now.utc < ends_at + end + def link_present? link_text.present? end diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 12f5e08b..d40a87d8 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -25,6 +25,7 @@ = csrf_meta_tags %body#version1 = render 'layouts/header' + = render 'shared/announcements' = yield = render 'shared/locales' - if Rails.env.development? diff --git a/app/views/shared/_announcements.haml b/app/views/shared/_announcements.haml new file mode 100644 index 00000000..6885694d --- /dev/null +++ b/app/views/shared/_announcements.haml @@ -0,0 +1,6 @@ +.container.announcements + - @active_announcements.each do |announcement| + .alert.alert-announcement + %p= announcement.content + - if announcement.link_present? + %a.alert-link{ href: announcement.link_href }= announcement.link_text From a505e7ee71abb01995b88d646f4fc2e5a7c195bb Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 19 Apr 2020 21:45:07 +0100 Subject: [PATCH 15/27] Ensure we're logged in as an admin when using the AnnouncementController --- app/controllers/announcement_controller.rb | 2 ++ config/routes.rb | 15 +++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/controllers/announcement_controller.rb b/app/controllers/announcement_controller.rb index 93083c1e..ee2bbb24 100644 --- a/app/controllers/announcement_controller.rb +++ b/app/controllers/announcement_controller.rb @@ -1,4 +1,6 @@ class AnnouncementController < ApplicationController + before_action :authenticate_user! + def index @announcements = Announcement.all end diff --git a/config/routes.rb b/config/routes.rb index bdc9a8c7..56dfd2b4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -127,12 +127,15 @@ Rails.application.routes.draw do match '/:username/groups(/p/:page)', to: 'user#groups', via: 'get', as: :show_user_groups, defaults: {page: 1} match '/:username/questions(/p/:page)', to: 'user#questions', via: 'get', as: :show_user_questions, defaults: {page: 1} - match "/admin/announcements", to: "announcement#index", via: :get, as: :announcement_index - match "/admin/announcements", to: "announcement#create", via: :post, as: :announcement_create - match "/admin/announcements/new", to: "announcement#new", via: :get, as: :announcement_new - match "/admin/announcements/:id/edit", to: "announcement#edit", via: :get, as: :announcement_edit - match "/admin/announcements/:id", to: "announcement#update", via: :patch, as: :announcement_update - match "/admin/announcements/:id", to: "announcement#destroy", via: :delete, as: :announcement_destroy + constraints ->(req) { req.env['warden'].authenticate?(scope: :user) && + (req.env['warden'].user.admin?) } do + match "/admin/announcements", to: "announcement#index", via: :get, as: :announcement_index + match "/admin/announcements", to: "announcement#create", via: :post, as: :announcement_create + match "/admin/announcements/new", to: "announcement#new", via: :get, as: :announcement_new + match "/admin/announcements/:id/edit", to: "announcement#edit", via: :get, as: :announcement_edit + match "/admin/announcements/:id", to: "announcement#update", via: :patch, as: :announcement_update + match "/admin/announcements/:id", to: "announcement#destroy", via: :delete, as: :announcement_destroy + end puts 'processing time of routes.rb: ' + "#{(Time.now - start).round(3).to_s.ljust(5, '0')}s".light_green end From e731d76d52f4a404b1c274fff49854bf59e8161c Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 19 Apr 2020 21:54:40 +0100 Subject: [PATCH 16/27] Update admin/announcements routes to use has_role? --- config/routes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index b63596a9..a79b0092 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -128,7 +128,7 @@ Rails.application.routes.draw do match '/:username/questions(/p/:page)', to: 'user#questions', via: 'get', as: :show_user_questions, defaults: {page: 1} constraints ->(req) { req.env['warden'].authenticate?(scope: :user) && - (req.env['warden'].user.admin?) } do + (req.env['warden'].user.has_role?(:administrator)) } do match "/admin/announcements", to: "announcement#index", via: :get, as: :announcement_index match "/admin/announcements", to: "announcement#create", via: :post, as: :announcement_create match "/admin/announcements/new", to: "announcement#new", via: :get, as: :announcement_new From 80c6a4272a33c29b71d28024b22efb5962aeb284 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 19 Apr 2020 21:58:08 +0100 Subject: [PATCH 17/27] Fix indentation in new.html.haml --- app/views/announcement/new.html.haml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/views/announcement/new.html.haml b/app/views/announcement/new.html.haml index 57ed3eaf..25942f28 100644 --- a/app/views/announcement/new.html.haml +++ b/app/views/announcement/new.html.haml @@ -2,15 +2,15 @@ .container.j2-page = bootstrap_form_for(@announcement, url: {action: "create"}) do |f| - if @announcement.errors.any? - .row - .col-md-12 - .alert.alert-danger - %strong - = pluralize(@announcement.errors.count, "error") - prohibited this announcement from being saved: - %ul - - @announcement.errors.full_messages.each do |err| - %li= err + .row + .col-md-12 + .alert.alert-danger + %strong + = pluralize(@announcement.errors.count, "error") + prohibited this announcement from being saved: + %ul + - @announcement.errors.full_messages.each do |err| + %li= err .row .col-md-12 = f.text_area :content, label: "Content" From 4889071f95dad21d71569fd6eb07bd0cf434acd6 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 19 Apr 2020 22:27:50 +0100 Subject: [PATCH 18/27] Add JS for dismissing announcements --- app/assets/javascripts/announcement.coffee | 3 --- app/assets/javascripts/application.js.erb.coffee | 10 ++++++++++ app/assets/stylesheets/announcement.scss | 3 --- app/views/layouts/_profile.html.haml | 4 ++++ app/views/shared/_announcements.haml | 4 +++- 5 files changed, 17 insertions(+), 7 deletions(-) delete mode 100644 app/assets/javascripts/announcement.coffee delete mode 100644 app/assets/stylesheets/announcement.scss diff --git a/app/assets/javascripts/announcement.coffee b/app/assets/javascripts/announcement.coffee deleted file mode 100644 index 24f83d18..00000000 --- a/app/assets/javascripts/announcement.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/application.js.erb.coffee b/app/assets/javascripts/application.js.erb.coffee index b135cf66..73e9ef73 100644 --- a/app/assets/javascripts/application.js.erb.coffee +++ b/app/assets/javascripts/application.js.erb.coffee @@ -74,6 +74,16 @@ _ready = -> lineColor: bodyColor density: 23000 + $(".alert-announcement").each -> + aId = $(this)[0].dataset.announcementId + unless (window.localStorage.getItem("announcement#{aId}")) + $(this).toggleClass("hidden") + + $(document).on "click", ".alert-announcement button.close", (evt) -> + announcement = event.target.closest(".alert-announcement") + aId = announcement.dataset.announcementId + window.localStorage.setItem("announcement#{aId}", true) + $('.arctic_scroll').arctic_scroll speed: 500 diff --git a/app/assets/stylesheets/announcement.scss b/app/assets/stylesheets/announcement.scss deleted file mode 100644 index e922d541..00000000 --- a/app/assets/stylesheets/announcement.scss +++ /dev/null @@ -1,3 +0,0 @@ -// Place all the styles related to the announcement controller here. -// They will automatically be included in application.css. -// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/views/layouts/_profile.html.haml b/app/views/layouts/_profile.html.haml index d01c7293..519195db 100644 --- a/app/views/layouts/_profile.html.haml +++ b/app/views/layouts/_profile.html.haml @@ -28,6 +28,10 @@ %a{href: pghero_path} %i.fa.fa-fw.fa-database Database Monitor + %li + %a{href: announcement_index_path} + %i.fa.fa-fw.fa-info + Announcements %li.divider - if current_user.mod? %li diff --git a/app/views/shared/_announcements.haml b/app/views/shared/_announcements.haml index 6885694d..99fa74d6 100644 --- a/app/views/shared/_announcements.haml +++ b/app/views/shared/_announcements.haml @@ -1,6 +1,8 @@ .container.announcements - @active_announcements.each do |announcement| - .alert.alert-announcement + .alert.alert-announcement.alert-info.alert-dismissable.hidden{ data: { 'announcement-id': announcement.id } } + %button.close{ type: "button", "data-dismiss" => "alert" } + %span{ "aria-hidden" => "true" } × %p= announcement.content - if announcement.link_present? %a.alert-link{ href: announcement.link_href }= announcement.link_text From 477ec3e9c9be9b00c017512c46b9171002bfe515 Mon Sep 17 00:00:00 2001 From: Georg Gadinger Date: Sun, 19 Apr 2020 23:27:29 +0200 Subject: [PATCH 19/27] Do not allow creation of groups with empty names --- Gemfile | 1 + Gemfile.lock | 4 +++ app/models/group.rb | 3 ++ spec/models/group_spec.rb | 66 +++++++++++++++++++++++++++++++++++++++ spec/rails_helper.rb | 1 + 5 files changed, 75 insertions(+) create mode 100644 spec/models/group_spec.rb diff --git a/Gemfile b/Gemfile index e20ff191..f81ee66b 100644 --- a/Gemfile +++ b/Gemfile @@ -92,6 +92,7 @@ group :development, :test do gem 'rake' gem 'puma' gem 'rspec-rails', '~> 3.9' + gem 'rspec-its', '~> 1.3' gem 'factory_bot_rails', require: false gem 'faker' gem 'capybara' diff --git a/Gemfile.lock b/Gemfile.lock index c70c9ee6..bc9c4bd1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -388,6 +388,9 @@ GEM rspec-expectations (3.9.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.9.0) + rspec-its (1.3.0) + rspec-core (>= 3.0.0) + rspec-expectations (>= 3.0.0) rspec-mocks (3.9.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.9.0) @@ -556,6 +559,7 @@ DEPENDENCIES redcarpet redis rolify (~> 5.2) + rspec-its (~> 1.3) rspec-rails (~> 3.9) ruby-progressbar sanitize diff --git a/app/models/group.rb b/app/models/group.rb index c7fbb9ad..0f04893f 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -1,7 +1,10 @@ +# frozen_string_literal: true + class Group < ApplicationRecord belongs_to :user has_many :group_members, dependent: :destroy + validates :name, length: { minimum: 1 } validates :display_name, length: { maximum: 30 } before_validation do diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb new file mode 100644 index 00000000..bec7560c --- /dev/null +++ b/spec/models/group_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe(Group, type: :model) do + let(:user) { FactoryBot.build(:user) } + + describe 'name mangling' do + subject do + Group.new(user: user, display_name: display_name).tap(&:validate) + end + + { + 'great group' => 'great-group', + 'followers' => '-followers-', + ' followers ' => '-followers-', + " the game \t\nyes" => 'the-game-yes', + + # not nice, but this is just the way it is: + "\u{1f98a} :3" => '3', + "\u{1f98a}" => '' + }.each do |display_name, expected_name| + context "when display name is #{display_name.inspect}" do + let(:display_name) { display_name } + + its(:name) { should eq expected_name } + end + end + end + + describe 'validations' do + subject do + Group.new(user: user, display_name: display_name).validate + end + + context "when display name is 'great group' (valid)" do + let(:display_name) { 'great group' } + + it { is_expected.to be true } + end + + context "when display name is '1' (valid)" do + let(:display_name) { '1' } + + it { is_expected.to be true } + end + + context 'when display name is the letter E 621 times (invalid, too long)' do + let(:display_name) { 'E' * 621 } + + it { is_expected.to be false } + end + + context 'when display name is an empty string (invalid, as `name` would be empty)' do + let(:display_name) { '' } + + it { is_expected.to be false } + end + + context "when display name is \u{1f98a} (invalid, as `name` would be empty)" do + let(:display_name) { "\u{1f98a}" } + + it { is_expected.to be false } + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 7517cd89..5fc46546 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -8,6 +8,7 @@ abort("The Rails environment is running in production mode!") if Rails.env.produ require "spec_helper" require "rspec/rails" # Add additional requires below this line. Rails is not loaded until this point! +require "rspec/its" require "devise" require "capybara/rails" require "capybara/rspec" From 864d5844dc189ae5b103cf4cc3d3b4da7cbaf09b Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 19 Apr 2020 22:38:01 +0100 Subject: [PATCH 20/27] Add Announcement tests --- Gemfile | 1 + Gemfile.lock | 2 ++ spec/models/announcement_spec.rb | 50 +++++++++++++++++++++++++++++--- 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index e20ff191..43a669b7 100644 --- a/Gemfile +++ b/Gemfile @@ -104,4 +104,5 @@ group :development, :test do gem 'letter_opener' # Use this just in local test environments gem 'brakeman' gem 'guard-brakeman' + gem 'timecop' end diff --git a/Gemfile.lock b/Gemfile.lock index c70c9ee6..ee793309 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -456,6 +456,7 @@ GEM thor (1.0.1) thread_safe (0.3.6) tilt (2.0.10) + timecop (0.9.1) tiny-color-rails (0.0.2) railties (>= 3.0) turbolinks (2.5.4) @@ -566,6 +567,7 @@ DEPENDENCIES simplecov-rcov spring (~> 2.0) sweetalert-rails + timecop tiny-color-rails tumblr_client! turbolinks (~> 2.5.3) diff --git a/spec/models/announcement_spec.rb b/spec/models/announcement_spec.rb index 0bfc218b..35a76d78 100644 --- a/spec/models/announcement_spec.rb +++ b/spec/models/announcement_spec.rb @@ -1,5 +1,47 @@ -require 'rails_helper' +# frozen_string_literal: true -RSpec.describe Announcement, type: :model do - pending "add some examples to (or delete) #{__FILE__}" -end +require "rails_helper" + +RSpec.describe(Announcement, type: :model) do + let!(:user) { FactoryBot.create :user } + let!(:me) do + Announcement.new( + content: "Raccoon", + starts_at: Time.current, + ends_at: Time.current + 1.day, + user: user + ) + end + + describe "#active?" do + it "returns true when the current time is between starts_at and ends_at" do + expect(me.active?).to be(true) + end + + it "returns false when the current time is before starts_at" do + Timecop.freeze(me.starts_at - 1.second) + expect(me.active?).to be(false) + Timecop.return + end + + it "returns false when the current time is after ends_at" do + Timecop.freeze(me.ends_at) + expect(me.active?).to be(false) + Timecop.return + end + end + + describe "#link_present?" do + it "returns true if a link is present" do + me.link_text = "Very good dogs" + me.link_href = "https://www.reddit.com/r/rarepuppers/" + expect(me.link_present?).to be(true) + end + + it "returns false if a link is not present" do + me.link_text = nil + me.link_href = nil + expect(me.link_present?).to be(false) + end + end +end \ No newline at end of file From c4164a8f343bef7c44ad2c45a0bb91e2c8ed0791 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 19 Apr 2020 22:40:06 +0100 Subject: [PATCH 21/27] Add AnnouncementController tests --- Gemfile | 1 + Gemfile.lock | 5 + .../announcement_controller_spec.rb | 159 ++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 spec/controllers/announcement_controller_spec.rb diff --git a/Gemfile b/Gemfile index 43a669b7..f81e1de1 100644 --- a/Gemfile +++ b/Gemfile @@ -105,4 +105,5 @@ group :development, :test do gem 'brakeman' gem 'guard-brakeman' gem 'timecop' + gem 'rails-controller-testing' end diff --git a/Gemfile.lock b/Gemfile.lock index ee793309..fd7eed2a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -344,6 +344,10 @@ GEM rails-assets-growl (1.3.5) rails-assets-jquery rails-assets-jquery (2.2.4) + rails-controller-testing (1.0.4) + actionpack (>= 5.0.1.x) + actionview (>= 5.0.1.x) + activesupport (>= 5.0.1.x) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) @@ -551,6 +555,7 @@ DEPENDENCIES rails (~> 5.2) rails-assets-growl! rails-assets-jquery (~> 2.2.0)! + rails-controller-testing rails-i18n (~> 5.0) rails_admin rake diff --git a/spec/controllers/announcement_controller_spec.rb b/spec/controllers/announcement_controller_spec.rb new file mode 100644 index 00000000..51649e3c --- /dev/null +++ b/spec/controllers/announcement_controller_spec.rb @@ -0,0 +1,159 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe AnnouncementController, type: :controller do + let(:user) { FactoryBot.create(:user) } + before(:each) { user.add_role :administrator } + + describe "#index" do + subject { get :index } + + context "user signed in" do + before(:each) { sign_in(user) } + + it "renders the index template" do + subject + expect(response).to render_template(:index) + end + + context "no announcements" do + it "@announcements is empty" do + subject + expect(assigns(:announcements)).to be_blank + end + end + + context "one announcement" do + let!(:announcement) { Announcement.create(content: "I am announcement", user: user, starts_at: Time.current, ends_at: Time.current + 2.days) } + + it "includes the announcement in the @announcements assign" do + subject + expect(assigns(:announcements)).to include(announcement) + end + end + end + end + + describe "#new" do + subject { get :new } + + context "user signed in" do + before(:each) { sign_in(user) } + + it "renders the new template" do + subject + expect(response).to render_template(:new) + end + end + end + + describe "#create" do + let :announcement_params do + { + announcement: { + content: "I like dogs!", + starts_at: Time.current, + ends_at: Time.current + 2.days + } + } + end + + subject { post :create, params: announcement_params } + + context "user signed in" do + before(:each) { sign_in(user) } + + it "creates an announcement" do + expect { subject }.to change { Announcement.count }.by(1) + end + + it "redirects to announcement#index" do + subject + expect(response).to redirect_to(:announcement_index) + end + end + end + + describe "#edit" do + let! :announcement do + Announcement.create(content: "Dogs are pretty cool, I guess", + starts_at: Time.current + 3.days, + ends_at: Time.current + 10.days, + user: user) + end + + subject { get :edit, params: { id: announcement.id } } + + context "user signed in" do + before(:each) { sign_in(user) } + + it "renders the edit template" do + subject + expect(response).to render_template(:edit) + end + end + end + + describe "#update" do + let :announcement_params do + { + content: "The trebuchet is the superior siege weapon" + } + end + + let! :announcement do + Announcement.create(content: "Dogs are pretty cool, I guess", + starts_at: Time.current + 3.days, + ends_at: Time.current + 10.days, + user: user) + end + + subject do + patch :update, params: { + id: announcement.id, + announcement: announcement_params + } + end + + context "user signed in" do + before(:each) { sign_in(user) } + + it "updates the announcement" do + subject + updated = Announcement.find announcement.id + expect(updated.content).to eq(announcement_params[:content]) + end + + it "redirects to announcement#index" do + subject + expect(response).to redirect_to(:announcement_index) + end + end + end + + describe "#destroy" do + let! :announcement do + Announcement.create(content: "Dogs are pretty cool, I guess", + starts_at: Time.current + 3.days, + ends_at: Time.current + 10.days, + user: user) + end + + subject { delete :destroy, params: { id: announcement.id } } + + context "user signed in" do + before(:each) { sign_in(user) } + + it "deletes the announcement" do + expect { subject }.to change { Announcement.count }.by(-1) + end + + it "redirects to announcement#index" do + subject + expect(response).to redirect_to(:announcement_index) + end + end + end +end + From 16ade832ca9a6e63bf026168d8995850925c7c74 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 19 Apr 2020 22:58:47 +0100 Subject: [PATCH 22/27] Address @nilsding's review changes --- config/routes.rb | 17 +++++++---------- spec/helpers/announcement_helper_spec.rb | 15 --------------- 2 files changed, 7 insertions(+), 25 deletions(-) delete mode 100644 spec/helpers/announcement_helper_spec.rb diff --git a/config/routes.rb b/config/routes.rb index a79b0092..5d3e2464 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -10,6 +10,13 @@ Rails.application.routes.draw do mount Sidekiq::Web, at: "/sidekiq" mount PgHero::Engine, at: "/pghero", as: "pghero" + + match "/admin/announcements", to: "announcement#index", via: :get, as: :announcement_index + match "/admin/announcements", to: "announcement#create", via: :post, as: :announcement_create + match "/admin/announcements/new", to: "announcement#new", via: :get, as: :announcement_new + match "/admin/announcements/:id/edit", to: "announcement#edit", via: :get, as: :announcement_edit + match "/admin/announcements/:id", to: "announcement#update", via: :patch, as: :announcement_update + match "/admin/announcements/:id", to: "announcement#destroy", via: :delete, as: :announcement_destroy end # Moderation panel @@ -127,15 +134,5 @@ Rails.application.routes.draw do match '/:username/groups(/p/:page)', to: 'user#groups', via: 'get', as: :show_user_groups, defaults: {page: 1} match '/:username/questions(/p/:page)', to: 'user#questions', via: 'get', as: :show_user_questions, defaults: {page: 1} - constraints ->(req) { req.env['warden'].authenticate?(scope: :user) && - (req.env['warden'].user.has_role?(:administrator)) } do - match "/admin/announcements", to: "announcement#index", via: :get, as: :announcement_index - match "/admin/announcements", to: "announcement#create", via: :post, as: :announcement_create - match "/admin/announcements/new", to: "announcement#new", via: :get, as: :announcement_new - match "/admin/announcements/:id/edit", to: "announcement#edit", via: :get, as: :announcement_edit - match "/admin/announcements/:id", to: "announcement#update", via: :patch, as: :announcement_update - match "/admin/announcements/:id", to: "announcement#destroy", via: :delete, as: :announcement_destroy - end - puts 'processing time of routes.rb: ' + "#{(Time.now - start).round(3).to_s.ljust(5, '0')}s".light_green end diff --git a/spec/helpers/announcement_helper_spec.rb b/spec/helpers/announcement_helper_spec.rb deleted file mode 100644 index 04b6d9e2..00000000 --- a/spec/helpers/announcement_helper_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'rails_helper' - -# Specs in this file have access to a helper object that includes -# the AnnouncementHelper. For example: -# -# describe AnnouncementHelper do -# describe "string concat" do -# it "concats two strings with spaces" do -# expect(helper.concat_strings("this","that")).to eq("this that") -# end -# end -# end -RSpec.describe AnnouncementHelper, type: :helper do - pending "add some examples to (or delete) #{__FILE__}" -end From 6e605758a6e447bfecd90988085edb5aa2b1ab72 Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 19 Apr 2020 22:59:57 +0100 Subject: [PATCH 23/27] Minor cleanup in AnnouncementController --- spec/controllers/announcement_controller_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/controllers/announcement_controller_spec.rb b/spec/controllers/announcement_controller_spec.rb index 51649e3c..4af41374 100644 --- a/spec/controllers/announcement_controller_spec.rb +++ b/spec/controllers/announcement_controller_spec.rb @@ -3,8 +3,7 @@ require "rails_helper" describe AnnouncementController, type: :controller do - let(:user) { FactoryBot.create(:user) } - before(:each) { user.add_role :administrator } + let(:user) { FactoryBot.create(:user, roles: [:administrator]) } describe "#index" do subject { get :index } From c292f51957814f1e108a30167f88fd172d3383b2 Mon Sep 17 00:00:00 2001 From: "Dominik M. Kwiatek" Date: Mon, 20 Apr 2020 21:02:48 +0100 Subject: [PATCH 24/27] Set up GitHub Actions (#72) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add GitHub Actions config * Add extra dependencies and use environment variable for DB config Moved the env vars up to outside of the postgres service so this might not work * Pass environment variables for Postgres credentials to Postgres container * Pass service ports to application Have a suspicion that Redis one won't work as justask.yml is probably not using ERB * Add database.yml.postgres with port * Cache gems; pass Redis URL as env var * Add host to DB config * Pass DB credentials for db:setup * Use 127.0.0.1 instead of localhost to force TCP; Use bundler config without instead of --without * I can't read 🗑 * 🤔 * 💻🔨 I have no idea what I'm doing… * Testing env defined outside steps * Move templated vars down * Add build badge --- .github/workflows/retrospring.yml | 69 +++++++++++++++++++++++++++++++ README.md | 1 + config/database.yml.postgres | 8 ++-- config/initializers/15_sidekiq.rb | 6 ++- 4 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/retrospring.yml diff --git a/.github/workflows/retrospring.yml b/.github/workflows/retrospring.yml new file mode 100644 index 00000000..bf6914fb --- /dev/null +++ b/.github/workflows/retrospring.yml @@ -0,0 +1,69 @@ +name: Retrospring + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:10.12 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: justask_test + ports: + - 5432/tcp + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + redis: + image: redis + ports: + - 6379:6379 + options: --entrypoint redis-server + + env: + RAILS_ENV: test + POSTGRES_HOST: localhost + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: justask_test + + steps: + - uses: actions/checkout@v2 + - uses: actions/cache@v1 + with: + path: vendor/bundle + key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + ${{ runner.os }}-gems- + - name: Set up Ruby 2.7 + uses: actions/setup-ruby@v1 + with: + ruby-version: 2.7.x + - name: Install dependencies + run: sudo apt-get install -y libpq-dev libxml2-dev libxslt1-dev libmagickwand-dev imagemagick + - name: Copy default configuration + run: | + cp config/database.yml.postgres config/database.yml + cp config/justask.yml.example config/justask.yml + - name: Install gems + run: | + gem install bundler + bundle config path vendor/bundle + bundle config set without 'production' + bundle install --jobs 4 --retry 3 + - name: Set up database + run: bundle exec rake db:setup + env: + POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }} + - name: Run tests + run: bundle exec rake spec + env: + POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }} + REDIS_URL: "redis://localhost:${{ job.services.redis.ports[6379] }}" diff --git a/README.md b/README.md index dbd63a31..ff811b59 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Retrospring +![Retrospring](https://github.com/Retrospring/retrospring/workflows/Retrospring/badge.svg) This is the source code that powers Retrospring. This is a detached fork of [nilsding/justask](https://github.com/nilsding/justask), where we continue diff --git a/config/database.yml.postgres b/config/database.yml.postgres index 2126da56..eaf84191 100644 --- a/config/database.yml.postgres +++ b/config/database.yml.postgres @@ -28,7 +28,9 @@ development: test: &test adapter: postgresql encoding: unicode - database: justask_test + database: <%= ENV['POSTGRES_DB'] %> + host: <%= ENV.fetch('POSTGRES_HOST', 'localhost') %> pool: 5 - username: postgres - password: + username: <%= ENV['POSTGRES_USER'] %> + password: <%= ENV['POSTGRES_PASSWORD'] %> + port: <%= ENV.fetch('POSTGRES_PORT', 5432) %> diff --git a/config/initializers/15_sidekiq.rb b/config/initializers/15_sidekiq.rb index 2dd4a530..f0985236 100644 --- a/config/initializers/15_sidekiq.rb +++ b/config/initializers/15_sidekiq.rb @@ -1,7 +1,9 @@ +redis_url = ENV.fetch("REDIS_URL") { APP_CONFIG["redis_url"] } + Sidekiq.configure_server do |config| - config.redis = { url: APP_CONFIG['redis_url'] } + config.redis = { url: redis_url } end Sidekiq.configure_client do |config| - config.redis = { url: APP_CONFIG['redis_url'] } + config.redis = { url: redis_url } end \ No newline at end of file From 101b3b68d3ba3e1ac67b5cb4c0bd4dcc2b49531f Mon Sep 17 00:00:00 2001 From: Georg Gadinger Date: Mon, 20 Apr 2020 23:03:57 +0200 Subject: [PATCH 25/27] Use cursored pagination, remove WillPaginate --- Gemfile | 2 - Gemfile.lock | 5 - app/controllers/group_controller.rb | 9 +- app/controllers/inbox_controller.rb | 31 ++-- app/controllers/notifications_controller.rb | 26 ++- app/controllers/public_controller.rb | 5 +- app/controllers/question_controller.rb | 5 +- app/controllers/static_controller.rb | 7 +- app/controllers/user_controller.rb | 16 +- app/models/answer.rb | 2 + app/models/answer/timeline_methods.rb | 14 ++ app/models/concerns/.keep | 0 app/models/concerns/cursor_paginatable.rb | 38 +++++ app/models/group.rb | 7 +- app/models/group/timeline_methods.rb | 12 ++ app/models/notification.rb | 9 + app/models/question.rb | 2 + app/models/question/answer_methods.rb | 13 ++ app/models/user.rb | 11 +- app/models/user/answer_methods.rb | 13 ++ app/models/user/inbox_methods.rb | 14 ++ app/models/user/question_methods.rb | 13 ++ app/models/user/relationship_methods.rb | 16 ++ app/models/user/timeline_methods.rb | 12 ++ app/views/group/index.html.haml | 6 +- app/views/group/index.js.erb | 6 +- app/views/inbox/show.html.haml | 6 +- app/views/inbox/show.js.erb | 6 +- app/views/notifications/index.html.haml | 6 +- app/views/notifications/index.js.erb | 6 +- app/views/public/index.html.haml | 6 +- app/views/public/index.js.erb | 6 +- app/views/question/show.html.haml | 6 +- app/views/question/show.js.erb | 6 +- .../shared/_cursored_pagination_dummy.haml | 11 ++ app/views/static/index.html.haml | 8 +- app/views/static/index.js.erb | 6 +- app/views/user/questions.html.haml | 6 +- app/views/user/questions.js.erb | 4 +- app/views/user/show.html.haml | 6 +- app/views/user/show.js.erb | 6 +- app/views/user/show_follow.html.haml | 6 +- app/views/user/show_follow.js.erb | 4 +- config/initializers/20_will_paginate.rb | 1 - config/initializers/rails_admin.rb | 6 - spec/factories/answer.rb | 12 ++ spec/factories/question.rb | 9 + spec/factories/{users.rb => user.rb} | 4 +- spec/models/user_spec.rb | 158 ++++++++++++++++-- 49 files changed, 470 insertions(+), 119 deletions(-) create mode 100644 app/models/answer/timeline_methods.rb delete mode 100644 app/models/concerns/.keep create mode 100644 app/models/concerns/cursor_paginatable.rb create mode 100644 app/models/group/timeline_methods.rb create mode 100644 app/models/question/answer_methods.rb create mode 100644 app/models/user/answer_methods.rb create mode 100644 app/models/user/inbox_methods.rb create mode 100644 app/models/user/question_methods.rb create mode 100644 app/models/user/relationship_methods.rb create mode 100644 app/models/user/timeline_methods.rb create mode 100644 app/views/shared/_cursored_pagination_dummy.haml delete mode 100644 config/initializers/20_will_paginate.rb create mode 100644 spec/factories/answer.rb create mode 100644 spec/factories/question.rb rename spec/factories/{users.rb => user.rb} (86%) diff --git a/Gemfile b/Gemfile index 4f148a2f..fc7ad28b 100644 --- a/Gemfile +++ b/Gemfile @@ -22,8 +22,6 @@ gem 'haml', '~> 5.0' gem 'bootstrap-sass', '~> 3.4.0' gem 'bootswatch-rails' gem 'sweetalert-rails' -gem 'will_paginate' -gem 'will_paginate-bootstrap' gem 'devise', '~> 4.0' gem 'devise-i18n' gem 'devise-async' diff --git a/Gemfile.lock b/Gemfile.lock index 3c142622..4ab36387 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -499,9 +499,6 @@ GEM websocket-driver (0.7.1) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.4) - will_paginate (3.3.0) - will_paginate-bootstrap (1.0.2) - will_paginate (>= 3.0.3) xpath (3.2.0) nokogiri (~> 1.8) @@ -584,8 +581,6 @@ DEPENDENCIES uglifier (>= 1.3.0) unicorn web-console (< 4.0.0) - will_paginate - will_paginate-bootstrap BUNDLED WITH 2.1.4 diff --git a/app/controllers/group_controller.rb b/app/controllers/group_controller.rb index 7fc45a42..95f772d6 100644 --- a/app/controllers/group_controller.rb +++ b/app/controllers/group_controller.rb @@ -3,6 +3,13 @@ class GroupController < ApplicationController def index @group = current_user.groups.find_by_name!(params[:group_name]) - @timeline = @group.timeline.paginate(page: params[:page]) + @timeline = @group.cursored_timeline(last_id: params[:last_id]) + @timeline_last_id = @timeline.map(&:id).min + @more_data_available = !@group.cursored_timeline(last_id: @timeline_last_id, size: 1).count.zero? + + respond_to do |format| + format.html + format.js + end end end diff --git a/app/controllers/inbox_controller.rb b/app/controllers/inbox_controller.rb index 5b40b455..24849f32 100644 --- a/app/controllers/inbox_controller.rb +++ b/app/controllers/inbox_controller.rb @@ -2,28 +2,37 @@ class InboxController < ApplicationController before_action :authenticate_user! def show - @inbox = Inbox.where(user: current_user) - .order(:created_at).reverse_order - .paginate(page: params[:page]) - @inbox_count = Inbox.where(user: current_user).count + @inbox = current_user.cursored_inbox(last_id: params[:last_id]) + @inbox_last_id = @inbox.map(&:id).min + @more_data_available = !current_user.cursored_inbox(last_id: @inbox_last_id, size: 1).count.zero? + @inbox_count = current_user.inboxes.count + if params[:author].present? begin @author = true @target_user = User.where('LOWER(screen_name) = ?', params[:author].downcase).first! - @inbox_author = current_user.inboxes.joins(:question) - .where(questions: { user_id: @target_user.id, author_is_anonymous: false }) - .paginate(page: params[:page]) - @inbox_author_count = current_user.inboxes.joins(:question) - .where(questions: { user_id: @target_user.id, author_is_anonymous: false }) - .count + @inbox_author = @inbox.joins(:question) + .where(questions: { user_id: @target_user.id, author_is_anonymous: false }) + @inbox_author_count = current_user.inboxes + .joins(:question) + .where(questions: { user_id: @target_user.id, author_is_anonymous: false }) + .count + if @inbox_author.empty? @empty = true flash.now[:info] = "No questions from @#{params[:author]} found, showing default entries instead!" else @inbox = @inbox_author @inbox_count = @inbox_author_count + @inbox_last_id = @inbox.map(&:id).min + @more_data_available = !current_user.cursored_inbox(last_id: @inbox_last_id, size: 1) + .joins(:question) + .where(questions: { user_id: @target_user.id, author_is_anonymous: false }) + .count + .zero? end - rescue + rescue => e + NewRelic::Agent.notice_error(e) flash.now[:error] = "No user with the name @#{params[:author]} found, showing default entries instead!" @not_found = true end diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 8e824ebc..006ba5e8 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -3,16 +3,28 @@ class NotificationsController < ApplicationController def index @type = params[:type] - @notifications = if @type == 'all' - Notification.for(current_user) - elsif @type == 'new' - Notification.for(current_user).where(new: true) - else - Notification.for(current_user).where('LOWER(target_type) = ?', @type) - end.paginate(page: params[:page]) + @notifications = cursored_notifications_for(type: @type, last_id: params[:last_id]) + @notifications_last_id = @notifications.map(&:id).min + @more_data_available = !cursored_notifications_for(type: @type, last_id: @notifications_last_id, size: 1).count.zero? + respond_to do |format| format.html format.js end end + + private + + def cursored_notifications_for(type:, last_id:, size: nil) + cursor_params = { last_id: last_id, size: size }.compact + + case type + when 'all' + Notification.cursored_for(current_user, **cursor_params) + when 'new' + Notification.cursored_for(current_user, new: true, **cursor_params) + else + Notification.cursored_for_type(current_user, type, **cursor_params) + end + end end diff --git a/app/controllers/public_controller.rb b/app/controllers/public_controller.rb index d5b8af94..9a7b1d97 100644 --- a/app/controllers/public_controller.rb +++ b/app/controllers/public_controller.rb @@ -2,7 +2,10 @@ class PublicController < ApplicationController before_action :authenticate_user! def index - @timeline = Answer.joins(:user).where(users: { privacy_allow_public_timeline: true }).all.reverse_order.paginate(page: params[:page]) + @timeline = Answer.cursored_public_timeline(last_id: params[:last_id]) + @timeline_last_id = @timeline.map(&:id).min + @more_data_available = !Answer.cursored_public_timeline(last_id: @timeline_last_id, size: 1).count.zero? + respond_to do |format| format.html format.js diff --git a/app/controllers/question_controller.rb b/app/controllers/question_controller.rb index 54fac76c..f698bdca 100644 --- a/app/controllers/question_controller.rb +++ b/app/controllers/question_controller.rb @@ -1,7 +1,10 @@ class QuestionController < ApplicationController def show @question = Question.find(params[:id]) - @answers = @question.answers.reverse_order.paginate(page: params[:page]) + @answers = @question.cursored_answers(last_id: params[:last_id]) + @answers_last_id = @answers.map(&:id).min + @more_data_available = !@question.cursored_answers(last_id: @answers_last_id, size: 1).count.zero? + respond_to do |format| format.html format.js diff --git a/app/controllers/static_controller.rb b/app/controllers/static_controller.rb index 1fd3bb62..449a5171 100644 --- a/app/controllers/static_controller.rb +++ b/app/controllers/static_controller.rb @@ -1,7 +1,12 @@ +# frozen_string_literal: true + class StaticController < ApplicationController def index if user_signed_in? - @timeline = current_user.timeline.paginate(page: params[:page]) + @timeline = current_user.cursored_timeline(last_id: params[:last_id]) + @timeline_last_id = @timeline.map(&:id).min + @more_data_available = !current_user.cursored_timeline(last_id: @timeline_last_id, size: 1).count.zero? + respond_to do |format| format.html format.js diff --git a/app/controllers/user_controller.rb b/app/controllers/user_controller.rb index acebb416..a35971ed 100644 --- a/app/controllers/user_controller.rb +++ b/app/controllers/user_controller.rb @@ -5,7 +5,9 @@ class UserController < ApplicationController def show @user = User.where('LOWER(screen_name) = ?', params[:username].downcase).first! - @answers = @user.answers.reverse_order.paginate(page: params[:page]) + @answers = @user.cursored_answers(last_id: params[:last_id]) + @answers_last_id = @answers.map(&:id).min + @more_data_available = !@user.cursored_answers(last_id: @answers_last_id, size: 1).count.zero? if user_signed_in? notif = Notification.where(target_type: "Relationship", target_id: @user.active_relationships.where(target_id: current_user.id).pluck(:id), recipient_id: current_user.id, new: true).first @@ -72,7 +74,9 @@ class UserController < ApplicationController def followers @title = 'Followers' @user = User.where('LOWER(screen_name) = ?', params[:username].downcase).first! - @users = @user.followers.reverse_order.paginate(page: params[:page]) + @users = @user.cursored_followers(last_id: params[:last_id]) + @users_last_id = @users.map(&:id).min + @more_data_available = !@user.cursored_followers(last_id: @users_last_id, size: 1).count.zero? @type = :friend render 'show_follow' end @@ -80,7 +84,9 @@ class UserController < ApplicationController def friends @title = 'Following' @user = User.where('LOWER(screen_name) = ?', params[:username].downcase).first! - @users = @user.friends.reverse_order.paginate(page: params[:page]) + @users = @user.cursored_friends(last_id: params[:last_id]) + @users_last_id = @users.map(&:id).min + @more_data_available = !@user.cursored_friends(last_id: @users_last_id, size: 1).count.zero? @type = :friend render 'show_follow' end @@ -88,7 +94,9 @@ class UserController < ApplicationController def questions @title = 'Questions' @user = User.where('LOWER(screen_name) = ?', params[:username].downcase).first! - @questions = @user.questions.where(author_is_anonymous: false).reverse_order.paginate(page: params[:page]) + @questions = @user.cursored_questions(last_id: params[:last_id]) + @questions_last_id = @questions.map(&:id).min + @more_data_available = !@user.cursored_questions(last_id: @questions_last_id, size: 1).count.zero? end def data diff --git a/app/models/answer.rb b/app/models/answer.rb index 652e24d8..5be72352 100644 --- a/app/models/answer.rb +++ b/app/models/answer.rb @@ -1,4 +1,6 @@ class Answer < ApplicationRecord + extend Answer::TimelineMethods + belongs_to :user belongs_to :question has_many :comments, dependent: :destroy diff --git a/app/models/answer/timeline_methods.rb b/app/models/answer/timeline_methods.rb new file mode 100644 index 00000000..732ba2bd --- /dev/null +++ b/app/models/answer/timeline_methods.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Answer::TimelineMethods + include CursorPaginatable + + define_cursor_paginator :cursored_public_timeline, :public_timeline + + def public_timeline + joins(:user) + .where(users: { privacy_allow_public_timeline: true }) + .order(:created_at) + .reverse_order + end +end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/app/models/concerns/cursor_paginatable.rb b/app/models/concerns/cursor_paginatable.rb new file mode 100644 index 00000000..826a172d --- /dev/null +++ b/app/models/concerns/cursor_paginatable.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module CursorPaginatable + extend ActiveSupport::Concern + + module ClassMethods + # Defines a cursor paginator. + # + # This method will define a new method +name+, which accepts the keyword + # arguments +last_id+ for defining the last id the cursor will operate on, + # and +size+ for the amount of records it should return. + # + # @param name [Symbol] The name of the method for the cursor paginator + # @param scope [Symbol] The name of the method which returns an + # ActiveRecord scope. + # + # @example + # class User + # has_many :answers + # + # include CursorPaginatable + # define_cursor_paginator :cursored_answers, :recent_answers + # + # def recent_answers + # self.answers.order(:created_at).reverse_order + # end + # end + def define_cursor_paginator(name, scope, default_size: APP_CONFIG.fetch('items_per_page')) + class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{name}(*args, last_id: nil, size: #{default_size}, **kwargs) + s = self.#{scope}(*args, **kwargs).limit(size) + s = s.where(s.arel_table[:id].lt(last_id)) if last_id.present? + s + end + RUBY + end + end +end diff --git a/app/models/group.rb b/app/models/group.rb index 0f04893f..8a90b598 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Group < ApplicationRecord + include Group::TimelineMethods + belongs_to :user has_many :group_members, dependent: :destroy @@ -22,9 +24,4 @@ class Group < ApplicationRecord def remove_member(user) GroupMember.where(group: self, user: user).first!.destroy end - - # @return [Array] the groups' timeline - def timeline - Answer.where("user_id in (?)", members.pluck(:user_id)).order(:created_at).reverse_order - end end diff --git a/app/models/group/timeline_methods.rb b/app/models/group/timeline_methods.rb new file mode 100644 index 00000000..cf42dcbc --- /dev/null +++ b/app/models/group/timeline_methods.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Group::TimelineMethods + include CursorPaginatable + + define_cursor_paginator :cursored_timeline, :timeline + + # @return [Array] the groups' timeline + def timeline + Answer.where('user_id in (?)', members.pluck(:user_id)).order(:created_at).reverse_order + end +end diff --git a/app/models/notification.rb b/app/models/notification.rb index 18d29bd2..97670ae2 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -3,10 +3,19 @@ class Notification < ApplicationRecord belongs_to :target, polymorphic: true class << self + include CursorPaginatable + + define_cursor_paginator :cursored_for, :for + define_cursor_paginator :cursored_for_type, :for_type + def for(recipient, options={}) self.where(options.merge!(recipient: recipient)).order(:created_at).reverse_order end + def for_type(recipient, type, options={}) + self.where(options.merge!(recipient: recipient)).where('LOWER(target_type) = ?', type).order(:created_at).reverse_order + end + def notify(recipient, target) return nil unless target.respond_to? :notification_type diff --git a/app/models/question.rb b/app/models/question.rb index 29146367..65daf030 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -1,4 +1,6 @@ class Question < ApplicationRecord + include Question::AnswerMethods + belongs_to :user has_many :answers, dependent: :destroy has_many :inboxes, dependent: :destroy diff --git a/app/models/question/answer_methods.rb b/app/models/question/answer_methods.rb new file mode 100644 index 00000000..2de7f3b2 --- /dev/null +++ b/app/models/question/answer_methods.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Question::AnswerMethods + include CursorPaginatable + + define_cursor_paginator :cursored_answers, :ordered_answers + + def ordered_answers + answers + .order(:created_at) + .reverse_order + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 119f8d81..033579c6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,4 +1,10 @@ class User < ApplicationRecord + include User::AnswerMethods + include User::InboxMethods + include User::QuestionMethods + include User::RelationshipMethods + include User::TimelineMethods + # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable and :omniauthable devise :database_authenticatable, :async, :registerable, @@ -100,11 +106,6 @@ class User < ApplicationRecord end end - # @return [Array] the users' timeline - def timeline - Answer.where("user_id in (?) OR user_id = ?", friend_ids, id).order(:created_at).reverse_order - end - # follows an user. def follow(target_user) active_relationships.create(target: target_user) diff --git a/app/models/user/answer_methods.rb b/app/models/user/answer_methods.rb new file mode 100644 index 00000000..3955a8cb --- /dev/null +++ b/app/models/user/answer_methods.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module User::AnswerMethods + include CursorPaginatable + + define_cursor_paginator :cursored_answers, :ordered_answers + + def ordered_answers + answers + .order(:created_at) + .reverse_order + end +end diff --git a/app/models/user/inbox_methods.rb b/app/models/user/inbox_methods.rb new file mode 100644 index 00000000..dea2fb36 --- /dev/null +++ b/app/models/user/inbox_methods.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module User::InboxMethods + include CursorPaginatable + + define_cursor_paginator :cursored_inbox, :ordered_inbox + + def ordered_inbox + inboxes + .includes(:question, :user) + .order(:created_at) + .reverse_order + end +end diff --git a/app/models/user/question_methods.rb b/app/models/user/question_methods.rb new file mode 100644 index 00000000..608efe6b --- /dev/null +++ b/app/models/user/question_methods.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module User::QuestionMethods + include CursorPaginatable + + define_cursor_paginator :cursored_questions, :ordered_questions + + def ordered_questions + questions + .order(:created_at) + .reverse_order + end +end diff --git a/app/models/user/relationship_methods.rb b/app/models/user/relationship_methods.rb new file mode 100644 index 00000000..8093b6bc --- /dev/null +++ b/app/models/user/relationship_methods.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module User::RelationshipMethods + include CursorPaginatable + + define_cursor_paginator :cursored_friends, :ordered_friends + define_cursor_paginator :cursored_followers, :ordered_followers + + def ordered_friends + friends.reverse_order + end + + def ordered_followers + followers.reverse_order + end +end diff --git a/app/models/user/timeline_methods.rb b/app/models/user/timeline_methods.rb new file mode 100644 index 00000000..364d756f --- /dev/null +++ b/app/models/user/timeline_methods.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module User::TimelineMethods + include CursorPaginatable + + define_cursor_paginator :cursored_timeline, :timeline + + # @return [Array] the users' timeline + def timeline + Answer.where('user_id in (?) OR user_id = ?', friend_ids, id).order(:created_at).reverse_order + end +end diff --git a/app/views/group/index.html.haml b/app/views/group/index.html.haml index 8c17ebca..26e38046 100644 --- a/app/views/group/index.html.haml +++ b/app/views/group/index.html.haml @@ -10,9 +10,9 @@ - @timeline.each do |answer| = render 'shared/answerbox', a: answer - #pagination= will_paginate @timeline, renderer: BootstrapPagination::Rails, page_links: false + = render 'shared/cursored_pagination_dummy', more_data_available: @more_data_available, last_id: @timeline_last_id - - if @timeline.next_page - %button#load-more-btn.btn.btn-default{type: :button, data: { current_page: @timeline.current_page }} + - if @more_data_available + %button#load-more-btn.btn.btn-default{type: :button, data: { last_id: @timeline_last_id }} = t 'views.actions.load' .visible-xs= render 'shared/links' diff --git a/app/views/group/index.js.erb b/app/views/group/index.js.erb index 9ce16da7..fd1ddb36 100644 --- a/app/views/group/index.js.erb +++ b/app/views/group/index.js.erb @@ -1,8 +1,8 @@ $('#timeline').append('<% @timeline.each do |answer| %><%= j render 'shared/answerbox', a: answer %><% end %>'); -<% if @timeline.next_page %> - $('#pagination').html('<%= j will_paginate @timeline, renderer: BootstrapPagination::Rails, page_links: false %>'); +<% if @more_data_available %> + $('#pagination').html('<%= j render 'shared/cursored_pagination_dummy', more_data_available: @more_data_available, last_id: @timeline_last_id %>'); <% else %> $('#pagination, #load-more-btn').remove(); -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/inbox/show.html.haml b/app/views/inbox/show.html.haml index 45c62ba0..cb9b1f1e 100644 --- a/app/views/inbox/show.html.haml +++ b/app/views/inbox/show.html.haml @@ -12,10 +12,10 @@ - if @inbox.empty? = t 'views.inbox.empty' - #pagination= will_paginate @inbox, renderer: BootstrapPagination::Rails, page_links: false + = render 'shared/cursored_pagination_dummy', more_data_available: @more_data_available, last_id: @inbox_last_id - - if @inbox.next_page - %button#load-more-btn.btn.btn-default{type: :button, data: { current_page: @inbox.current_page }} + - if @more_data_available + %button#load-more-btn.btn.btn-default{type: :button, data: { last_id: @inbox_last_id }} = t 'views.actions.load' .col-md-9.col-xs-12.col-sm-8.visible-xs diff --git a/app/views/inbox/show.js.erb b/app/views/inbox/show.js.erb index 32154b61..1d7ece9a 100644 --- a/app/views/inbox/show.js.erb +++ b/app/views/inbox/show.js.erb @@ -1,9 +1,9 @@ $('#entries').append('<% @inbox.each do |i| %><%= j render 'inbox/entry', i: i %><% end %>'); -<% if @inbox.next_page %> -$('#pagination').html('<%= j will_paginate @inbox, renderer: BootstrapPagination::Rails, page_links: false %>'); +<% if @more_data_available %> +$('#pagination').html('<%= j render 'shared/cursored_pagination_dummy', more_data_available: @more_data_available, last_id: @inbox_last_id %>'); <% else %> $('#pagination, #load-more-btn').remove(); <% end %> -<% Inbox.where(id: @inbox.pluck(:id)).update_all(new: false) %> \ No newline at end of file +<% Inbox.where(id: @inbox.pluck(:id)).update_all(new: false) %> diff --git a/app/views/notifications/index.html.haml b/app/views/notifications/index.html.haml index 68d32a0a..2993cffd 100644 --- a/app/views/notifications/index.html.haml +++ b/app/views/notifications/index.html.haml @@ -18,9 +18,9 @@ - @notifications.each do |notification| = render 'notifications/notification', notification: notification - #pagination= will_paginate @notifications, renderer: BootstrapPagination::Rails, page_links: false + = render 'shared/cursored_pagination_dummy', more_data_available: @more_data_available, last_id: @notifications_last_id, permitted_params: %i[type] - - if @notifications.next_page - %button#load-more-btn.btn.btn-default{type: :button, data: { current_page: @notifications.current_page }} + - if @more_data_available + %button#load-more-btn.btn.btn-default{type: :button, data: { last_id: @notifications_last_id }} Load more - Notification.for(current_user).update_all(new: false) diff --git a/app/views/notifications/index.js.erb b/app/views/notifications/index.js.erb index f3f5bd18..ba7adebb 100644 --- a/app/views/notifications/index.js.erb +++ b/app/views/notifications/index.js.erb @@ -1,8 +1,8 @@ $('#notifications').append('<% @notifications.each do |notification| %><%= j render 'notifications/notification', notification: notification %><% end %>'); -<% if @notifications.next_page %> - $('#pagination').html('<%= j will_paginate @notifications, renderer: BootstrapPagination::Rails, page_links: false %>'); +<% if @more_data_available %> + $('#pagination').html('<%= j render 'shared/cursored_pagination_dummy', more_data_available: @more_data_available, last_id: @notifications_last_id, permitted_params: %i[type] %>'); <% else %> $('#pagination, #load-more-btn').remove(); -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/public/index.html.haml b/app/views/public/index.html.haml index 6b8187f7..09336b59 100644 --- a/app/views/public/index.html.haml +++ b/app/views/public/index.html.haml @@ -10,9 +10,9 @@ - @timeline.each do |answer| = render 'shared/answerbox', a: answer - #pagination= will_paginate @timeline, renderer: BootstrapPagination::Rails, page_links: false + = render 'shared/cursored_pagination_dummy', more_data_available: @more_data_available, last_id: @timeline_last_id - - if @timeline.next_page - %button#load-more-btn.btn.btn-default{type: :button, data: { current_page: @timeline.current_page }} + - if @more_data_available + %button#load-more-btn.btn.btn-default{type: :button, data: { last_id: @timeline_last_id }} Load more .visible-xs= render 'shared/links' diff --git a/app/views/public/index.js.erb b/app/views/public/index.js.erb index 9ce16da7..fd1ddb36 100644 --- a/app/views/public/index.js.erb +++ b/app/views/public/index.js.erb @@ -1,8 +1,8 @@ $('#timeline').append('<% @timeline.each do |answer| %><%= j render 'shared/answerbox', a: answer %><% end %>'); -<% if @timeline.next_page %> - $('#pagination').html('<%= j will_paginate @timeline, renderer: BootstrapPagination::Rails, page_links: false %>'); +<% if @more_data_available %> + $('#pagination').html('<%= j render 'shared/cursored_pagination_dummy', more_data_available: @more_data_available, last_id: @timeline_last_id %>'); <% else %> $('#pagination, #load-more-btn').remove(); -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/question/show.html.haml b/app/views/question/show.html.haml index dbca8da0..5d407ab4 100644 --- a/app/views/question/show.html.haml +++ b/app/views/question/show.html.haml @@ -8,10 +8,10 @@ - @answers.each do |a| = render 'shared/answerbox', a: a, show_question: false - #pagination= will_paginate @answers, renderer: BootstrapPagination::Rails, page_links: false + = render 'shared/cursored_pagination_dummy', more_data_available: @more_data_available, last_id: @answers_last_id - - if @answers.next_page - %button#load-more-btn.btn.btn-default{type: :button, data: { current_page: @answers.current_page }} + - if @more_data_available + %button#load-more-btn.btn.btn-default{type: :button, data: { last_id: @answers_last_id }} Load more - if user_signed_in? and !current_user.answered? @question and current_user != @question.user and @question.user.privacy_allow_stranger_answers diff --git a/app/views/question/show.js.erb b/app/views/question/show.js.erb index bcaf826e..10610656 100644 --- a/app/views/question/show.js.erb +++ b/app/views/question/show.js.erb @@ -1,8 +1,8 @@ $('#answers').append('<% @answers.each do |answer| %><%= j render 'shared/answerbox', a: answer, show_question: false %><% end %>'); -<% if @answers.next_page %> - $('#pagination').html('<%= j will_paginate @answers, renderer: BootstrapPagination::Rails, page_links: false %>'); +<% if @more_data_available %> + $('#pagination').html('<%= j render 'shared/cursored_pagination_dummy', more_data_available: @more_data_available, last_id: @answers_last_id %>'); <% else %> $('#pagination, #load-more-btn').remove(); -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/shared/_cursored_pagination_dummy.haml b/app/views/shared/_cursored_pagination_dummy.haml new file mode 100644 index 00000000..84436d34 --- /dev/null +++ b/app/views/shared/_cursored_pagination_dummy.haml @@ -0,0 +1,11 @@ +-# this renders a pagination html to keep compatibility with the current pagination js +-# it _should_ be replaced with something else entirely later on. +- permitted_params ||= [] +#pagination + %ul.pagination + %li.next{class: more_data_available ? nil : "disabled"} + - if more_data_available + %a{rel: :next, href: url_for(params.permit(*permitted_params).merge(last_id: last_id))} + Next page + - else + Next page diff --git a/app/views/static/index.html.haml b/app/views/static/index.html.haml index 2abb97e6..8318e9b7 100644 --- a/app/views/static/index.html.haml +++ b/app/views/static/index.html.haml @@ -9,9 +9,9 @@ - @timeline.each do |answer| = render 'shared/answerbox', a: answer - #pagination= will_paginate @timeline, renderer: BootstrapPagination::Rails, page_links: false + = render 'shared/cursored_pagination_dummy', more_data_available: @more_data_available, last_id: @timeline_last_id - - if @timeline.next_page - %button#load-more-btn.btn.btn-default{type: :button, data: { current_page: @timeline.current_page }} + - if @more_data_available + %button#load-more-btn.btn.btn-default{type: :button, data: { last_id: @timeline_last_id }} Load more - .visible-xs= render 'shared/links' \ No newline at end of file + .visible-xs= render 'shared/links' diff --git a/app/views/static/index.js.erb b/app/views/static/index.js.erb index 9ce16da7..fd1ddb36 100644 --- a/app/views/static/index.js.erb +++ b/app/views/static/index.js.erb @@ -1,8 +1,8 @@ $('#timeline').append('<% @timeline.each do |answer| %><%= j render 'shared/answerbox', a: answer %><% end %>'); -<% if @timeline.next_page %> - $('#pagination').html('<%= j will_paginate @timeline, renderer: BootstrapPagination::Rails, page_links: false %>'); +<% if @more_data_available %> + $('#pagination').html('<%= j render 'shared/cursored_pagination_dummy', more_data_available: @more_data_available, last_id: @timeline_last_id %>'); <% else %> $('#pagination, #load-more-btn').remove(); -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/user/questions.html.haml b/app/views/user/questions.html.haml index 628118ae..efd269ec 100644 --- a/app/views/user/questions.html.haml +++ b/app/views/user/questions.html.haml @@ -13,9 +13,9 @@ - @questions.each do |q| = render 'shared/question', q: q, type: nil - #pagination= will_paginate @questions, renderer: BootstrapPagination::Rails, page_links: false + = render 'shared/cursored_pagination_dummy', more_data_available: @more_data_available, last_id: @questions_last_id - - if @questions.next_page - %button#load-more-btn.btn.btn-default{type: :button, data: { current_page: @questions.current_page }} + - if @more_data_available + %button#load-more-btn.btn.btn-default{type: :button, data: { last_id: @questions_last_id }} = t 'views.actions.load' .visible-xs= render 'shared/links' diff --git a/app/views/user/questions.js.erb b/app/views/user/questions.js.erb index a27fddb8..5b9e0ce7 100644 --- a/app/views/user/questions.js.erb +++ b/app/views/user/questions.js.erb @@ -1,8 +1,8 @@ $('#questions').append('<% @questions.each do |q| %><%= j render 'shared/question', q: q, type: nil %><% end %>'); -<% if @questions.next_page %> -$('#pagination').html('<%= j will_paginate @questions, renderer: BootstrapPagination::Rails, page_links: false %>'); +<% if @more_data_available %> +$('#pagination').html('<%= j render 'shared/cursored_pagination_dummy', more_data_available: @more_data_available, last_id: @questions_last_id %>'); <% else %> $('#pagination, #load-more-btn').remove(); <% end %> diff --git a/app/views/user/show.html.haml b/app/views/user/show.html.haml index 8e2d714a..c9ac350f 100644 --- a/app/views/user/show.html.haml +++ b/app/views/user/show.html.haml @@ -14,10 +14,10 @@ - @answers.each do |a| = render 'shared/answerbox', a: a - #pagination= will_paginate @answers, renderer: BootstrapPagination::Rails, page_links: false + = render 'shared/cursored_pagination_dummy', more_data_available: @more_data_available, last_id: @answers_last_id - - if @answers.next_page - %button#load-more-btn.btn.btn-default{type: :button, data: { current_page: @answers.current_page }} + - if @more_data_available + %button#load-more-btn.btn.btn-default{type: :button, data: { last_id: @answers_last_id }} = t 'views.actions.load' .visible-xs= render 'shared/links' - if user_signed_in? diff --git a/app/views/user/show.js.erb b/app/views/user/show.js.erb index 572ae837..443b9d7b 100644 --- a/app/views/user/show.js.erb +++ b/app/views/user/show.js.erb @@ -1,8 +1,8 @@ $('#answers').append('<% @answers.each do |a| %><%= j render 'shared/answerbox', a: a %><% end %>'); -<% if @answers.next_page %> - $('#pagination').html('<%= j will_paginate @answers, renderer: BootstrapPagination::Rails, page_links: false %>'); +<% if @more_data_available %> + $('#pagination').html('<%= j render 'shared/cursored_pagination_dummy', more_data_available: @more_data_available, last_id: @answers_last_id %>'); <% else %> $('#pagination, #load-more-btn').remove(); -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/user/show_follow.html.haml b/app/views/user/show_follow.html.haml index df39ac26..f5b020bb 100644 --- a/app/views/user/show_follow.html.haml +++ b/app/views/user/show_follow.html.haml @@ -14,10 +14,10 @@ .col-md-4.col-sm-6.col-xs-12 = render 'shared/userbox', user: user - #pagination= will_paginate @users, renderer: BootstrapPagination::Rails, page_links: false + = render 'shared/cursored_pagination_dummy', more_data_available: @more_data_available, last_id: @users_last_id - - if @users.next_page - %button#load-more-btn.btn.btn-default{type: :button, data: { current_page: @users.current_page }} + - if @more_data_available + %button#load-more-btn.btn.btn-default{type: :button, data: { last_id: @users_last_id }} = t 'views.actions.load' .visible-xs= render 'shared/links' - if user_signed_in? diff --git a/app/views/user/show_follow.js.erb b/app/views/user/show_follow.js.erb index bd3ecf51..6857406a 100644 --- a/app/views/user/show_follow.js.erb +++ b/app/views/user/show_follow.js.erb @@ -1,8 +1,8 @@ $('#users').append('<% @users.each do |user| %>
<%= j render 'shared/userbox', user: user %>
<% end %>'); -<% if @users.next_page %> - $('#pagination').html('<%= j will_paginate @users, renderer: BootstrapPagination::Rails, page_links: false %>'); +<% if @more_data_available %> + $('#pagination').html('<%= j render 'shared/cursored_pagination_dummy', more_data_available: @more_data_available, last_id: @users_last_id %>'); <% else %> $('#pagination, #load-more-btn').remove(); <% end %> diff --git a/config/initializers/20_will_paginate.rb b/config/initializers/20_will_paginate.rb deleted file mode 100644 index 6f8de892..00000000 --- a/config/initializers/20_will_paginate.rb +++ /dev/null @@ -1 +0,0 @@ -WillPaginate.per_page = APP_CONFIG['items_per_page'] \ No newline at end of file diff --git a/config/initializers/rails_admin.rb b/config/initializers/rails_admin.rb index e89e849a..1a6125be 100644 --- a/config/initializers/rails_admin.rb +++ b/config/initializers/rails_admin.rb @@ -1,12 +1,6 @@ # frozen_string_literal: true # workaround to get pagination right -if defined? WillPaginate - Kaminari.configure do |config| - config.page_method_name = :per_page_kaminari - end -end - RailsAdmin.config do |config| config.main_app_name = ['justask', 'Kontrollzentrum'] diff --git a/spec/factories/answer.rb b/spec/factories/answer.rb new file mode 100644 index 00000000..f0ce6069 --- /dev/null +++ b/spec/factories/answer.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :answer do + transient do + question_content { Faker::Lorem.sentence } + end + + content { Faker::Lorem.sentence } + question { FactoryBot.build(:question, content: question_content) } + end +end diff --git a/spec/factories/question.rb b/spec/factories/question.rb new file mode 100644 index 00000000..d5474c8b --- /dev/null +++ b/spec/factories/question.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :question do + user { nil } + content { Faker::Lorem.sentence } + author_is_anonymous { true } + end +end diff --git a/spec/factories/users.rb b/spec/factories/user.rb similarity index 86% rename from spec/factories/users.rb rename to spec/factories/user.rb index 488ff726..ddf58cb9 100644 --- a/spec/factories/users.rb +++ b/spec/factories/user.rb @@ -2,9 +2,9 @@ FactoryBot.define do factory :user do - sequence(:screen_name) { |i| "#{Faker::Internet.username(specifier: 0..12, separators: %w(_))}#{i}" } + sequence(:screen_name) { |i| "#{Faker::Internet.username(specifier: 0..12, separators: %w[_])}#{i}" } sequence(:email) { |i| "#{i}#{Faker::Internet.email}" } - password { "P4s5w0rD" } + password { 'P4s5w0rD' } confirmed_at { Time.now.utc } display_name { Faker::Name.name } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index af300d69..c928fc5b 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1,28 +1,158 @@ +# frozen_string_literal: true + require 'rails_helper' -RSpec.describe User, :type => :model do - before :each do - @user = User.new( +RSpec.describe User, type: :model do + let!(:me) { FactoryBot.create :user } + + describe 'basic assigns' do + before :each do + @user = User.new( screen_name: 'FunnyMeme2004', password: 'y_u_no_secure_password?', email: 'nice.meme@nsa.gov' - ) + ) + end + + subject { @user } + + it { should respond_to(:email) } + + it '#email returns a string' do + expect(@user.email).to match 'nice.meme@nsa.gov' + end + + it '#motivation_header has a default value' do + expect(@user.motivation_header).to match '' + end + + it 'does not save an invalid screen name' do + @user.screen_name = '$Funny-Meme-%&2004' + expect { @user.save! }.to raise_error(ActiveRecord::RecordInvalid) + end end - subject { @user } + # -- User::TimelineMethods -- - it { should respond_to(:email) } - - it '#email returns a string' do - expect(@user.email).to match 'nice.meme@nsa.gov' + shared_examples_for 'result is blank' do + it 'result is blank' do + expect(subject).to be_blank + end end - it '#motivation_header has a default value' do - expect(@user.motivation_header).to match '' + describe '#timeline' do + subject { me.timeline } + + context 'user answered nothing and is not following anyone' do + include_examples 'result is blank' + end + + context 'user answered something and is not following anyone' do + let(:answer) { FactoryBot.create(:answer, user: me) } + + let(:expected) { [answer] } + + it 'includes the answer' do + expect(subject).to eq(expected) + end + end + + context 'user answered something and follows users with answers' do + let(:user1) { FactoryBot.create(:user) } + let(:user2) { FactoryBot.create(:user) } + let(:answer1) { FactoryBot.create(:answer, user: user1, created_at: 12.hours.ago) } + let(:answer2) { FactoryBot.create(:answer, user: me, created_at: 1.day.ago) } + let(:answer3) { FactoryBot.create(:answer, user: user2, created_at: 10.minutes.ago) } + let(:answer4) { FactoryBot.create(:answer, user: user1, created_at: Time.now.utc) } + + let!(:expected) do + [answer4, answer3, answer1, answer2] + end + + before(:each) do + me.follow(user1) + me.follow(user2) + end + + it 'includes all answers' do + expect(subject).to include(answer1) + expect(subject).to include(answer2) + expect(subject).to include(answer3) + expect(subject).to include(answer4) + end + + it 'result is ordered by created_at in reverse order' do + expect(subject).to eq(expected) + end + end end - it 'does not save an invalid screen name' do - @user.screen_name = '$Funny-Meme-%&2004' - expect{@user.save!}.to raise_error(ActiveRecord::RecordInvalid) + describe '#cursored_timeline' do + let(:last_id) { nil } + + subject { me.cursored_timeline(last_id: last_id, size: 3) } + + context 'user answered nothing and is not following anyone' do + include_examples 'result is blank' + end + + context 'user answered something and is not following anyone' do + let(:answer) { FactoryBot.create(:answer, user: me) } + + let(:expected) { [answer] } + + it 'includes the answer' do + expect(subject).to eq(expected) + end + end + + context 'user answered something and follows users with answers' do + let(:user1) { FactoryBot.create(:user) } + let(:user2) { FactoryBot.create(:user) } + let!(:answer1) { FactoryBot.create(:answer, user: me, created_at: 1.day.ago) } + let!(:answer2) { FactoryBot.create(:answer, user: user1, created_at: 12.hours.ago) } + let!(:answer3) { FactoryBot.create(:answer, user: user2, created_at: 10.minutes.ago) } + let!(:answer4) { FactoryBot.create(:answer, user: user1, created_at: Time.now.utc) } + + before(:each) do + me.follow(user1) + me.follow(user2) + end + + context 'last_id is nil' do + let(:last_id) { nil } + let(:expected) do + [answer4, answer3, answer2] + end + + it 'includes three answers' do + expect(subject).not_to include(answer1) + expect(subject).to include(answer2) + expect(subject).to include(answer3) + expect(subject).to include(answer4) + end + + it 'result is ordered by created_at in reverse order' do + expect(subject).to eq(expected) + end + end + + context 'last_id is answer2.id' do + let(:last_id) { answer2.id } + + it 'includes answer1' do + expect(subject).to include(answer1) + expect(subject).not_to include(answer2) + expect(subject).not_to include(answer3) + expect(subject).not_to include(answer4) + end + end + + context 'last_id is answer1.id' do + let(:last_id) { answer1.id } + + include_examples 'result is blank' + end + end end end From a8c335899d78cf97a5babf5b5729781de8073613 Mon Sep 17 00:00:00 2001 From: "Dominik M. Kwiatek" Date: Wed, 22 Apr 2020 19:22:16 +0100 Subject: [PATCH 26/27] Use email field for password reset (#77) Fixes unsubmmittable password reset form --- app/views/devise/passwords/new.html.haml | 2 +- config/initializers/devise.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml index 71045f16..ce106ef3 100644 --- a/app/views/devise/passwords/new.html.haml +++ b/app/views/devise/passwords/new.html.haml @@ -4,7 +4,7 @@ = bootstrap_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| = devise_error_messages! - = f.text_field :login, autofocus: true, label: "User name" + = f.email_field :email, autofocus: true, label: "Email address" = f.submit "Send me password reset instructions" = render "devise/shared/links" diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index e7731fc3..1345aea6 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -181,7 +181,7 @@ Devise.setup do |config| # ==> Configuration for :recoverable # # Defines which key will be used when recovering the password for an account - config.reset_password_keys = [ :login ] + config.reset_password_keys = [ :email ] # Time interval you can reset your password with a reset password key. # Don't put a too small interval or your users won't have the time to From 445d9ebe2a03fe043c9ecce732340a896c810137 Mon Sep 17 00:00:00 2001 From: Georg Gadinger Date: Thu, 23 Apr 2020 03:31:07 +0200 Subject: [PATCH 27/27] Fix user questions page --- app/controllers/user_controller.rb | 4 ++-- app/models/user/question_methods.rb | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/controllers/user_controller.rb b/app/controllers/user_controller.rb index a35971ed..931d113c 100644 --- a/app/controllers/user_controller.rb +++ b/app/controllers/user_controller.rb @@ -94,9 +94,9 @@ class UserController < ApplicationController def questions @title = 'Questions' @user = User.where('LOWER(screen_name) = ?', params[:username].downcase).first! - @questions = @user.cursored_questions(last_id: params[:last_id]) + @questions = @user.cursored_questions(author_is_anonymous: false, last_id: params[:last_id]) @questions_last_id = @questions.map(&:id).min - @more_data_available = !@user.cursored_questions(last_id: @questions_last_id, size: 1).count.zero? + @more_data_available = !@user.cursored_questions(author_is_anonymous: false, last_id: @questions_last_id, size: 1).count.zero? end def data diff --git a/app/models/user/question_methods.rb b/app/models/user/question_methods.rb index 608efe6b..468e984a 100644 --- a/app/models/user/question_methods.rb +++ b/app/models/user/question_methods.rb @@ -5,8 +5,9 @@ module User::QuestionMethods define_cursor_paginator :cursored_questions, :ordered_questions - def ordered_questions + def ordered_questions(author_is_anonymous: nil) questions + .where({ author_is_anonymous: author_is_anonymous }.compact) .order(:created_at) .reverse_order end