Merge branch 'master' into feature/bootstrap

This commit is contained in:
Andreas Nedbal 2020-04-25 13:14:01 +02:00
commit 7767eeae9f
120 changed files with 1621 additions and 966 deletions

69
.github/workflows/retrospring.yml vendored Normal file
View File

@ -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] }}"

2
.gitignore vendored
View File

@ -35,3 +35,5 @@ desktop.ini
# ignore exports
/public/export
/spec/examples.txt

View File

@ -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

View File

@ -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="

128
CODE_OF_CONDUCT.md Normal file
View File

@ -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.

View File

@ -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).

View File

@ -1,10 +0,0 @@
List of contributors, format is "Name @github <email>"
Andreas N. @pixeldesu
Georg G. @nilsding
Yuki @yukimono
Adel D.-K.
Howl
Iain D.
Jona H. @schisma @ix
Robin B.

18
Capfile
View File

@ -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 }

18
Gemfile
View File

@ -21,8 +21,6 @@ gem 'bcrypt', '~> 3.1.7'
gem 'haml', '~> 5.0'
gem 'bootstrap', '~> 4.4', '>= 4.4.1'
gem 'sweetalert-rails'
gem 'will_paginate'
gem 'will_paginate-bootstrap'
gem 'devise', '~> 4.0'
gem 'devise-i18n'
gem 'devise-async'
@ -41,6 +39,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'
@ -81,17 +81,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
@ -100,6 +89,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'
@ -112,4 +102,6 @@ group :development, :test do
gem 'letter_opener' # Use this just in local test environments
gem 'brakeman'
gem 'guard-brakeman'
gem 'timecop'
gem 'rails-controller-testing'
end

View File

@ -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)
@ -92,16 +89,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)
@ -143,7 +130,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)
@ -290,9 +276,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)
@ -361,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)
@ -392,10 +379,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)
@ -403,11 +386,15 @@ 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)
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)
@ -468,9 +455,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)
@ -479,6 +463,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)
@ -514,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)
@ -525,15 +507,12 @@ PLATFORMS
DEPENDENCIES
bcrypt (~> 3.1.7)
bcrypt_pbkdf (>= 1.0, < 2.0)
better_errors
bootstrap (~> 4.4, >= 4.4.1)
bootstrap3-datetimepicker-rails (~> 4.7.14)
bootstrap_form
brakeman
byebug
capistrano (~> 3.8)
capistrano-rails
capybara
coffee-rails (~> 4.1)
colorize
@ -542,7 +521,6 @@ DEPENDENCIES
devise (~> 4.0)
devise-async
devise-i18n
ed25519
factory_bot_rails
fake_email_validator
faker
@ -576,13 +554,14 @@ DEPENDENCIES
rails (~> 5.2)
rails-assets-growl!
rails-assets-jquery (~> 2.2.0)!
rails-controller-testing
rails-i18n (~> 5.0)
rails_admin
rake
rbnacl (>= 3.2, < 5.0)
rbnacl-libsodium
redcarpet
redis
rolify (~> 5.2)
rspec-its (~> 1.3)
rspec-rails (~> 3.9)
ruby-progressbar
sanitize
@ -593,6 +572,7 @@ DEPENDENCIES
simplecov-rcov
spring (~> 2.0)
sweetalert-rails
timecop
tiny-color-rails
tumblr_client!
turbolinks (~> 2.5.3)
@ -600,8 +580,6 @@ DEPENDENCIES
uglifier (>= 1.3.0)
unicorn
web-console (< 4.0.0)
will_paginate
will_paginate-bootstrap
BUNDLED WITH
2.1.4

View File

@ -1,17 +1,25 @@
# Retrospring [![Build Status](https://travis-ci.org/Retrospring/retrospring.svg)](https://travis-ci.org/Retrospring/retrospring)
# 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 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).

View File

@ -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."

View File

@ -75,6 +75,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

View File

@ -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!

View File

@ -0,0 +1,52 @@
class AnnouncementController < ApplicationController
before_action :authenticate_user!
def index
@announcements = Announcement.all
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
@announcement = Announcement.find(params[:id])
end
def update
@announcement = Announcement.find(params[:id])
@announcement.update(announcement_params)
if @announcement.save
flash[:success] = "Announcement updated successfully."
redirect_to announcement_index_path
else
render 'announcement/edit'
end
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
def announcement_params
params.require(:announcement).permit(:content, :link_text, :link_href, :starts_at, :ends_at)
end
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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(author_is_anonymous: false, last_id: params[:last_id])
@questions_last_id = @questions.map(&:id).min
@more_data_available = !@user.cursored_questions(author_is_anonymous: false, last_id: @questions_last_id, size: 1).count.zero?
end
def data

View File

@ -0,0 +1,2 @@
module AnnouncementHelper
end

View File

@ -0,0 +1,28 @@
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 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
def validate_date_range
if starts_at > ends_at
errors.add(:starts_at, "Start date must be before end date")
end
end
end

View File

@ -1,4 +1,6 @@
class Answer < ApplicationRecord
extend Answer::TimelineMethods
belongs_to :user
belongs_to :question
has_many :comments, dependent: :destroy

View File

@ -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

View File

@ -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

View File

@ -1,7 +1,12 @@
# frozen_string_literal: true
class Group < ApplicationRecord
include Group::TimelineMethods
belongs_to :user
has_many :group_members, dependent: :destroy
validates :name, length: { minimum: 1 }
validates :display_name, length: { maximum: 30 }
before_validation do
@ -19,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

View File

@ -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

View File

@ -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

View File

@ -1,4 +1,6 @@
class Question < ApplicationRecord
include Question::AnswerMethods
belongs_to :user
has_many :answers, dependent: :destroy
has_many :inboxes, dependent: :destroy

View File

@ -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

15
app/models/role.rb Normal file
View File

@ -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

View File

@ -1,10 +1,18 @@
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,
:recoverable, :rememberable, :trackable,
:validatable, :confirmable, :authentication_keys => [:login]
rolify
# attr_accessor :login
has_many :questions, dependent: :destroy
@ -98,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)
@ -183,7 +186,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 +261,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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
module User::QuestionMethods
include CursorPaginatable
define_cursor_paginator :cursored_questions, :ordered_questions
def ordered_questions(author_is_anonymous: nil)
questions
.where({ author_is_anonymous: author_is_anonymous }.compact)
.order(:created_at)
.reverse_order
end
end

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,29 @@
- 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"
.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"

View File

@ -0,0 +1,14 @@
- 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
.panel-heading
= announcement.starts_at
.panel-body
= announcement.content
.panel-footer
= 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?'

View File

@ -0,0 +1,29 @@
- 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"
.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"

View File

@ -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"

View File

@ -11,9 +11,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 }}
= t 'views.actions.load'
.d-block.d-sm-none= render 'shared/links'
- 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'

View File

@ -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 %>
<% end %>

View File

@ -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.d-block.d-sm-none

View File

@ -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) %>
<% Inbox.where(id: @inbox.pluck(:id)).update_all(new: false) %>

View File

@ -13,7 +13,7 @@
%i.fa.fa-fw.fa-cog
= t('views.navigation.settings')
.dropdown-divider
- if current_user.admin?
- if current_user.has_role?(:administrator)
%a.dropdown-item{href: rails_admin_path}
%i.fa.fa-fw.fa-cogs
= t('views.navigation.admin')
@ -23,6 +23,9 @@
%a.dropdown-item{href: pghero_path}
%i.fa.fa-fw.fa-database
Database Monitor
%a.dropdown-item{href: announcement_index_path}
%i.fa.fa-fw.fa-info
Announcements
.dropdown-divider
- if current_user.mod?
%a.dropdown-item{href: moderation_path}

View File

@ -25,6 +25,7 @@
= csrf_meta_tags
%body#version1
= render 'layouts/header'
= render 'shared/announcements'
= yield
= render 'shared/locales'
- if Rails.env.development?

View File

@ -19,9 +19,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 }}
Load more
- 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)

View File

@ -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 %>
<% end %>

View File

@ -11,9 +11,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 }}
Load more
- 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'

View File

@ -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 %>
<% end %>

View File

@ -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

View File

@ -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 %>
<% end %>

View File

@ -0,0 +1,8 @@
.container.announcements
- @active_announcements.each do |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" } &times;
%p= announcement.content
- if announcement.link_present?
%a.alert-link{ href: announcement.link_href }= announcement.link_text

View File

@ -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

View File

@ -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
·

View File

@ -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 }}
Load more
.visible-xs= render 'shared/links'
- 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'

View File

@ -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 %>
<% end %>

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -1,11 +1,11 @@
.card#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'

View File

@ -14,9 +14,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 }}
= t 'views.actions.load'
- 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'

View File

@ -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 %>

View File

@ -15,11 +15,11 @@
- @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 }}
= t 'views.actions.load'
- 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?
= render 'user/modal_group_memberships'

View File

@ -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 %>
<% end %>

View File

@ -15,11 +15,11 @@
.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 }}
= t 'views.actions.load'
- 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?
= render 'user/modal_group_memberships'

View File

@ -1,8 +1,8 @@
$('#users').append('<% @users.each do |user|
%><div class="col-md-4 col-sm-6 col-xs-12"><%= j render 'shared/userbox', user: user
%></div><% 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 %>

View File

@ -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) %>

View File

@ -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'

View File

@ -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"
# }

View File

@ -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

View File

@ -1 +0,0 @@
WillPaginate.per_page = APP_CONFIG['items_per_page']

View File

@ -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

View File

@ -1,17 +1,12 @@
# 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']
## == 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)

View File

@ -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

View File

@ -2,19 +2,26 @@ 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"
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
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'}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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_185535) 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"
@ -137,6 +149,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 +292,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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,158 @@
# frozen_string_literal: true
require "rails_helper"
describe AnnouncementController, type: :controller do
let(:user) { FactoryBot.create(:user, roles: [: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

View File

@ -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

View File

@ -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

12
spec/factories/answer.rb Normal file
View File

@ -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

View File

@ -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

View File

@ -1,8 +0,0 @@
FactoryBot.define do
factory :notification do
target_type { "MyString" }
target_id { 1 }
recipient_id { 1 }
new { false }
end
end

View File

@ -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

View File

@ -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

21
spec/factories/user.rb Normal file
View File

@ -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

View File

@ -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

Some files were not shown because too many files have changed in this diff Show More