first
This commit is contained in:
commit
5fba9fe725
2468 changed files with 284429 additions and 0 deletions
12
.codeclimate.yml
Normal file
12
.codeclimate.yml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
exclude_paths:
|
||||
- "bin/"
|
||||
- "config/"
|
||||
- "db/"
|
||||
- "factories/"
|
||||
- "features/"
|
||||
- "public/**/*.min.js"
|
||||
- "public/javascripts/bootstrap/"
|
||||
- "public/javascripts/tinymce/"
|
||||
- "script/"
|
||||
- "spec/"
|
||||
- "test/"
|
||||
1
.codecov.yml
Normal file
1
.codecov.yml
Normal file
|
|
@ -0,0 +1 @@
|
|||
comment: false
|
||||
45
.erb-lint.yml
Normal file
45
.erb-lint.yml
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
EnableDefaultLinters: true
|
||||
linters:
|
||||
AllowedScriptType:
|
||||
allowed_types:
|
||||
- "text/javascript"
|
||||
- "speculationrules"
|
||||
DeprecatedClasses:
|
||||
enabled: true
|
||||
rule_set:
|
||||
- deprecated: ["bookmarks", "collections", "readings", "works"]
|
||||
suggestion: "Avoid the plural form of these classes."
|
||||
RequireInputAutocomplete:
|
||||
enabled: false
|
||||
Rubocop:
|
||||
enabled: true
|
||||
rubocop_config:
|
||||
inherit_from:
|
||||
- .rubocop.yml
|
||||
Layout/ArgumentAlignment:
|
||||
EnforcedStyle: with_fixed_indentation
|
||||
Layout/InitialIndentation:
|
||||
Enabled: false
|
||||
Layout/LineLength:
|
||||
Enabled: false
|
||||
Layout/TrailingEmptyLines:
|
||||
Enabled: false
|
||||
Layout/TrailingWhitespace:
|
||||
Enabled: false
|
||||
Naming/FileName:
|
||||
Enabled: false
|
||||
Style/FrozenStringLiteralComment:
|
||||
Enabled: false
|
||||
Lint/UselessAssignment:
|
||||
Enabled: false
|
||||
# Workaround for RuboCop 0.72 and later
|
||||
# https://github.com/Shopify/erb-lint/issues/130
|
||||
Rails:
|
||||
Enabled: true
|
||||
Rails/OutputSafety:
|
||||
Enabled: false
|
||||
SelfClosingTag:
|
||||
enabled: true
|
||||
# XHTML style
|
||||
enforced_style: always
|
||||
6
.gitattributes
vendored
Normal file
6
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Set the default behavior, in case people don't have core.autocrlf set.
|
||||
* text=auto
|
||||
# Bash scripts needs to have LF line endings, even on Windows
|
||||
*.sh text eol=lf
|
||||
# /usr/local/bin/ruby: warning: shebang line ending with \r may cause problems
|
||||
/bin/* text eol=lf
|
||||
108
.gitignore
vendored
Normal file
108
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
*.swp
|
||||
.DS_Store
|
||||
.bundle
|
||||
.idea
|
||||
stdout
|
||||
*~
|
||||
aria_log_control
|
||||
.vscode/
|
||||
docker-compose.yml
|
||||
# /
|
||||
/*.tmproj
|
||||
/sphinx
|
||||
/.DAV
|
||||
/rerun.txt
|
||||
/capybara-*.html
|
||||
/.dropbox
|
||||
/.dropbox.attr
|
||||
/.rspec
|
||||
REVISION
|
||||
aria_log.00000001
|
||||
# /config/
|
||||
/config/brakeman.ignore
|
||||
/config/docker/database.yml
|
||||
/config/database.yml
|
||||
/config/local.yml
|
||||
/config/backup.yml
|
||||
/config/*.sphinx.conf
|
||||
/config/s3.yml
|
||||
/config/sphinx.yml
|
||||
/config/unicorn.rb
|
||||
/config/newrelic.yml
|
||||
/config/redis.yml
|
||||
/config/redis-cucumber.conf
|
||||
/config/skins.dump
|
||||
config/local.yml.save
|
||||
ibdata1
|
||||
ib_buffer_pool
|
||||
ib_logfile0
|
||||
bunder_gems/
|
||||
bundler_gems/
|
||||
otwarchive_production/
|
||||
performance_schema/
|
||||
/bunder_gems
|
||||
/bunder_gems/
|
||||
/bunder_gems/*
|
||||
/bundler_gems
|
||||
/bundler_gems/
|
||||
/bundler_gems/*
|
||||
ibtmp1
|
||||
/performance_schema
|
||||
/performance_schema/*
|
||||
/performance-schema
|
||||
/performance_schema/
|
||||
/mysql/*
|
||||
/otwarchive_production/*
|
||||
/db/*
|
||||
/elastic-data/*
|
||||
/elastic-data
|
||||
/elastic-data/
|
||||
/elastic/*
|
||||
# /config/environments/
|
||||
/config/environments/development.rb
|
||||
|
||||
# /db/
|
||||
/db/*.sqlite3
|
||||
/db/backup
|
||||
/db/sphinx
|
||||
/db/seed
|
||||
|
||||
# /log/
|
||||
/log/*
|
||||
|
||||
# /public/
|
||||
/public/downloads
|
||||
/public/stylesheets/cached_for_screen.css
|
||||
/public/stylesheets/skins
|
||||
public/system/development
|
||||
/public/system/skins
|
||||
public/system/test
|
||||
/public/system/work_skins
|
||||
/public/tags
|
||||
|
||||
# /public/images/
|
||||
/public/images/Thumbs.db
|
||||
|
||||
# /tmp/
|
||||
/tmp/*
|
||||
|
||||
# ActiveRecord storage path
|
||||
storage/
|
||||
|
||||
# /vendor/
|
||||
/vendor/gems
|
||||
|
||||
# /vendor/gems/
|
||||
/vendor/gems/*
|
||||
|
||||
# /vendor/plugins/
|
||||
|
||||
features/cassette_library/
|
||||
|
||||
spec/results.html
|
||||
spec/results
|
||||
features_report.html
|
||||
coverage
|
||||
redis-cucumber-dump.rdb
|
||||
|
||||
.rspec
|
||||
17
.gitpod.yml
Normal file
17
.gitpod.yml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
tasks:
|
||||
- init: ./script/docker/init.sh
|
||||
command: docker compose up -d web
|
||||
|
||||
ports:
|
||||
- port: 3000
|
||||
onOpen: open-browser
|
||||
- port: 3306
|
||||
onOpen: ignore
|
||||
- port: 6379
|
||||
onOpen: ignore
|
||||
- port: 9200
|
||||
onOpen: ignore
|
||||
- port: 9300
|
||||
onOpen: ignore
|
||||
- port: 9400
|
||||
onOpen: ignore
|
||||
9
.hound.yml
Normal file
9
.hound.yml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Available linter versions:
|
||||
# http://help.houndci.com/en/articles/2461415-supported-linters
|
||||
|
||||
jshint:
|
||||
config_file: .jshintrc
|
||||
ignore_file: .jshintignore
|
||||
|
||||
rubocop:
|
||||
enabled: false
|
||||
4
.jshintignore
Normal file
4
.jshintignore
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
coverage
|
||||
public/**/*.min.js
|
||||
public/javascripts/bootstrap
|
||||
public/javascripts/tinymce
|
||||
1
.jshintrc
Normal file
1
.jshintrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
164
.phrase.yml
Normal file
164
.phrase.yml
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
phrase:
|
||||
project_id: a3667b8095533c2b3f8d3ac946bb642f
|
||||
file_format: yml
|
||||
push:
|
||||
sources:
|
||||
# controllers
|
||||
- file: ./config/locales/controllers/en.yml
|
||||
params:
|
||||
update_translations: true
|
||||
# devise
|
||||
- file: ./config/locales/devise/en.yml
|
||||
params:
|
||||
update_translations: true
|
||||
# mailers
|
||||
- file: ./config/locales/mailers/en.yml
|
||||
params:
|
||||
update_translations: true
|
||||
# models
|
||||
- file: ./config/locales/models/en.yml
|
||||
params:
|
||||
update_translations: true
|
||||
# validators
|
||||
- file: ./config/locales/validators/en.yml
|
||||
params:
|
||||
update_translations: true
|
||||
# views
|
||||
- file: ./config/locales/views/en.yml
|
||||
params:
|
||||
update_translations: true
|
||||
pull:
|
||||
targets:
|
||||
- file: ./config/locales/phrase-exports/af.yml
|
||||
params:
|
||||
locale_id: af
|
||||
- file: ./config/locales/phrase-exports/ar.yml
|
||||
params:
|
||||
locale_id: ar
|
||||
- file: ./config/locales/phrase-exports/bg.yml
|
||||
params:
|
||||
locale_id: bg
|
||||
- file: ./config/locales/phrase-exports/bn.yml
|
||||
params:
|
||||
locale_id: bn
|
||||
- file: ./config/locales/phrase-exports/ca.yml
|
||||
params:
|
||||
locale_id: ca
|
||||
- file: ./config/locales/phrase-exports/cs.yml
|
||||
params:
|
||||
locale_id: cs
|
||||
- file: ./config/locales/phrase-exports/cy.yml
|
||||
params:
|
||||
locale_id: cy
|
||||
- file: ./config/locales/phrase-exports/da.yml
|
||||
params:
|
||||
locale_id: da
|
||||
- file: ./config/locales/phrase-exports/de.yml
|
||||
params:
|
||||
locale_id: de
|
||||
- file: ./config/locales/phrase-exports/el.yml
|
||||
params:
|
||||
locale_id: el
|
||||
- file: ./config/locales/phrase-exports/es.yml
|
||||
params:
|
||||
locale_id: es
|
||||
- file: ./config/locales/phrase-exports/fa.yml
|
||||
params:
|
||||
locale_id: fa
|
||||
- file: ./config/locales/phrase-exports/fi.yml
|
||||
params:
|
||||
locale_id: fi
|
||||
- file: ./config/locales/phrase-exports/fil.yml
|
||||
params:
|
||||
locale_id: fil
|
||||
- file: ./config/locales/phrase-exports/fr.yml
|
||||
params:
|
||||
locale_id: fr
|
||||
- file: ./config/locales/phrase-exports/he.yml
|
||||
params:
|
||||
locale_id: he
|
||||
- file: ./config/locales/phrase-exports/hi.yml
|
||||
params:
|
||||
locale_id: hi
|
||||
- file: ./config/locales/phrase-exports/hr.yml
|
||||
params:
|
||||
locale_id: hr
|
||||
- file: ./config/locales/phrase-exports/hu.yml
|
||||
params:
|
||||
locale_id: hu
|
||||
- file: ./config/locales/phrase-exports/id.yml
|
||||
params:
|
||||
locale_id: id
|
||||
- file: ./config/locales/phrase-exports/it.yml
|
||||
params:
|
||||
locale_id: it
|
||||
- file: ./config/locales/phrase-exports/ja.yml
|
||||
params:
|
||||
locale_id: ja
|
||||
- file: ./config/locales/phrase-exports/ko.yml
|
||||
params:
|
||||
locale_id: ko
|
||||
- file: ./config/locales/phrase-exports/lt.yml
|
||||
params:
|
||||
locale_id: lt
|
||||
- file: ./config/locales/phrase-exports/lv.yml
|
||||
params:
|
||||
locale_id: lv
|
||||
- file: ./config/locales/phrase-exports/mk.yml
|
||||
params:
|
||||
locale_id: mk
|
||||
- file: ./config/locales/phrase-exports/mr.yml
|
||||
params:
|
||||
locale_id: mr
|
||||
- file: ./config/locales/phrase-exports/ms.yml
|
||||
params:
|
||||
locale_id: ms
|
||||
- file: ./config/locales/phrase-exports/nb.yml
|
||||
params:
|
||||
locale_id: nb
|
||||
- file: ./config/locales/phrase-exports/nl.yml
|
||||
params:
|
||||
locale_id: nl
|
||||
- file: ./config/locales/phrase-exports/pl.yml
|
||||
params:
|
||||
locale_id: pl
|
||||
- file: ./config/locales/phrase-exports/pt-BR.yml
|
||||
params:
|
||||
locale_id: pt-BR
|
||||
- file: ./config/locales/phrase-exports/pt-PT.yml
|
||||
params:
|
||||
locale_id: pt-PT
|
||||
- file: ./config/locales/phrase-exports/ro.yml
|
||||
params:
|
||||
locale_id: ro
|
||||
- file: ./config/locales/phrase-exports/ru.yml
|
||||
params:
|
||||
locale_id: ru
|
||||
- file: ./config/locales/phrase-exports/scr.yml
|
||||
params:
|
||||
locale_id: scr
|
||||
- file: ./config/locales/phrase-exports/sk.yml
|
||||
params:
|
||||
locale_id: sk
|
||||
- file: ./config/locales/phrase-exports/sl.yml
|
||||
params:
|
||||
locale_id: sl
|
||||
- file: ./config/locales/phrase-exports/sv.yml
|
||||
params:
|
||||
locale_id: sv
|
||||
- file: ./config/locales/phrase-exports/th.yml
|
||||
params:
|
||||
locale_id: th
|
||||
- file: ./config/locales/phrase-exports/tr.yml
|
||||
params:
|
||||
locale_id: tr
|
||||
- file: ./config/locales/phrase-exports/uk.yml
|
||||
params:
|
||||
locale_id: uk
|
||||
- file: ./config/locales/phrase-exports/vi.yml
|
||||
params:
|
||||
locale_id: vi
|
||||
- file: ./config/locales/phrase-exports/zh-CN.yml
|
||||
params:
|
||||
locale_id: zh-CN
|
||||
|
||||
310
.rubocop.yml
Normal file
310
.rubocop.yml
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
# Options available at https://github.com/bbatsov/rubocop/blob/master/config/default.yml
|
||||
|
||||
require:
|
||||
- rubocop-rails
|
||||
- rubocop-rspec
|
||||
- ./rubocop/rubocop
|
||||
|
||||
inherit_mode:
|
||||
merge:
|
||||
- Exclude
|
||||
|
||||
AllCops:
|
||||
NewCops: enable
|
||||
TargetRubyVersion: 3.1
|
||||
|
||||
Bundler/OrderedGems:
|
||||
Enabled: false
|
||||
|
||||
I18n/DeprecatedTranslationKey:
|
||||
Rules:
|
||||
name_with_colon: "Prefer `name` with `mailer.general.metadata_label_indicator` over `name_with_colon`"
|
||||
|
||||
Layout/DotPosition:
|
||||
EnforcedStyle: leading
|
||||
|
||||
Layout/EmptyLinesAroundAttributeAccessor:
|
||||
Enabled: false
|
||||
|
||||
Layout/FirstArrayElementIndentation:
|
||||
EnforcedStyle: consistent
|
||||
|
||||
Layout/LineLength:
|
||||
Enabled: false
|
||||
|
||||
Layout/MultilineMethodCallIndentation:
|
||||
EnforcedStyle: indented
|
||||
|
||||
Layout/SingleLineBlockChain:
|
||||
Enabled: true
|
||||
|
||||
Layout/TrailingWhitespace:
|
||||
Enabled: false
|
||||
|
||||
Lint/AmbiguousBlockAssociation:
|
||||
Exclude:
|
||||
# Exception for specs where we use change matchers:
|
||||
# https://github.com/rubocop-hq/rubocop/issues/4222
|
||||
- 'features/step_definitions/**/*.rb'
|
||||
- 'spec/**/*.rb'
|
||||
|
||||
Lint/AmbiguousRegexpLiteral:
|
||||
Enabled: false
|
||||
|
||||
Lint/RedundantSafeNavigation:
|
||||
Exclude:
|
||||
# Take a better safe than sorry approach to safe navigation in admin
|
||||
# policies.
|
||||
- 'app/policies/*.rb'
|
||||
|
||||
Metrics/AbcSize:
|
||||
Enabled: false
|
||||
|
||||
Metrics/BlockLength:
|
||||
Enabled: false
|
||||
|
||||
Metrics/ClassLength:
|
||||
Enabled: false
|
||||
|
||||
Metrics/CyclomaticComplexity:
|
||||
Enabled: false
|
||||
|
||||
Metrics/MethodLength:
|
||||
Enabled: false
|
||||
|
||||
Metrics/ModuleLength:
|
||||
Enabled: false
|
||||
|
||||
Metrics/ParameterLists:
|
||||
CountKeywordArgs: false
|
||||
|
||||
Metrics/PerceivedComplexity:
|
||||
Enabled: false
|
||||
|
||||
Migration/LargeTableSchemaUpdate:
|
||||
Tables:
|
||||
- abuse_reports
|
||||
- active_storage_attachments
|
||||
- active_storage_blobs
|
||||
- active_storage_variant_records
|
||||
- admin_activities
|
||||
- audits
|
||||
- blocks
|
||||
- bookmarks
|
||||
- chapters
|
||||
- comments
|
||||
- common_taggings
|
||||
- collection_items
|
||||
- collection_participants
|
||||
- collection_preferences
|
||||
- collection_profiles
|
||||
- collections
|
||||
- creatorships
|
||||
- external_works
|
||||
- favorite_tags
|
||||
- feedbacks
|
||||
- filter_counts
|
||||
- filter_taggings
|
||||
- gifts
|
||||
- inbox_comments
|
||||
- invitations
|
||||
- kudos
|
||||
- log_items
|
||||
- meta_taggings
|
||||
- mutes
|
||||
- preferences
|
||||
- profiles
|
||||
- prompts
|
||||
- pseuds
|
||||
- readings
|
||||
- related_works
|
||||
- set_taggings
|
||||
- serial_works
|
||||
- series
|
||||
- skins
|
||||
- stat_counters
|
||||
- subscriptions
|
||||
- tag_nominations
|
||||
- tag_set_associations
|
||||
- tag_sets
|
||||
- taggings
|
||||
- tags
|
||||
- users
|
||||
- works
|
||||
|
||||
Naming/VariableNumber:
|
||||
AllowedIdentifiers:
|
||||
- age_over_13
|
||||
- no_age_over_13
|
||||
|
||||
Rails/DefaultScope:
|
||||
Enabled: true
|
||||
|
||||
Rails/DynamicFindBy:
|
||||
AllowedMethods:
|
||||
# Exceptions for Tag.find_by_name and Tag.find_by_name!
|
||||
- find_by_name
|
||||
- find_by_name!
|
||||
# Exception for Tagging.find_by_tag
|
||||
- find_by_tag
|
||||
# Exceptions for Work.find_by_*
|
||||
- find_by_url
|
||||
- find_by_url_cache_key
|
||||
- find_by_url_generation
|
||||
- find_by_url_generation_key
|
||||
- find_by_url_uncached
|
||||
# Exceptions for InboxComment.find_by_filters
|
||||
- find_by_filters
|
||||
|
||||
Rails/EnvironmentVariableAccess:
|
||||
Enabled: true
|
||||
|
||||
# Allow all uses of html_safe, they're everywhere...
|
||||
Rails/OutputSafety:
|
||||
Enabled: false
|
||||
|
||||
Rails/Output:
|
||||
Exclude:
|
||||
# Allow patches to print warnings to console:
|
||||
- 'config/initializers/monkeypatches/*.rb'
|
||||
# Allow migrations to print pt-osc comments to console:
|
||||
- 'db/migrate/*.rb'
|
||||
|
||||
Rails/RakeEnvironment:
|
||||
Enabled: false
|
||||
|
||||
Rails/ReversibleMigrationMethodDefinition:
|
||||
Enabled: true
|
||||
|
||||
# Allow update_attribute, update_all, touch, etc.
|
||||
Rails/SkipsModelValidations:
|
||||
Enabled: false
|
||||
|
||||
Rails/UnknownEnv:
|
||||
Environments:
|
||||
- development
|
||||
- test
|
||||
- staging
|
||||
- production
|
||||
|
||||
RSpec:
|
||||
Include:
|
||||
- "(?:^|/)factories/"
|
||||
- "(?:^|/)features/"
|
||||
- "(?:^|/)spec/"
|
||||
|
||||
# Allow "allow_any_instance_of"
|
||||
RSpec/AnyInstance:
|
||||
Enabled: false
|
||||
|
||||
# By default allow only prefixes "when", "with", "without".
|
||||
# We have too many, so let's allow everything.
|
||||
RSpec/ContextWording:
|
||||
Enabled: false
|
||||
|
||||
RSpec/DescribeClass:
|
||||
Exclude:
|
||||
# Exception for specs about I18n configurations
|
||||
- 'spec/lib/i18n/**/*.rb'
|
||||
# Exception for rake specs, where the top level describe uses a task name
|
||||
- 'spec/lib/tasks/*.rake_spec.rb'
|
||||
# Exception for integration specs, which may not test a specific class
|
||||
- 'spec/requests/**/*.rb'
|
||||
|
||||
RSpec/DescribedClass:
|
||||
Enabled: false
|
||||
|
||||
RSpec/ExampleLength:
|
||||
Enabled: false
|
||||
|
||||
# Prefer: expect { run }.to change { Foo.bar }
|
||||
# over: expect { run }.to change(Foo, :bar)
|
||||
RSpec/ExpectChange:
|
||||
EnforcedStyle: block
|
||||
|
||||
RSpec/FilePath:
|
||||
Exclude:
|
||||
# Exception for WorksController, whose many specs need multiple files
|
||||
- 'spec/controllers/works/*.rb'
|
||||
# Exception for concern specs, which may test multiple classes
|
||||
- 'spec/models/concerns/**/*.rb'
|
||||
|
||||
# Avoid instance variables, except for those not assigned within the spec,
|
||||
# e.g. @request.
|
||||
RSpec/InstanceVariable:
|
||||
AssignmentOnly: true
|
||||
|
||||
# Allow unreferenced let! calls for test setup
|
||||
RSpec/LetSetup:
|
||||
Enabled: false
|
||||
|
||||
# Allow both "have_received" and "receive" for expectations
|
||||
RSpec/MessageSpies:
|
||||
Enabled: false
|
||||
|
||||
# Allow multiple top level describes (rake specs)
|
||||
RSpec/MultipleDescribes:
|
||||
Enabled: false
|
||||
|
||||
# Allow unlimited expectations per test
|
||||
RSpec/MultipleExpectations:
|
||||
Enabled: false
|
||||
|
||||
# Allow unnamed subjects
|
||||
RSpec/NamedSubject:
|
||||
Enabled: false
|
||||
|
||||
# Allow unlimited nested groups
|
||||
RSpec/NestedGroups:
|
||||
Enabled: false
|
||||
|
||||
RSpec/PredicateMatcher:
|
||||
Enabled: false
|
||||
|
||||
Style/AndOr:
|
||||
EnforcedStyle: conditionals
|
||||
|
||||
Style/ClassAndModuleChildren:
|
||||
Enabled: false
|
||||
|
||||
Style/Documentation:
|
||||
Enabled: false
|
||||
|
||||
Style/EmptyMethod:
|
||||
EnforcedStyle: expanded
|
||||
|
||||
# Prefer template tokens (like %{foo}) over annotated tokens (like %s)
|
||||
Style/FormatStringToken:
|
||||
EnforcedStyle: template
|
||||
|
||||
Style/FrozenStringLiteralComment:
|
||||
Enabled: false
|
||||
|
||||
Style/GlobalVars:
|
||||
AllowedVariables:
|
||||
- $elasticsearch
|
||||
- $rollout
|
||||
|
||||
Style/PercentLiteralDelimiters:
|
||||
Exclude:
|
||||
# Exception for Cucumber step definitions, where we heavily use %{} for strings
|
||||
- 'features/**/*.rb'
|
||||
|
||||
# Stop checking if uses of "self" are redundant
|
||||
Style/RedundantSelf:
|
||||
Enabled: false
|
||||
|
||||
Style/SelectByRegexp:
|
||||
Enabled: true
|
||||
|
||||
Style/SingleLineMethods:
|
||||
AllowIfMethodIsEmpty: false
|
||||
|
||||
Style/StringLiterals:
|
||||
EnforcedStyle: double_quotes
|
||||
|
||||
Style/SymbolArray:
|
||||
Enabled: false
|
||||
|
||||
Style/TernaryParentheses:
|
||||
EnforcedStyle: require_parentheses_when_complex
|
||||
1
.ruby-version
Normal file
1
.ruby-version
Normal file
|
|
@ -0,0 +1 @@
|
|||
ruby-3.2.7
|
||||
10
.simplecov
Normal file
10
.simplecov
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
if ENV["CI"] == "true"
|
||||
require "simplecov-cobertura"
|
||||
SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter
|
||||
end
|
||||
|
||||
SimpleCov.start "rails" do
|
||||
add_filter "/factories/"
|
||||
merge_timeout 7200
|
||||
command_name ENV["TEST_GROUP"].gsub(/[^\w]/, "_") if ENV["TEST_GROUP"]
|
||||
end
|
||||
27
ACKNOWLEDGMENTS.md
Normal file
27
ACKNOWLEDGMENTS.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
Acknowledgments
|
||||
=========
|
||||
|
||||
<p><a href="https://www.browserstack.com/"><img alt="" valign="middle" src="http://media.archiveofourown.org/ao3/logos/Browserstack.svg" width="200"/> BrowserStack</a> for our cross-browser testing tool.</p>
|
||||
<!--cc0 image https://pixabay.com/en/capybara-mammal-capibara-animal-147184/ -->
|
||||
<p><a href="http://teamcapybara.github.io/capybara/"><img alt="" valign="middle" src="http://media.archiveofourown.org/ao3/logos/capabara.png" width="200"/> Capybara</a> for integration testing.</p>
|
||||
<p><a href="https://codeship.com/"><img alt="" valign="middle" src="http://media.archiveofourown.org/ao3/logos/logo_codeship_colour_whitebg.png" width="200"/> Codeship</a> for continuous integration.</p>
|
||||
<p><a href="https://cucumber.io/"><img alt="" valign="middle" src="http://media.archiveofourown.org/ao3/logos/logo-cucumber.png" width="200"/> Cucumber</a> for integration testing.</p>
|
||||
<p><a href="https://www.debian.org/"><img alt="" valign="middle" src="http://media.archiveofourown.org/ao3/logos/Debian.png" width="200"/> Debian</a> for our Linux distribution.</p>
|
||||
<p><a href="https://www.elastic.co/products/elasticsearch"><img alt="" valign="middle" src="http://media.archiveofourown.org/ao3/logos/Elasticsearch-Logo-Color-H.png" width="200"/> Elasticsearch</a> for our tag searching.</p>
|
||||
<p><a href="http://www.exim.org/"><img alt="" valign="middle" src="http://media.archiveofourown.org/ao3/logos/exim.png" width="200"/> Exim</a> for mail sending.</p>
|
||||
<p><a href="http://galeracluster.com/products/"><img alt="" valign="middle" src="http://media.archiveofourown.org/ao3/logos/galera.png" width="200"/> Galera Cluster</a> for clustered MySQL.</p>
|
||||
<p><a href="github.com/otwcode/otwarchive"><img alt="" valign="middle" src="http://media.archiveofourown.org/ao3/logos/GitHub_Logo.png" width="200"/> GitHub</a> for collaborative programming.</p>
|
||||
<p><a href="http://www.haproxy.org/"><img alt="" valign="middle" src="http://media.archiveofourown.org/ao3/logos/logo-med.png" width="200"/> HAProxy</a> for load balancing.</p>
|
||||
<p><a href="https://houndci.com/"><img alt="" valign="middle" src="http://media.archiveofourown.org/ao3/logos/Hound-Dribbble.png" width="200"/> Hound</a> and <a href="https://github.com/reviewdog/reviewdog"><img alt="reviewdog logo" valign="middle" src="https://raw.githubusercontent.com/haya14busa/i/d598ed7dc49fefb0018e422e4c43e5ab8f207a6b/reviewdog/reviewdog.logo.png" width="200"/> reviewdog</a> for style guidance.</p>
|
||||
<p><a href="https://otwarchive.atlassian.net/"><img alt="" valign="middle" src="http://media.archiveofourown.org/ao3/logos/jira_rgb_blue.svg" width="200"/> Jira</a> for our issue tracking.</p>
|
||||
<p><a href="https://nginx.org/en/"><img alt="" valign="middle" src="http://media.archiveofourown.org/ao3/logos/Nginx_logo.svg" width="200"/> NGINX</a> for our front end.</p>
|
||||
<p><a href="https://memcached.org/"><img alt="" valign="middle" src="http://media.archiveofourown.org/ao3/logos/Memcached.png" width="200"/> Memcached</a> for caching.</p>
|
||||
<p><a href="https://mariadb.com/products/mariadb-maxscale"><img alt="" valign="middle" src="http://media.archiveofourown.org/ao3/logos/maxscale.png" width="200"/> MaxScale</a> for MySQL load balancing.</p>
|
||||
<p><a href="https://www.percona.com/software/mysql-database/percona-xtradb-cluster"><img alt="" valign="middle" src="http://media.archiveofourown.org/ao3/logos/logo_percona_xtradbcluster_new.png" width="200"/> Percona XtraDB Cluster</a> for our relational database.</p>
|
||||
<p><a href="http://rubyonrails.org/"><img alt="" valign="middle" src="http://media.archiveofourown.org/ao3/logos/rails.png" width="200"/> Rails</a> for our framework.</p>
|
||||
<p><a href="https://redis.io/"><img alt="" valign="middle" src="http://media.archiveofourown.org/ao3/logos/Redis_Logo.svg" width="200"/> Redis</a> for NoSQL.</p>
|
||||
<p><a href="http://rspec.info/"><img alt="" valign="middle" src="http://media.archiveofourown.org/ao3/logos/rspec.png" width="200"/> RSpec</a> for unit tests.</p>
|
||||
<p><a href="https://www.ruby-lang.org/en/"><img alt="" valign="middle" src="http://media.archiveofourown.org/ao3/logos/ruby.png" width="200"/> Ruby</a> as our language.</p>
|
||||
<p><a href="https://www.jetbrains.com/ruby/"><img alt="" valign="middle" src="http://media.archiveofourown.org/ao3/logos/logo_RubyMine.svg" width="200"/> RubyMine</a> for our integrated development environment.</p>
|
||||
<p><a href="https://sentry.io/"><svg class="css-lfbo6j e1igk8x04" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 222 66" width="200" height="60"><path d="M29,2.26a4.67,4.67,0,0,0-8,0L14.42,13.53A32.21,32.21,0,0,1,32.17,40.19H27.55A27.68,27.68,0,0,0,12.09,17.47L6,28a15.92,15.92,0,0,1,9.23,12.17H4.62A.76.76,0,0,1,4,39.06l2.94-5a10.74,10.74,0,0,0-3.36-1.9l-2.91,5a4.54,4.54,0,0,0,1.69,6.24A4.66,4.66,0,0,0,4.62,44H19.15a19.4,19.4,0,0,0-8-17.31l2.31-4A23.87,23.87,0,0,1,23.76,44H36.07a35.88,35.88,0,0,0-16.41-31.8l4.67-8a.77.77,0,0,1,1.05-.27c.53.29,20.29,34.77,20.66,35.17a.76.76,0,0,1-.68,1.13H40.6q.09,1.91,0,3.81h4.78A4.59,4.59,0,0,0,50,39.43a4.49,4.49,0,0,0-.62-2.28Z M124.32,28.28,109.56,9.22h-3.68V34.77h3.73V15.19l15.18,19.58h3.26V9.22h-3.73ZM87.15,23.54h13.23V20.22H87.14V12.53h14.93V9.21H83.34V34.77h18.92V31.45H87.14ZM71.59,20.3h0C66.44,19.06,65,18.08,65,15.7c0-2.14,1.89-3.59,4.71-3.59a12.06,12.06,0,0,1,7.07,2.55l2-2.83a14.1,14.1,0,0,0-9-3c-5.06,0-8.59,3-8.59,7.27,0,4.6,3,6.19,8.46,7.52C74.51,24.74,76,25.78,76,28.11s-2,3.77-5.09,3.77a12.34,12.34,0,0,1-8.3-3.26l-2.25,2.69a15.94,15.94,0,0,0,10.42,3.85c5.48,0,9-2.95,9-7.51C79.75,23.79,77.47,21.72,71.59,20.3ZM195.7,9.22l-7.69,12-7.64-12h-4.46L186,24.67V34.78h3.84V24.55L200,9.22Zm-64.63,3.46h8.37v22.1h3.84V12.68h8.37V9.22H131.08ZM169.41,24.8c3.86-1.07,6-3.77,6-7.63,0-4.91-3.59-8-9.38-8H154.67V34.76h3.8V25.58h6.45l6.48,9.2h4.44l-7-9.82Zm-10.95-2.5V12.6h7.17c3.74,0,5.88,1.77,5.88,4.84s-2.29,4.86-5.84,4.86Z" transform="translate(11, 11)" fill="#362d59"></path></svg> Sentry</a> for APM/application monitoring.</p>
|
||||
<p><a href="https://slack.com/"><img alt="" valign="middle" src="http://media.archiveofourown.org/ao3/logos/Slack_RGB.svg" width="200"/> Slack</a> for communications.</p>
|
||||
63
CONTRIBUTING.md
Normal file
63
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# Contributing to OTW-Archive
|
||||
|
||||
## Reporting bugs
|
||||
|
||||
We maintain a [Jira issue tracker](https://otwarchive.atlassian.net/projects/AO3/issues) for developers,
|
||||
and a [list of Known Issues](https://archiveofourown.org/known_issues) for
|
||||
[Archive of Our Own](https://archiveofourown.org) users, neither of which are
|
||||
publicly editable.
|
||||
|
||||
If you need help using the site, or want to report an issue you have found,
|
||||
please [contact the AO3 Support team](https://archiveofourown.org/support). Our Support team is staffed by volunteers, so please wait for a response before submitting another ticket. Duplicate submissions will not make things happen faster.
|
||||
|
||||
|
||||
## Reporting security issues
|
||||
|
||||
Please refer to [SECURITY.md](https://github.com/otwcode/otwarchive/blob/master/SECURITY.md).
|
||||
|
||||
|
||||
## Updating documentation
|
||||
|
||||
Our [development wiki](https://github.com/otwcode/otwarchive/wiki) is publicly
|
||||
editable. Unless a page says at the top that it should only be edited by
|
||||
official OTW volunteers, please feel free to make changes!
|
||||
|
||||
|
||||
## Suggesting new features
|
||||
|
||||
Please [contact the AO3 Support team](https://archiveofourown.org/support). Our Support team is staffed by volunteers, so please wait for a response before submitting another ticket. Duplicate submissions will not make things happen faster.
|
||||
|
||||
|
||||
## Contributing code
|
||||
|
||||
**We only accept pull requests for issues we have already added to [Jira](https://otwarchive.atlassian.net)**,
|
||||
with the exception of spelling corrections and documentation improvements
|
||||
(e.g. any Markdown files). We also do not accept code generated by AI tools; for more information,
|
||||
please refer to [our commit policy](https://github.com/otwcode/otwarchive/wiki/Commit-Policy#scary-legal-stuff).
|
||||
|
||||
Please check out our development wiki for more information on:
|
||||
|
||||
- [how to set up a development environment](https://github.com/otwcode/otwarchive/wiki)
|
||||
- [code conventions](https://github.com/otwcode/otwarchive/wiki/Commit-policy)
|
||||
|
||||
### Workflow
|
||||
|
||||
1. If you're a new contributor, find a task on the [issues reserved for first timers](https://otwarchive.atlassian.net/issues/?filter=13119). Otherwise, or if you're up for a challenge, pick a task from the general [open and unassigned issues](https://otwarchive.atlassian.net/issues/?filter=10800). (If you're a new contributor, don't worry about claiming the issue for now. If you make a Jira account, you'll get permissions for claiming issues in step 5.)
|
||||
2. Write code to address the issue.
|
||||
3. Optional: Create a Jira account if you'd like the ability to comment on, assign, and transition issues. Please make sure the Full Name on your Jira account either closely matches the name you'd like us to credit in the release notes or includes it in parentheses, e.g. "Nickname (CREDIT NAME)."
|
||||
4. Submit the code with a pull request following the checklist on [our template](https://github.com/otwcode/otwarchive/blob/master/.github/PULL_REQUEST_TEMPLATE.md).
|
||||
5. Once you've submitted a pull request, we'll review your code and give you permissions on Jira. Please be patient with us! Due to our workload, it may take some time before we can review and eventually merge your pull request.
|
||||
6. Once your pull request is merged, we will deploy it to our internal testing site and our QA team will check that everything is working as intended.
|
||||
7. If something is not working as intended, we may set the issue to ["Broken on Test"](https://github.com/otwcode/otwarchive/wiki/Issue-Tracking-with-Jira) and ask you to make further changes in new pull requests.
|
||||
8. If all is well, your contribution will be deployed to the [Archive of Our Own](https://archiveofourown.org) and you will be credited in the [release notes](https://archiveofourown.org/admin_posts?tag=1)!
|
||||
|
||||
|
||||
## Volunteering for the OTW
|
||||
|
||||
If you would like to donate more of your time and expertise in a multi-national,
|
||||
inclusive, fandom-oriented team, you might enjoy [becoming an official OTW volunteer](http://transformativeworks.org/how-you-can-help/volunteer).
|
||||
|
||||
|
||||
## Questions?
|
||||
|
||||
[Drop us an email](mailto:otw-coders@transformativeworks.org) if you have any questions.
|
||||
4
Capfile
Normal file
4
Capfile
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
load 'deploy' if respond_to?(:namespace) # cap2 differentiator
|
||||
Dir['vendor/plugins/*/recipes/*.rb'].each { |plugin| load(plugin) }
|
||||
|
||||
load 'config/deploy' # remove this line to skip loading any of the default tasks
|
||||
186
Gemfile
Normal file
186
Gemfile
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
ruby "3.2.7"
|
||||
|
||||
gem 'test-unit', '~> 3.2'
|
||||
|
||||
gem 'bundler'
|
||||
|
||||
gem "rails", "~> 7.2"
|
||||
gem "rails-i18n"
|
||||
gem "rack", "~> 2.2"
|
||||
gem "sprockets", "< 4"
|
||||
|
||||
gem 'rails-observers', git: 'https://github.com/rails/rails-observers'
|
||||
gem 'actionpack-page_caching'
|
||||
gem 'rails-controller-testing'
|
||||
|
||||
# Database
|
||||
# gem 'sqlite3-ruby', require: 'sqlite3'
|
||||
gem "mysql2"
|
||||
|
||||
gem 'rack-attack'
|
||||
|
||||
# Version of redis-rb gem
|
||||
# We are currently running Redis 3.2.1 (7/2018)
|
||||
gem "redis", "~> 3.3.5"
|
||||
gem 'redis-namespace'
|
||||
|
||||
# Here are all our application-specific gems
|
||||
|
||||
# Used to convert strings to ascii
|
||||
gem 'unicode'
|
||||
gem 'unidecoder'
|
||||
gem 'unicode_utils', '>=1.4.0'
|
||||
|
||||
# Lograge is opinionated, very opinionated.
|
||||
gem "lograge" # https://github.com/roidrage/lograge
|
||||
|
||||
gem 'will_paginate', '>=3.0.2'
|
||||
gem "pagy", "~> 9.3"
|
||||
gem 'acts_as_list', '~> 0.9.7'
|
||||
gem 'akismetor'
|
||||
|
||||
gem 'httparty'
|
||||
gem 'htmlentities'
|
||||
gem 'whenever', '~>0.6.2', require: false
|
||||
gem 'nokogiri', '>= 1.8.5'
|
||||
gem 'mechanize'
|
||||
gem 'sanitize', '>= 4.6.5'
|
||||
gem "rest-client", "~> 2.1.0", require: "rest_client"
|
||||
gem 'resque', '>=1.14.0'
|
||||
gem 'resque-scheduler'
|
||||
gem 'after_commit_everywhere'
|
||||
#gem 'daemon-spawn', require: 'daemon_spawn'
|
||||
gem "elasticsearch", "8.18.0"
|
||||
gem "aws-sdk-s3"
|
||||
gem 'css_parser'
|
||||
|
||||
gem "terrapin"
|
||||
|
||||
# for looking up image dimensions quickly
|
||||
gem 'fastimage'
|
||||
|
||||
# Gems for authentication
|
||||
gem "devise"
|
||||
gem "devise-async" # To mails through queues
|
||||
gem "bcrypt"
|
||||
gem "devise-pwned_password"
|
||||
|
||||
# Needed for modern ssh
|
||||
gem "ed25519", ">= 1.2", "< 2.0"
|
||||
gem "bcrypt_pbkdf", ">= 1.0", "< 2.0"
|
||||
|
||||
# A highly updated version of the authorization plugin
|
||||
gem 'permit_yo'
|
||||
gem "pundit"
|
||||
|
||||
# fix for annoying UTF-8 error messages as per this:
|
||||
# http://openhood.com/rack/ruby/2010/07/15/rack-test-warning/
|
||||
gem 'escape_utils', '1.2.1'
|
||||
|
||||
gem 'timeliness'
|
||||
|
||||
# for generating graphs
|
||||
gem 'google_visualr', git: 'https://github.com/winston/google_visualr'
|
||||
|
||||
# Globalize for translations
|
||||
gem "globalize", "~> 7.0"
|
||||
|
||||
# Add a clean notifier that shows we are on dev or test
|
||||
gem 'rack-dev-mark', '>=0.7.8'
|
||||
|
||||
#Phrase-app
|
||||
gem 'phraseapp-in-context-editor-ruby', '>=1.0.6'
|
||||
|
||||
# For URL mangling
|
||||
gem 'addressable'
|
||||
gem 'audited', '~> 5.3'
|
||||
|
||||
# For controlling application behavour dynamically
|
||||
gem 'rollout'
|
||||
|
||||
# Use update memcached client with kinder, gentler I/O for Ruby
|
||||
gem 'connection_pool'
|
||||
gem 'dalli'
|
||||
gem 'kgio', '2.10.0'
|
||||
|
||||
gem "marcel", "1.0.2"
|
||||
|
||||
# Library for helping run pt-online-schema-change commands:
|
||||
gem "departure", "~> 6.8"
|
||||
|
||||
gem "rack-timeout"
|
||||
gem "puma_worker_killer"
|
||||
|
||||
group :test do
|
||||
gem "rspec-rails", "~> 6.0"
|
||||
gem 'pickle'
|
||||
gem 'shoulda'
|
||||
gem "capybara"
|
||||
gem "cucumber"
|
||||
gem 'database_cleaner'
|
||||
gem "selenium-webdriver"
|
||||
gem 'capybara-screenshot'
|
||||
gem 'cucumber-rails', require: false
|
||||
gem 'launchy' # So you can do Then show me the page
|
||||
|
||||
# Record and replay data from external URLs
|
||||
gem "vcr", "~> 6.2"
|
||||
gem "webmock"
|
||||
gem 'timecop'
|
||||
gem 'cucumber-timecop', require: false
|
||||
# Code coverage
|
||||
gem "simplecov"
|
||||
gem "simplecov-cobertura", require: false
|
||||
gem 'email_spec', '1.6.0'
|
||||
gem "n_plus_one_control"
|
||||
end
|
||||
|
||||
group :test, :development do
|
||||
gem 'awesome_print'
|
||||
gem 'brakeman'
|
||||
gem 'pry-byebug'
|
||||
gem 'whiny_validation'
|
||||
gem "factory_bot_rails"
|
||||
gem 'minitest'
|
||||
gem "i18n-tasks", require: false
|
||||
end
|
||||
|
||||
group :development do
|
||||
gem 'bundler-audit'
|
||||
gem 'active_record_query_trace', '~> 1.6', '>= 1.6.1'
|
||||
end
|
||||
|
||||
group :linters do
|
||||
gem "erb_lint", "0.4.0"
|
||||
gem "rubocop", "1.22.3"
|
||||
gem "rubocop-rails", "2.12.4"
|
||||
gem "rubocop-rspec", "2.6.0"
|
||||
end
|
||||
|
||||
group :test, :development, :staging do
|
||||
gem 'bullet', '>= 5.7.3'
|
||||
gem "factory_bot", require: false
|
||||
gem "faker", require: false
|
||||
end
|
||||
|
||||
# Deploy with Capistrano
|
||||
gem 'capistrano-gitflow_version', '>=0.0.3', require: false
|
||||
gem 'rvm-capistrano'
|
||||
|
||||
# Use unicorn as the web server
|
||||
gem 'unicorn', '~> 5.5', require: false
|
||||
# Install puma so we can migrate to it
|
||||
gem "puma", "~> 6.5.0"
|
||||
# Use god as the monitor
|
||||
gem 'god', '~> 0.13.7'
|
||||
|
||||
group :staging, :production do
|
||||
gem "stackprof"
|
||||
gem "sentry-ruby"
|
||||
gem "sentry-rails"
|
||||
gem "sentry-resque"
|
||||
end
|
||||
|
||||
gem "image_processing", "~> 1.12"
|
||||
781
Gemfile.lock
Normal file
781
Gemfile.lock
Normal file
|
|
@ -0,0 +1,781 @@
|
|||
GIT
|
||||
remote: https://github.com/rails/rails-observers
|
||||
revision: 389b577322d8b17336730b2dc4e179060a23c8e7
|
||||
specs:
|
||||
rails-observers (0.2.0)
|
||||
activemodel (>= 4.2)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/winston/google_visualr
|
||||
revision: 17b97114a345baadd011e7b442b9a6c91a2b7ab5
|
||||
specs:
|
||||
google_visualr (2.5.1)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
aaronh-chronic (0.3.9)
|
||||
actioncable (7.2.2.2)
|
||||
actionpack (= 7.2.2.2)
|
||||
activesupport (= 7.2.2.2)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (7.2.2.2)
|
||||
actionpack (= 7.2.2.2)
|
||||
activejob (= 7.2.2.2)
|
||||
activerecord (= 7.2.2.2)
|
||||
activestorage (= 7.2.2.2)
|
||||
activesupport (= 7.2.2.2)
|
||||
mail (>= 2.8.0)
|
||||
actionmailer (7.2.2.2)
|
||||
actionpack (= 7.2.2.2)
|
||||
actionview (= 7.2.2.2)
|
||||
activejob (= 7.2.2.2)
|
||||
activesupport (= 7.2.2.2)
|
||||
mail (>= 2.8.0)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (7.2.2.2)
|
||||
actionview (= 7.2.2.2)
|
||||
activesupport (= 7.2.2.2)
|
||||
nokogiri (>= 1.8.5)
|
||||
racc
|
||||
rack (>= 2.2.4, < 3.2)
|
||||
rack-session (>= 1.0.1)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
actionpack-page_caching (1.2.4)
|
||||
actionpack (>= 4.0.0)
|
||||
actiontext (7.2.2.2)
|
||||
actionpack (= 7.2.2.2)
|
||||
activerecord (= 7.2.2.2)
|
||||
activestorage (= 7.2.2.2)
|
||||
activesupport (= 7.2.2.2)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.2.2.2)
|
||||
activesupport (= 7.2.2.2)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
active_record_query_trace (1.8.2)
|
||||
activerecord (>= 6.0.0)
|
||||
activejob (7.2.2.2)
|
||||
activesupport (= 7.2.2.2)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.2.2.2)
|
||||
activesupport (= 7.2.2.2)
|
||||
activerecord (7.2.2.2)
|
||||
activemodel (= 7.2.2.2)
|
||||
activesupport (= 7.2.2.2)
|
||||
timeout (>= 0.4.0)
|
||||
activestorage (7.2.2.2)
|
||||
actionpack (= 7.2.2.2)
|
||||
activejob (= 7.2.2.2)
|
||||
activerecord (= 7.2.2.2)
|
||||
activesupport (= 7.2.2.2)
|
||||
marcel (~> 1.0)
|
||||
activesupport (7.2.2.2)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||
connection_pool (>= 2.2.5)
|
||||
drb
|
||||
i18n (>= 1.6, < 2)
|
||||
logger (>= 1.4.2)
|
||||
minitest (>= 5.1)
|
||||
securerandom (>= 0.3)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
acts_as_list (0.9.19)
|
||||
activerecord (>= 3.0)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
after_commit_everywhere (1.6.0)
|
||||
activerecord (>= 4.2)
|
||||
activesupport
|
||||
akismetor (1.0.0)
|
||||
ast (2.4.3)
|
||||
audited (5.8.0)
|
||||
activerecord (>= 5.2, < 8.2)
|
||||
activesupport (>= 5.2, < 8.2)
|
||||
awesome_print (1.9.2)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.895.0)
|
||||
aws-sdk-core (3.191.3)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.8)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.77.0)
|
||||
aws-sdk-core (~> 3, >= 3.191.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.143.0)
|
||||
aws-sdk-core (~> 3, >= 3.191.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.8)
|
||||
aws-sigv4 (1.8.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.20)
|
||||
bcrypt_pbkdf (1.1.0)
|
||||
benchmark (0.4.1)
|
||||
better_html (2.0.2)
|
||||
actionview (>= 6.0)
|
||||
activesupport (>= 6.0)
|
||||
ast (~> 2.0)
|
||||
erubi (~> 1.4)
|
||||
parser (>= 2.4)
|
||||
smart_properties
|
||||
bigdecimal (3.2.2)
|
||||
brakeman (7.0.2)
|
||||
racc
|
||||
builder (3.3.0)
|
||||
bullet (8.0.8)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
bundler-audit (0.9.1)
|
||||
bundler (>= 1.2.0, < 3)
|
||||
thor (~> 1.0)
|
||||
byebug (12.0.0)
|
||||
capistrano (2.15.11)
|
||||
highline
|
||||
net-scp (>= 1.0.0)
|
||||
net-sftp (>= 2.0.0)
|
||||
net-ssh (>= 2.0.14)
|
||||
net-ssh-gateway (>= 1.1.0)
|
||||
capistrano-ext (1.2.1)
|
||||
capistrano (>= 1.0.0)
|
||||
capistrano-gitflow_version (0.0.3.1)
|
||||
capistrano-ext (>= 1.2.1)
|
||||
stringex
|
||||
capybara (3.40.0)
|
||||
addressable
|
||||
matrix
|
||||
mini_mime (>= 0.1.3)
|
||||
nokogiri (~> 1.11)
|
||||
rack (>= 1.6.0)
|
||||
rack-test (>= 0.6.3)
|
||||
regexp_parser (>= 1.5, < 3.0)
|
||||
xpath (~> 3.2)
|
||||
capybara-screenshot (1.0.26)
|
||||
capybara (>= 1.0, < 4)
|
||||
launchy
|
||||
chronic (0.10.2)
|
||||
climate_control (1.2.0)
|
||||
coderay (1.1.3)
|
||||
concurrent-ruby (1.3.5)
|
||||
connection_pool (2.5.3)
|
||||
crack (1.0.0)
|
||||
bigdecimal
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
css_parser (1.16.0)
|
||||
addressable
|
||||
cucumber (9.2.1)
|
||||
builder (~> 3.2)
|
||||
cucumber-ci-environment (> 9, < 11)
|
||||
cucumber-core (> 13, < 14)
|
||||
cucumber-cucumber-expressions (~> 17.0)
|
||||
cucumber-gherkin (> 24, < 28)
|
||||
cucumber-html-formatter (> 20.3, < 22)
|
||||
cucumber-messages (> 19, < 25)
|
||||
diff-lcs (~> 1.5)
|
||||
mini_mime (~> 1.1)
|
||||
multi_test (~> 1.1)
|
||||
sys-uname (~> 1.2)
|
||||
cucumber-ci-environment (10.0.1)
|
||||
cucumber-core (13.0.3)
|
||||
cucumber-gherkin (>= 27, < 28)
|
||||
cucumber-messages (>= 20, < 23)
|
||||
cucumber-tag-expressions (> 5, < 7)
|
||||
cucumber-cucumber-expressions (17.1.0)
|
||||
bigdecimal
|
||||
cucumber-gherkin (27.0.0)
|
||||
cucumber-messages (>= 19.1.4, < 23)
|
||||
cucumber-html-formatter (21.13.0)
|
||||
cucumber-messages (> 19, < 28)
|
||||
cucumber-messages (22.0.0)
|
||||
cucumber-rails (3.1.1)
|
||||
capybara (>= 3.11, < 4)
|
||||
cucumber (>= 5, < 10)
|
||||
railties (>= 5.2, < 9)
|
||||
cucumber-tag-expressions (6.1.2)
|
||||
cucumber-timecop (0.0.6)
|
||||
chronic
|
||||
cucumber
|
||||
timecop
|
||||
dalli (3.2.8)
|
||||
database_cleaner (2.1.0)
|
||||
database_cleaner-active_record (>= 2, < 3)
|
||||
database_cleaner-active_record (2.2.1)
|
||||
activerecord (>= 5.a)
|
||||
database_cleaner-core (~> 2.0.0)
|
||||
database_cleaner-core (2.0.1)
|
||||
date (3.4.1)
|
||||
departure (6.8.0)
|
||||
activerecord (>= 6.0.0, < 7.3.0, != 7.0.0)
|
||||
mysql2 (>= 0.4.0, < 0.6.0)
|
||||
railties (>= 6.0.0, < 7.3.0, != 7.0.0)
|
||||
devise (4.9.3)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
devise-async (1.0.0)
|
||||
activejob (>= 5.0)
|
||||
devise (>= 4.0)
|
||||
devise-pwned_password (0.1.12)
|
||||
devise (~> 4)
|
||||
pwned (~> 2.4)
|
||||
diff-lcs (1.6.2)
|
||||
docile (1.4.1)
|
||||
domain_name (0.6.20240107)
|
||||
drb (2.2.3)
|
||||
ed25519 (1.3.0)
|
||||
elastic-transport (8.3.5)
|
||||
faraday (< 3)
|
||||
multi_json
|
||||
elasticsearch (8.18.0)
|
||||
elastic-transport (~> 8.3)
|
||||
elasticsearch-api (= 8.18.0)
|
||||
elasticsearch-api (8.18.0)
|
||||
multi_json
|
||||
email_spec (1.6.0)
|
||||
launchy (~> 2.1)
|
||||
mail (~> 2.2)
|
||||
erb (5.0.2)
|
||||
erb_lint (0.4.0)
|
||||
activesupport
|
||||
better_html (>= 2.0.1)
|
||||
parser (>= 2.7.1.4)
|
||||
rainbow
|
||||
rubocop
|
||||
smart_properties
|
||||
erubi (1.13.1)
|
||||
escape_utils (1.2.1)
|
||||
et-orbi (1.2.11)
|
||||
tzinfo
|
||||
factory_bot (6.5.4)
|
||||
activesupport (>= 6.1.0)
|
||||
factory_bot_rails (6.5.0)
|
||||
factory_bot (~> 6.5)
|
||||
railties (>= 6.1.0)
|
||||
faker (3.5.2)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (2.13.1)
|
||||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
faraday-net_http (3.4.0)
|
||||
net-http (>= 0.5.0)
|
||||
fastimage (2.3.0)
|
||||
ffi (1.17.2)
|
||||
fugit (1.11.1)
|
||||
et-orbi (~> 1, >= 1.2.11)
|
||||
raabro (~> 1.4)
|
||||
get_process_mem (1.0.0)
|
||||
bigdecimal (>= 2.0)
|
||||
ffi (~> 1.0)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
globalize (7.0.0)
|
||||
activemodel (>= 7.0, < 8.1)
|
||||
activerecord (>= 7.0, < 8.1)
|
||||
activesupport (>= 7.0, < 8.1)
|
||||
request_store (~> 1.0)
|
||||
god (0.13.7)
|
||||
hashdiff (1.2.0)
|
||||
highline (3.1.2)
|
||||
reline
|
||||
htmlentities (4.3.4)
|
||||
http-accept (1.7.0)
|
||||
http-cookie (1.0.5)
|
||||
domain_name (~> 0.5)
|
||||
httparty (0.21.0)
|
||||
mini_mime (>= 1.0.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-tasks (1.0.15)
|
||||
activesupport (>= 4.0.2)
|
||||
ast (>= 2.1.0)
|
||||
erubi
|
||||
highline (>= 2.0.0)
|
||||
i18n
|
||||
parser (>= 3.2.2.1)
|
||||
rails-i18n
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
ruby-progressbar (~> 1.8, >= 1.8.1)
|
||||
terminal-table (>= 1.5.1)
|
||||
image_processing (1.12.2)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
io-console (0.8.1)
|
||||
irb (1.15.2)
|
||||
pp (>= 0.6.0)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jmespath (1.6.2)
|
||||
json (2.12.2)
|
||||
kgio (2.10.0)
|
||||
launchy (2.5.2)
|
||||
addressable (~> 2.8)
|
||||
logger (1.7.0)
|
||||
lograge (0.14.0)
|
||||
actionpack (>= 4)
|
||||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
request_store (~> 1.0)
|
||||
loofah (2.24.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.8.1)
|
||||
mini_mime (>= 0.1.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
marcel (1.0.2)
|
||||
matrix (0.4.3)
|
||||
mechanize (2.12.2)
|
||||
addressable (~> 2.8)
|
||||
base64
|
||||
domain_name (~> 0.5, >= 0.5.20190701)
|
||||
http-cookie (~> 1.0, >= 1.0.3)
|
||||
mime-types (~> 3.3)
|
||||
net-http-digest_auth (~> 1.4, >= 1.4.1)
|
||||
net-http-persistent (>= 2.5.2, < 5.0.dev)
|
||||
nkf
|
||||
nokogiri (~> 1.11, >= 1.11.2)
|
||||
rubyntlm (~> 0.6, >= 0.6.3)
|
||||
webrick (~> 1.7)
|
||||
webrobots (~> 0.1.2)
|
||||
method_source (1.1.0)
|
||||
mime-types (3.5.2)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2024.0206)
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.9)
|
||||
minitest (5.25.5)
|
||||
mono_logger (1.1.2)
|
||||
multi_json (1.15.0)
|
||||
multi_test (1.1.0)
|
||||
multi_xml (0.6.0)
|
||||
mustermann (3.0.0)
|
||||
ruby2_keywords (~> 0.0.1)
|
||||
mysql2 (0.5.6)
|
||||
n_plus_one_control (0.7.2)
|
||||
net-http (0.6.0)
|
||||
uri
|
||||
net-http-digest_auth (1.4.1)
|
||||
net-http-persistent (4.0.2)
|
||||
connection_pool (~> 2.2)
|
||||
net-imap (0.5.9)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
net-protocol
|
||||
net-protocol (0.2.2)
|
||||
timeout
|
||||
net-scp (4.0.0)
|
||||
net-ssh (>= 2.6.5, < 8.0.0)
|
||||
net-sftp (4.0.0)
|
||||
net-ssh (>= 5.0.0, < 8.0.0)
|
||||
net-smtp (0.5.1)
|
||||
net-protocol
|
||||
net-ssh (7.2.1)
|
||||
net-ssh-gateway (2.0.0)
|
||||
net-ssh (>= 4.0.0)
|
||||
netrc (0.11.0)
|
||||
nio4r (2.7.4)
|
||||
nkf (0.2.0)
|
||||
nokogiri (1.18.9)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
orm_adapter (0.5.0)
|
||||
pagy (9.3.3)
|
||||
parallel (1.25.1)
|
||||
parser (3.3.8.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
permit_yo (2.1.3)
|
||||
phraseapp-in-context-editor-ruby (1.4.0)
|
||||
i18n (>= 0.6)
|
||||
json (>= 1.8, < 3)
|
||||
phraseapp-ruby (~> 1.3)
|
||||
request_store (~> 1.3)
|
||||
phraseapp-ruby (1.6.0)
|
||||
pickle (0.9.0)
|
||||
cucumber (>= 3.0, < 10.0)
|
||||
rake
|
||||
power_assert (2.0.3)
|
||||
pp (0.6.2)
|
||||
prettyprint
|
||||
prettyprint (0.2.0)
|
||||
pry (0.15.2)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
pry-byebug (3.11.0)
|
||||
byebug (~> 12.0)
|
||||
pry (>= 0.13, < 0.16)
|
||||
psych (5.2.6)
|
||||
date
|
||||
stringio
|
||||
public_suffix (6.0.2)
|
||||
puma (6.5.0)
|
||||
nio4r (~> 2.0)
|
||||
puma_worker_killer (1.0.0)
|
||||
bigdecimal (>= 2.0)
|
||||
get_process_mem (>= 0.2)
|
||||
puma (>= 2.7)
|
||||
pundit (2.3.1)
|
||||
activesupport (>= 3.0.0)
|
||||
pwned (2.4.1)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (2.2.17)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-dev-mark (0.8.0)
|
||||
rack (>= 1.1, < 4.0)
|
||||
rack-protection (3.2.0)
|
||||
base64 (>= 0.1.0)
|
||||
rack (~> 2.2, >= 2.2.4)
|
||||
rack-session (1.0.2)
|
||||
rack (< 3)
|
||||
rack-test (2.2.0)
|
||||
rack (>= 1.3)
|
||||
rack-timeout (0.7.0)
|
||||
rackup (1.0.1)
|
||||
rack (< 3)
|
||||
webrick
|
||||
rails (7.2.2.2)
|
||||
actioncable (= 7.2.2.2)
|
||||
actionmailbox (= 7.2.2.2)
|
||||
actionmailer (= 7.2.2.2)
|
||||
actionpack (= 7.2.2.2)
|
||||
actiontext (= 7.2.2.2)
|
||||
actionview (= 7.2.2.2)
|
||||
activejob (= 7.2.2.2)
|
||||
activemodel (= 7.2.2.2)
|
||||
activerecord (= 7.2.2.2)
|
||||
activestorage (= 7.2.2.2)
|
||||
activesupport (= 7.2.2.2)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.2.2.2)
|
||||
rails-controller-testing (1.0.5)
|
||||
actionpack (>= 5.0.1.rc1)
|
||||
actionview (>= 5.0.1.rc1)
|
||||
activesupport (>= 5.0.1.rc1)
|
||||
rails-dom-testing (2.3.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.6.2)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||
rails-i18n (7.0.9)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 6.0.0, < 8)
|
||||
railties (7.2.2.2)
|
||||
actionpack (= 7.2.2.2)
|
||||
activesupport (= 7.2.2.2)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
raindrops (0.20.1)
|
||||
rake (13.3.0)
|
||||
rdoc (6.14.2)
|
||||
erb
|
||||
psych (>= 4.0.0)
|
||||
redis (3.3.5)
|
||||
redis-namespace (1.8.2)
|
||||
redis (>= 3.0.4)
|
||||
regexp_parser (2.10.0)
|
||||
reline (0.6.2)
|
||||
io-console (~> 0.5)
|
||||
request_store (1.6.0)
|
||||
rack (>= 1.4)
|
||||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
resque (2.6.0)
|
||||
mono_logger (~> 1.0)
|
||||
multi_json (~> 1.0)
|
||||
redis-namespace (~> 1.6)
|
||||
sinatra (>= 0.9.2)
|
||||
resque-scheduler (4.10.2)
|
||||
mono_logger (~> 1.0)
|
||||
redis (>= 3.3)
|
||||
resque (>= 1.27)
|
||||
rufus-scheduler (~> 3.2, != 3.3)
|
||||
rest-client (2.1.0)
|
||||
http-accept (>= 1.7.0, < 2.0)
|
||||
http-cookie (>= 1.0.2, < 2.0)
|
||||
mime-types (>= 1.16, < 4.0)
|
||||
netrc (~> 0.8)
|
||||
rexml (3.4.1)
|
||||
rollout (2.4.3)
|
||||
rspec-core (3.13.5)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.5)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-mocks (3.13.5)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (6.1.5)
|
||||
actionpack (>= 6.1)
|
||||
activesupport (>= 6.1)
|
||||
railties (>= 6.1)
|
||||
rspec-core (~> 3.13)
|
||||
rspec-expectations (~> 3.13)
|
||||
rspec-mocks (~> 3.13)
|
||||
rspec-support (~> 3.13)
|
||||
rspec-support (3.13.4)
|
||||
rubocop (1.22.3)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.0.0.0)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml
|
||||
rubocop-ast (>= 1.12.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 3.0)
|
||||
rubocop-ast (1.31.2)
|
||||
parser (>= 3.3.0.4)
|
||||
rubocop-rails (2.12.4)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-rspec (2.6.0)
|
||||
rubocop (~> 1.19)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-vips (2.2.1)
|
||||
ffi (~> 1.12)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyntlm (0.6.3)
|
||||
rubyzip (2.4.1)
|
||||
rufus-scheduler (3.9.1)
|
||||
fugit (~> 1.1, >= 1.1.6)
|
||||
rvm-capistrano (1.5.6)
|
||||
capistrano (~> 2.15.4)
|
||||
sanitize (6.1.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
securerandom (0.4.1)
|
||||
selenium-webdriver (4.34.0)
|
||||
base64 (~> 0.2)
|
||||
logger (~> 1.4)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
rubyzip (>= 1.2.2, < 3.0)
|
||||
websocket (~> 1.0)
|
||||
sentry-rails (5.18.0)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby (~> 5.18.0)
|
||||
sentry-resque (5.18.0)
|
||||
resque (>= 1.24)
|
||||
sentry-ruby (~> 5.18.0)
|
||||
sentry-ruby (5.18.0)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
shoulda (4.0.0)
|
||||
shoulda-context (~> 2.0)
|
||||
shoulda-matchers (~> 4.0)
|
||||
shoulda-context (2.0.0)
|
||||
shoulda-matchers (4.5.1)
|
||||
activesupport (>= 4.2.0)
|
||||
simplecov (0.22.0)
|
||||
docile (~> 1.1)
|
||||
simplecov-html (~> 0.11)
|
||||
simplecov_json_formatter (~> 0.1)
|
||||
simplecov-cobertura (3.0.0)
|
||||
rexml
|
||||
simplecov (~> 0.19)
|
||||
simplecov-html (0.13.2)
|
||||
simplecov_json_formatter (0.1.4)
|
||||
sinatra (3.2.0)
|
||||
mustermann (~> 3.0)
|
||||
rack (~> 2.2, >= 2.2.4)
|
||||
rack-protection (= 3.2.0)
|
||||
tilt (~> 2.0)
|
||||
smart_properties (1.17.0)
|
||||
sprockets (3.7.2)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (> 1, < 3)
|
||||
stackprof (0.2.26)
|
||||
stringex (2.8.6)
|
||||
stringio (3.1.7)
|
||||
sys-uname (1.3.1)
|
||||
ffi (~> 1.1)
|
||||
terminal-table (4.0.0)
|
||||
unicode-display_width (>= 1.1.1, < 4)
|
||||
terrapin (1.0.1)
|
||||
climate_control
|
||||
test-unit (3.6.2)
|
||||
power_assert
|
||||
thor (1.4.0)
|
||||
tilt (2.3.0)
|
||||
timecop (0.9.10)
|
||||
timeliness (0.4.5)
|
||||
timeout (0.4.3)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
unicode (0.4.4.5)
|
||||
unicode-display_width (2.6.0)
|
||||
unicode_utils (1.4.0)
|
||||
unicorn (5.8.0)
|
||||
kgio (~> 2.6)
|
||||
raindrops (~> 0.7)
|
||||
unidecoder (1.1.2)
|
||||
uniform_notifier (1.17.0)
|
||||
uri (1.0.3)
|
||||
useragent (0.16.11)
|
||||
vcr (6.3.1)
|
||||
base64
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
webmock (3.25.1)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webrick (1.9.1)
|
||||
webrobots (0.1.2)
|
||||
websocket (1.2.11)
|
||||
websocket-driver (0.8.0)
|
||||
base64
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
whenever (0.6.8)
|
||||
aaronh-chronic (>= 0.3.9)
|
||||
activesupport (>= 2.3.4)
|
||||
whiny_validation (1.1)
|
||||
activemodel
|
||||
activesupport
|
||||
will_paginate (4.0.0)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.7.3)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
actionpack-page_caching
|
||||
active_record_query_trace (~> 1.6, >= 1.6.1)
|
||||
acts_as_list (~> 0.9.7)
|
||||
addressable
|
||||
after_commit_everywhere
|
||||
akismetor
|
||||
audited (~> 5.3)
|
||||
awesome_print
|
||||
aws-sdk-s3
|
||||
bcrypt
|
||||
bcrypt_pbkdf (>= 1.0, < 2.0)
|
||||
brakeman
|
||||
bullet (>= 5.7.3)
|
||||
bundler
|
||||
bundler-audit
|
||||
capistrano-gitflow_version (>= 0.0.3)
|
||||
capybara
|
||||
capybara-screenshot
|
||||
connection_pool
|
||||
css_parser
|
||||
cucumber
|
||||
cucumber-rails
|
||||
cucumber-timecop
|
||||
dalli
|
||||
database_cleaner
|
||||
departure (~> 6.8)
|
||||
devise
|
||||
devise-async
|
||||
devise-pwned_password
|
||||
ed25519 (>= 1.2, < 2.0)
|
||||
elasticsearch (= 8.18.0)
|
||||
email_spec (= 1.6.0)
|
||||
erb_lint (= 0.4.0)
|
||||
escape_utils (= 1.2.1)
|
||||
factory_bot
|
||||
factory_bot_rails
|
||||
faker
|
||||
fastimage
|
||||
globalize (~> 7.0)
|
||||
god (~> 0.13.7)
|
||||
google_visualr!
|
||||
htmlentities
|
||||
httparty
|
||||
i18n-tasks
|
||||
image_processing (~> 1.12)
|
||||
kgio (= 2.10.0)
|
||||
launchy
|
||||
lograge
|
||||
marcel (= 1.0.2)
|
||||
mechanize
|
||||
minitest
|
||||
mysql2
|
||||
n_plus_one_control
|
||||
nokogiri (>= 1.8.5)
|
||||
pagy (~> 9.3)
|
||||
permit_yo
|
||||
phraseapp-in-context-editor-ruby (>= 1.0.6)
|
||||
pickle
|
||||
pry-byebug
|
||||
puma (~> 6.5.0)
|
||||
puma_worker_killer
|
||||
pundit
|
||||
rack (~> 2.2)
|
||||
rack-attack
|
||||
rack-dev-mark (>= 0.7.8)
|
||||
rack-timeout
|
||||
rails (~> 7.2)
|
||||
rails-controller-testing
|
||||
rails-i18n
|
||||
rails-observers!
|
||||
redis (~> 3.3.5)
|
||||
redis-namespace
|
||||
resque (>= 1.14.0)
|
||||
resque-scheduler
|
||||
rest-client (~> 2.1.0)
|
||||
rollout
|
||||
rspec-rails (~> 6.0)
|
||||
rubocop (= 1.22.3)
|
||||
rubocop-rails (= 2.12.4)
|
||||
rubocop-rspec (= 2.6.0)
|
||||
rvm-capistrano
|
||||
sanitize (>= 4.6.5)
|
||||
selenium-webdriver
|
||||
sentry-rails
|
||||
sentry-resque
|
||||
sentry-ruby
|
||||
shoulda
|
||||
simplecov
|
||||
simplecov-cobertura
|
||||
sprockets (< 4)
|
||||
stackprof
|
||||
terrapin
|
||||
test-unit (~> 3.2)
|
||||
timecop
|
||||
timeliness
|
||||
unicode
|
||||
unicode_utils (>= 1.4.0)
|
||||
unicorn (~> 5.5)
|
||||
unidecoder
|
||||
vcr (~> 6.2)
|
||||
webmock
|
||||
whenever (~> 0.6.2)
|
||||
whiny_validation
|
||||
will_paginate (>= 3.0.2)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.2.7p253
|
||||
|
||||
BUNDLED WITH
|
||||
2.6.3
|
||||
339
LICENSE
Normal file
339
LICENSE
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
{description}
|
||||
Copyright (C) {year} {fullname}
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
{signature of Ty Coon}, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
69
README.md
Normal file
69
README.md
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
OTW-Archive
|
||||
=========
|
||||
|
||||
this is the fork of the OTW's OTW-Archive software that powers symphony and sunset archives. the views and such are for symphony; the sunset version is coming soon.
|
||||
|
||||
features:
|
||||
|
||||
-status updates
|
||||
-random user button
|
||||
-estimated reading time for each work
|
||||
|
||||
more always in progress!
|
||||
|
||||
current features in progress:
|
||||
|
||||
-forums
|
||||
|
||||
|
||||
Credits for features not made by me
|
||||
======
|
||||
|
||||
Reading time estimation: [paintedflowers](https://bytes.4-walls.net/paintedflowers/Futurethingstoteswiitotwcode) on Bytes
|
||||
---------
|
||||
|
||||
|
||||
[](https://github.com/otwcode/otwarchive/actions/workflows/automated-tests.yml?query=branch%3Amaster) [](https://www.codeship.io/projects/14476) [](https://app.codecov.io/gh/otwcode/otwarchive)
|
||||
|
||||
The OTW-Archive software is an open-source web application intended for hosting archives of fanworks, including fanfic, fanart, and fan vids.
|
||||
|
||||
Its development is sponsored and managed by the [Organization for Transformative Works](https://www.transformativeworks.org/), a nonprofit organization by and for fans.
|
||||
|
||||
Release Status
|
||||
---------
|
||||
Development of the OTW-Archive software is an ongoing labor of love. You can see it in action on the [Archive of Our Own](https://archiveofourown.org/), aka AO3, a multifandom archive also run by the OTW.
|
||||
|
||||
You can find more information about the [history and future of the AO3 project on the OTW website](https://www.transformativeworks.org/archive_of_our_own/).
|
||||
|
||||
If you wish to use this software, SquidgeWorld has generously provided [setup notes](https://squidgeworld.org/works/34491).
|
||||
|
||||
How to Contribute
|
||||
----------
|
||||
We welcome pull requests for bugs described in our issue tracker. Please see our [Contributing Guidelines](https://github.com/otwcode/otwarchive/blob/master/CONTRIBUTING.md) for further information!
|
||||
|
||||
* [Bug Tracker](https://otwarchive.atlassian.net/projects/AO3/issues)
|
||||
* [Developer Documentation](https://github.com/otwcode/otwarchive/wiki)
|
||||
* [Commit Policy](https://github.com/otwcode/otwarchive/wiki/Commit-policy)
|
||||
|
||||
We do not have a public chat, but you are welcome to contact us at otw-coders@transformativeworks.org if you have any questions.
|
||||
|
||||
We grant your Jira account permissions for commenting on, assigning, and transitioning issues [after you create your first pull request](https://github.com/otwcode/otwarchive/blob/master/CONTRIBUTING.md#workflow).
|
||||
|
||||
API
|
||||
----------
|
||||
There is currently no API for the OTW-Archive software. While it is something we're considering for the future, we ask that contributors instead focus on issues already in our [Jira issue tracker](https://otwarchive.atlassian.net/).
|
||||
|
||||
License and Acknowledgments
|
||||
----------
|
||||
The Archive code is licensed under [GPL-2.0-or-later](https://www.gnu.org/licenses/gpl-2.0.html) by the [Organization for Transformative Works](https://www.transformativeworks.org/).
|
||||
|
||||
We benefit from software and services that are free to use for Open Source projects, including:
|
||||
|
||||
* [RubyMine IDE](https://www.jetbrains.com/ruby/) by JetBrains
|
||||
* [Codeship](https://codeship.com/)
|
||||
* [Hound](https://houndci.com/) by [thoughtbot](https://thoughtbot.com/)
|
||||
* [BrowserStack](https://www.browserstack.com)
|
||||
* [Sentry](https://sentry.io)
|
||||
* [Full list of acknowledgments](ACKNOWLEDGMENTS.md)
|
||||
|
||||
Thank you kindly!
|
||||
9
Rakefile
Normal file
9
Rakefile
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Add your own tasks in files placed in lib/tasks ending in .rake,
|
||||
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
|
||||
|
||||
require File.expand_path('../config/application', __FILE__)
|
||||
require 'rake'
|
||||
require 'resque/tasks'
|
||||
|
||||
include Rake::DSL
|
||||
Otwarchive::Application.load_tasks
|
||||
14
SECURITY.md
Normal file
14
SECURITY.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Reporting a security issue
|
||||
|
||||
Please email us directly at [otw-coders@transformativeworks.org](mailto:otw-coders@transformativeworks.org)
|
||||
with details and reproduction steps. We will get back to you as soon as possible,
|
||||
and we may ask for additional information or guidance.
|
||||
|
||||
Please avoid testing for security issues on the Archive of Our Own itself,
|
||||
as you risk disrupting other users and violating the [Terms of Service](https://archiveofourown.org/content#II.K.1).
|
||||
|
||||
Please give us a reasonable amount of time to address the issue before any
|
||||
disclosure to the public or a third-party.
|
||||
|
||||
We thank you for your effort and responsible disclosure! We will make sure you
|
||||
receive due credit on [our public release notes](https://archiveofourown.org/admin_posts?tag=1).
|
||||
38
app/controllers/abuse_reports_controller.rb
Normal file
38
app/controllers/abuse_reports_controller.rb
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
class AbuseReportsController < ApplicationController
|
||||
skip_before_action :store_location
|
||||
before_action :load_abuse_languages
|
||||
|
||||
def new
|
||||
@abuse_report = AbuseReport.new
|
||||
reporter = current_admin || current_user
|
||||
if reporter.present?
|
||||
@abuse_report.email = reporter.email
|
||||
@abuse_report.username = reporter.login
|
||||
end
|
||||
@abuse_report.url = params[:url] || request.env["HTTP_REFERER"]
|
||||
end
|
||||
|
||||
def create
|
||||
@abuse_report = AbuseReport.new(abuse_report_params)
|
||||
@abuse_report.ip_address = request.remote_ip
|
||||
if @abuse_report.save
|
||||
@abuse_report.email_and_send
|
||||
flash[:notice] = ts("Your report was submitted to the Policy & Abuse team. A confirmation message has been sent to the email address you provided.")
|
||||
redirect_to root_path
|
||||
else
|
||||
render action: "new"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_abuse_languages
|
||||
@abuse_languages = Language.where(abuse_support_available: true).default_order
|
||||
end
|
||||
|
||||
def abuse_report_params
|
||||
params.require(:abuse_report).permit(
|
||||
:username, :email, :language, :summary, :url, :comment
|
||||
)
|
||||
end
|
||||
end
|
||||
11
app/controllers/admin/activities_controller.rb
Normal file
11
app/controllers/admin/activities_controller.rb
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
class Admin::ActivitiesController < Admin::BaseController
|
||||
def index
|
||||
@activities = AdminActivity.order("created_at DESC").page(params[:page])
|
||||
authorize @activities
|
||||
end
|
||||
|
||||
def show
|
||||
@activity = AdminActivity.find(params[:id])
|
||||
authorize @activity
|
||||
end
|
||||
end
|
||||
61
app/controllers/admin/admin_invitations_controller.rb
Normal file
61
app/controllers/admin/admin_invitations_controller.rb
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
class Admin::AdminInvitationsController < Admin::BaseController
|
||||
|
||||
def index
|
||||
end
|
||||
|
||||
def create
|
||||
@invitation = current_admin.invitations.new(invitation_params)
|
||||
|
||||
if @invitation.invitee_email.blank?
|
||||
flash[:error] = t('no_email', default: "Please enter an email address.")
|
||||
render action: 'index'
|
||||
elsif @invitation.save
|
||||
flash[:notice] = t('sent', default: "An invitation was sent to %{email_address}", email_address: @invitation.invitee_email)
|
||||
redirect_to admin_invitations_path
|
||||
else
|
||||
render action: 'index'
|
||||
end
|
||||
end
|
||||
|
||||
def invite_from_queue
|
||||
count = invitation_params[:invite_from_queue].to_i
|
||||
InviteFromQueueJob.perform_later(count: count, creator: current_admin)
|
||||
|
||||
flash[:notice] = t(".success", count: count)
|
||||
redirect_to admin_invitations_path
|
||||
end
|
||||
|
||||
def grant_invites_to_users
|
||||
if invitation_params[:user_group] == "All"
|
||||
Invitation.grant_all(invitation_params[:number_of_invites].to_i)
|
||||
else
|
||||
Invitation.grant_empty(invitation_params[:number_of_invites].to_i)
|
||||
end
|
||||
flash[:notice] = t('invites_created', default: 'Invitations successfully created.')
|
||||
redirect_to admin_invitations_path
|
||||
end
|
||||
|
||||
def find
|
||||
unless invitation_params[:user_name].blank?
|
||||
@user = User.find_by(login: invitation_params[:user_name])
|
||||
@hide_dashboard = true
|
||||
@invitations = @user.invitations if @user
|
||||
end
|
||||
if !invitation_params[:token].blank?
|
||||
@invitations = Invitation.where(token: invitation_params[:token])
|
||||
elsif invitation_params[:invitee_email].present?
|
||||
@invitations = Invitation.where("invitee_email LIKE ?", "%#{invitation_params[:invitee_email]}%")
|
||||
end
|
||||
|
||||
return if @user || @invitations.present?
|
||||
|
||||
flash.now[:error] = t(".user_not_found")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def invitation_params
|
||||
params.require(:invitation).permit(:invitee_email, :invite_from_queue,
|
||||
:user_group, :number_of_invites, :user_name, :token)
|
||||
end
|
||||
end
|
||||
237
app/controllers/admin/admin_users_controller.rb
Normal file
237
app/controllers/admin/admin_users_controller.rb
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
class Admin::AdminUsersController < Admin::BaseController
|
||||
include ExportsHelper
|
||||
|
||||
before_action :set_roles, only: [:index, :bulk_search]
|
||||
before_action :load_user, only: [:show, :update, :confirm_delete_user_creations, :destroy_user_creations, :troubleshoot, :activate, :creations]
|
||||
before_action :user_is_banned, only: [:confirm_delete_user_creations, :destroy_user_creations]
|
||||
before_action :load_user_creations, only: [:confirm_delete_user_creations, :creations]
|
||||
|
||||
def set_roles
|
||||
@roles = Role.assignable.distinct
|
||||
end
|
||||
|
||||
def load_user
|
||||
@user = User.find_by!(login: params[:id])
|
||||
end
|
||||
|
||||
def user_is_banned
|
||||
return if @user&.banned?
|
||||
|
||||
flash[:error] = ts("That user is not banned!")
|
||||
redirect_to admin_users_path
|
||||
end
|
||||
|
||||
def load_user_creations
|
||||
@works = @user.works.paginate(page: params[:works_page])
|
||||
@comments = @user.comments.paginate(page: params[:comments_page])
|
||||
end
|
||||
|
||||
def index
|
||||
authorize User
|
||||
|
||||
# Values for the role dropdown:
|
||||
@role_values = @roles.map { |role| [role.name.humanize.titlecase, role.id] }
|
||||
|
||||
return if search_params.empty?
|
||||
|
||||
@query = UserQuery.new(search_params)
|
||||
@users = @query.search_results.scope(:with_includes_for_admin_index)
|
||||
end
|
||||
|
||||
def bulk_search
|
||||
authorize User
|
||||
@emails = params[:emails].split if params[:emails]
|
||||
if @emails.present?
|
||||
found_users, not_found_emails, duplicates = User.search_multiple_by_email(@emails)
|
||||
@users = found_users.paginate(page: params[:page] || 1)
|
||||
|
||||
if params[:download_button]
|
||||
header = [%w(Email Username)]
|
||||
found = found_users.map { |u| [u.email, u.login] }
|
||||
not_found = not_found_emails.map { |email| [email, ""] }
|
||||
send_csv_data(header + found + not_found, "bulk_user_search_#{Time.now.strftime("%Y-%m-%d-%H%M")}.csv")
|
||||
flash.now[:notice] = ts("Downloaded CSV")
|
||||
end
|
||||
@results = {
|
||||
total: @emails.size,
|
||||
searched: found_users.size + not_found_emails.size,
|
||||
found_users: found_users,
|
||||
not_found_emails: not_found_emails,
|
||||
duplicates: duplicates
|
||||
}
|
||||
else
|
||||
@results = {}
|
||||
end
|
||||
end
|
||||
|
||||
# GET admin/users/1
|
||||
def show
|
||||
authorize @user
|
||||
@page_subtitle = t(".page_title", login: @user.login)
|
||||
log_items
|
||||
end
|
||||
|
||||
# POST admin/users/update
|
||||
def update
|
||||
authorize @user
|
||||
|
||||
attributes = permitted_attributes(@user)
|
||||
if attributes[:email].present?
|
||||
@user.skip_reconfirmation!
|
||||
@user.email = attributes[:email]
|
||||
end
|
||||
if attributes[:roles].present?
|
||||
# Roles that the current admin can add or remove
|
||||
allowed_roles = UserPolicy::ALLOWED_USER_ROLES_BY_ADMIN_ROLES
|
||||
.values_at(*current_admin.roles)
|
||||
.compact
|
||||
.flatten
|
||||
|
||||
# Other roles the current user has
|
||||
out_of_scope_roles = @user.roles.to_a.reject { |role| allowed_roles.include?(role.name) }
|
||||
|
||||
request_roles = Role.where(
|
||||
id: attributes[:roles],
|
||||
name: [allowed_roles]
|
||||
)
|
||||
|
||||
@user.roles = out_of_scope_roles + request_roles
|
||||
end
|
||||
|
||||
if @user.save
|
||||
flash[:notice] = ts("User was successfully updated.")
|
||||
else
|
||||
flash[:error] = ts("The user %{name} could not be updated: %{errors}", name: params[:id], errors: @user.errors.full_messages.join(" "))
|
||||
end
|
||||
redirect_to request.referer || root_path
|
||||
end
|
||||
|
||||
def update_next_of_kin
|
||||
@user = authorize User.find_by!(login: params[:user_login])
|
||||
kin = User.find_by(login: params[:next_of_kin_name])
|
||||
kin_email = params[:next_of_kin_email]
|
||||
|
||||
fnok = @user.fannish_next_of_kin
|
||||
previous_kin = fnok&.kin
|
||||
fnok ||= @user.build_fannish_next_of_kin
|
||||
fnok.assign_attributes(kin: kin, kin_email: kin_email)
|
||||
|
||||
unless fnok.changed?
|
||||
flash[:notice] = ts("No change to fannish next of kin.")
|
||||
redirect_to admin_user_path(@user) and return
|
||||
end
|
||||
|
||||
# Remove FNOK that already exists.
|
||||
if fnok.persisted? && kin.blank? && kin_email.blank?
|
||||
fnok.destroy
|
||||
@user.log_removal_of_next_of_kin(previous_kin, admin: current_admin)
|
||||
flash[:notice] = ts("Fannish next of kin was removed.")
|
||||
redirect_to admin_user_path(@user) and return
|
||||
end
|
||||
|
||||
if fnok.save
|
||||
@user.log_removal_of_next_of_kin(previous_kin, admin: current_admin)
|
||||
@user.log_assignment_of_next_of_kin(kin, admin: current_admin)
|
||||
flash[:notice] = ts("Fannish next of kin was updated.")
|
||||
redirect_to admin_user_path(@user)
|
||||
else
|
||||
@hide_dashboard = true
|
||||
log_items
|
||||
render :show
|
||||
end
|
||||
end
|
||||
|
||||
def update_status
|
||||
@user = User.find_by!(login: params[:user_login])
|
||||
|
||||
# Authorize on the manager, as we need to check which specific actions the admin can do.
|
||||
@user_manager = authorize UserManager.new(current_admin, @user, params)
|
||||
if @user_manager.save
|
||||
flash[:notice] = @user_manager.success_message
|
||||
if @user_manager.admin_action == "spamban"
|
||||
redirect_to confirm_delete_user_creations_admin_user_path(@user)
|
||||
else
|
||||
redirect_to admin_user_path(@user)
|
||||
end
|
||||
else
|
||||
flash[:error] = @user_manager.error_message
|
||||
redirect_to admin_user_path(@user)
|
||||
end
|
||||
end
|
||||
|
||||
def confirm_delete_user_creations
|
||||
authorize @user
|
||||
@bookmarks = @user.bookmarks
|
||||
@collections = @user.sole_owned_collections
|
||||
@series = @user.series
|
||||
@page_subtitle = t(".page_title", login: @user.login)
|
||||
end
|
||||
|
||||
def destroy_user_creations
|
||||
authorize @user
|
||||
|
||||
creations = @user.works + @user.bookmarks + @user.sole_owned_collections
|
||||
creations.each do |creation|
|
||||
AdminActivity.log_action(current_admin, creation, action: "destroy spam", summary: creation.inspect)
|
||||
creation.mark_as_spam! if creation.respond_to?(:mark_as_spam!)
|
||||
creation.destroy
|
||||
end
|
||||
|
||||
# comments are special and needs to be handled separately
|
||||
@user.comments.not_deleted.each do |comment|
|
||||
AdminActivity.log_action(current_admin, comment, action: "destroy spam", summary: comment.inspect)
|
||||
comment.submit_spam
|
||||
comment.destroy_or_mark_deleted # comments with replies cannot be destroyed, mark deleted instead
|
||||
end
|
||||
|
||||
flash[:notice] = t(".success", login: @user.login)
|
||||
redirect_to(admin_users_path)
|
||||
end
|
||||
|
||||
def troubleshoot
|
||||
authorize @user
|
||||
|
||||
@user.fix_user_subscriptions
|
||||
@user.set_user_work_dates
|
||||
@user.reindex_user_creations
|
||||
@user.update_works_index_timestamp!
|
||||
@user.create_log_item(options = { action: ArchiveConfig.ACTION_TROUBLESHOOT, admin_id: current_admin.id })
|
||||
flash[:notice] = ts("User account troubleshooting complete.")
|
||||
redirect_to(request.env["HTTP_REFERER"] || root_path) && return
|
||||
end
|
||||
|
||||
def activate
|
||||
authorize @user
|
||||
|
||||
@user.activate
|
||||
if @user.active?
|
||||
@user.create_log_item( options = { action: ArchiveConfig.ACTION_ACTIVATE, note: "Manually Activated", admin_id: current_admin.id })
|
||||
flash[:notice] = ts("User Account Activated")
|
||||
redirect_to action: :show
|
||||
else
|
||||
flash[:error] = ts("Attempt to activate account failed.")
|
||||
redirect_to action: :show
|
||||
end
|
||||
end
|
||||
|
||||
def creations
|
||||
authorize @user
|
||||
@page_subtitle = t(".page_title", login: @user.login)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def search_params
|
||||
allowed_params = if policy(User).can_view_past?
|
||||
%i[name email role_id user_id inactive page commit search_past]
|
||||
else
|
||||
%i[name email role_id user_id inactive page commit]
|
||||
end
|
||||
|
||||
params.permit(*allowed_params)
|
||||
end
|
||||
|
||||
def log_items
|
||||
@log_items ||= @user.log_items.sort_by(&:created_at).reverse
|
||||
end
|
||||
end
|
||||
71
app/controllers/admin/api_controller.rb
Normal file
71
app/controllers/admin/api_controller.rb
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
class Admin::ApiController < Admin::BaseController
|
||||
before_action :check_for_cancel, only: [:create, :update]
|
||||
|
||||
def index
|
||||
@page_subtitle = t(".page_title")
|
||||
@api_keys = if params[:query]
|
||||
sql_query = "%" + params[:query] + "%"
|
||||
ApiKey.where("name LIKE ?", sql_query).order("name").paginate(page: params[:page])
|
||||
else
|
||||
ApiKey.order("name").paginate(page: params[:page])
|
||||
end
|
||||
authorize @api_keys
|
||||
end
|
||||
|
||||
def show
|
||||
redirect_to action: "index"
|
||||
end
|
||||
|
||||
def new
|
||||
@page_subtitle = t(".page_title")
|
||||
@api_key = authorize ApiKey.new
|
||||
end
|
||||
|
||||
def create
|
||||
authorize ApiKey
|
||||
# Use provided api key params if available otherwise fallback to empty
|
||||
# ApiKey object
|
||||
@api_key = params[:api_key].nil? ? ApiKey.new : ApiKey.new(api_key_params)
|
||||
if @api_key.save
|
||||
flash[:notice] = ts("New token successfully created")
|
||||
redirect_to action: "index"
|
||||
else
|
||||
render "new"
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@page_subtitle = t(".page_title")
|
||||
@api_key = authorize ApiKey.find(params[:id])
|
||||
end
|
||||
|
||||
def update
|
||||
@api_key = authorize ApiKey.find(params[:id])
|
||||
if @api_key.update(api_key_params)
|
||||
flash[:notice] = ts("Access token was successfully updated")
|
||||
redirect_to action: "index"
|
||||
else
|
||||
render "edit"
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@api_key = authorize ApiKey.find(params[:id])
|
||||
@api_key.destroy
|
||||
redirect_to(admin_api_path)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def api_key_params
|
||||
params.require(:api_key).permit(
|
||||
:name, :access_token, :banned
|
||||
)
|
||||
end
|
||||
|
||||
def check_for_cancel
|
||||
if params[:cancel_button]
|
||||
redirect_to action: "index"
|
||||
end
|
||||
end
|
||||
end
|
||||
82
app/controllers/admin/banners_controller.rb
Normal file
82
app/controllers/admin/banners_controller.rb
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
class Admin::BannersController < Admin::BaseController
|
||||
|
||||
# GET /admin/banners
|
||||
def index
|
||||
authorize(AdminBanner)
|
||||
|
||||
@admin_banners = AdminBanner.order("id DESC").paginate(page: params[:page])
|
||||
end
|
||||
|
||||
# GET /admin/banners/1
|
||||
def show
|
||||
@admin_banner = authorize AdminBanner.find(params[:id])
|
||||
end
|
||||
|
||||
# GET /admin/banners/new
|
||||
def new
|
||||
@admin_banner = authorize AdminBanner.new
|
||||
end
|
||||
|
||||
# GET /admin/banners/1/edit
|
||||
def edit
|
||||
@admin_banner = authorize AdminBanner.find(params[:id])
|
||||
end
|
||||
|
||||
# POST /admin/banners
|
||||
def create
|
||||
@admin_banner = authorize AdminBanner.new(admin_banner_params)
|
||||
|
||||
if @admin_banner.save
|
||||
if @admin_banner.active?
|
||||
AdminBanner.banner_on
|
||||
flash[:notice] = ts('Setting banner back on for all users. This may take some time.')
|
||||
else
|
||||
flash[:notice] = ts('Banner successfully created.')
|
||||
end
|
||||
redirect_to @admin_banner
|
||||
else
|
||||
render action: 'new'
|
||||
end
|
||||
end
|
||||
|
||||
# PUT /admin/banners/1
|
||||
def update
|
||||
@admin_banner = authorize AdminBanner.find(params[:id])
|
||||
|
||||
if !@admin_banner.update(admin_banner_params)
|
||||
render action: 'edit'
|
||||
elsif params[:admin_banner_minor_edit]
|
||||
flash[:notice] = ts('Updating banner for users who have not already dismissed it. This may take some time.')
|
||||
redirect_to @admin_banner
|
||||
else
|
||||
if @admin_banner.active?
|
||||
AdminBanner.banner_on
|
||||
flash[:notice] = ts('Setting banner back on for all users. This may take some time.')
|
||||
else
|
||||
flash[:notice] = ts('Banner successfully updated.')
|
||||
end
|
||||
redirect_to @admin_banner
|
||||
end
|
||||
end
|
||||
|
||||
# GET /admin/banners/1/confirm_delete
|
||||
def confirm_delete
|
||||
@admin_banner = authorize AdminBanner.find(params[:id])
|
||||
end
|
||||
|
||||
# DELETE /admin/banners/1
|
||||
def destroy
|
||||
@admin_banner = authorize AdminBanner.find(params[:id])
|
||||
@admin_banner.destroy
|
||||
|
||||
flash[:notice] = ts('Banner successfully deleted.')
|
||||
redirect_to admin_banners_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def admin_banner_params
|
||||
params.require(:admin_banner).permit(:content, :banner_type, :active)
|
||||
end
|
||||
|
||||
end
|
||||
3
app/controllers/admin/base_controller.rb
Normal file
3
app/controllers/admin/base_controller.rb
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
class Admin::BaseController < ApplicationController
|
||||
before_action :admin_only
|
||||
end
|
||||
40
app/controllers/admin/blacklisted_emails_controller.rb
Normal file
40
app/controllers/admin/blacklisted_emails_controller.rb
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
class Admin::BlacklistedEmailsController < Admin::BaseController
|
||||
|
||||
def index
|
||||
authorize AdminBlacklistedEmail
|
||||
@admin_blacklisted_email = AdminBlacklistedEmail.new
|
||||
if params[:query]
|
||||
@admin_blacklisted_emails = AdminBlacklistedEmail.where(["email LIKE ?", '%' + params[:query] + '%'])
|
||||
@admin_blacklisted_emails = @admin_blacklisted_emails.paginate(page: params[:page], per_page: ArchiveConfig.ITEMS_PER_PAGE)
|
||||
end
|
||||
@page_subtitle = t(".browser_title")
|
||||
end
|
||||
|
||||
def create
|
||||
@admin_blacklisted_email = authorize AdminBlacklistedEmail.new(admin_blacklisted_email_params)
|
||||
@page_subtitle = t(".browser_title")
|
||||
|
||||
if @admin_blacklisted_email.save
|
||||
flash[:notice] = ts("Email address %{email} banned.", email: @admin_blacklisted_email.email)
|
||||
redirect_to admin_blacklisted_emails_path
|
||||
else
|
||||
render action: "index"
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@admin_blacklisted_email = authorize AdminBlacklistedEmail.find(params[:id])
|
||||
@admin_blacklisted_email.destroy
|
||||
|
||||
flash[:notice] = ts("Email address %{email} removed from banned emails list.", email: @admin_blacklisted_email.email)
|
||||
redirect_to admin_blacklisted_emails_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def admin_blacklisted_email_params
|
||||
params.require(:admin_blacklisted_email).permit(
|
||||
:email
|
||||
)
|
||||
end
|
||||
end
|
||||
7
app/controllers/admin/passwords_controller.rb
Normal file
7
app/controllers/admin/passwords_controller.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::PasswordsController < Devise::PasswordsController
|
||||
before_action :user_logout_required
|
||||
skip_before_action :store_location
|
||||
layout "session"
|
||||
end
|
||||
14
app/controllers/admin/sessions_controller.rb
Normal file
14
app/controllers/admin/sessions_controller.rb
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Namespaced Admin class
|
||||
class Admin
|
||||
# Handle admin session authentication
|
||||
class SessionsController < Devise::SessionsController
|
||||
before_action :user_logout_required, except: :destroy
|
||||
skip_before_action :store_location, raise: false
|
||||
|
||||
# GET /admin/logout
|
||||
def confirm_logout
|
||||
# If the user is already logged out, we just redirect to the front page.
|
||||
redirect_to root_path unless admin_signed_in?
|
||||
end
|
||||
end
|
||||
end
|
||||
24
app/controllers/admin/settings_controller.rb
Normal file
24
app/controllers/admin/settings_controller.rb
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
class Admin::SettingsController < Admin::BaseController
|
||||
before_action :load_admin_setting
|
||||
|
||||
def index
|
||||
authorize @admin_setting
|
||||
end
|
||||
|
||||
# PUT /admin_settings/1
|
||||
def update
|
||||
authorize @admin_setting
|
||||
if @admin_setting.update(permitted_attributes(@admin_setting).merge(last_updated: current_admin))
|
||||
flash[:notice] = t(".success")
|
||||
redirect_to admin_settings_path
|
||||
else
|
||||
render :index
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_admin_setting
|
||||
@admin_setting = AdminSetting.first || AdminSetting.create(last_updated_by: Admin.first)
|
||||
end
|
||||
end
|
||||
98
app/controllers/admin/skins_controller.rb
Normal file
98
app/controllers/admin/skins_controller.rb
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
class Admin::SkinsController < Admin::BaseController
|
||||
|
||||
def index
|
||||
authorize Skin
|
||||
@unapproved_skins = Skin.unapproved_skins.sort_by_recent
|
||||
end
|
||||
|
||||
def index_rejected
|
||||
authorize Skin
|
||||
@rejected_skins = Skin.rejected_skins.sort_by_recent
|
||||
end
|
||||
|
||||
def index_approved
|
||||
authorize Skin
|
||||
@approved_skins = Skin.approved_skins.usable.sort_by_recent
|
||||
end
|
||||
|
||||
def update
|
||||
authorize Skin, :index?
|
||||
|
||||
flash[:notice] = []
|
||||
modified_skin_titles = []
|
||||
%w(official rejected cached featured in_chooser).each do |action|
|
||||
skins_to_set = params["make_#{action}"] ? Skin.where(id: params["make_#{action}"].map {|id| id.to_i}) : []
|
||||
skins_to_unset = params["make_un#{action}"] ? Skin.where(id: params["make_un#{action}"].map {|id| id.to_i}) : []
|
||||
skins_to_set.each do |skin|
|
||||
# Silently fail if the user doesn't have permission to update:
|
||||
next unless policy(skin).update?
|
||||
|
||||
case action
|
||||
when "official"
|
||||
skin.update_attribute(:official, true)
|
||||
when "rejected"
|
||||
skin.update_attribute(:rejected, true)
|
||||
when "cached"
|
||||
next unless skin.official? && !skin.is_a?(WorkSkin)
|
||||
skin.cache!
|
||||
when "featured"
|
||||
next unless skin.official? && !skin.is_a?(WorkSkin)
|
||||
skin.cache! unless skin.cached?
|
||||
skin.update_attribute(:featured, true)
|
||||
when "in_chooser"
|
||||
next unless skin.official? && !skin.is_a?(WorkSkin)
|
||||
skin.cache! unless skin.cached?
|
||||
skin.update_attribute(:in_chooser, true)
|
||||
end
|
||||
skin.update_attribute(:admin_note, params[:skin_admin_note]["#{skin.id}"]) if params[:skin_admin_note] && params[:skin_admin_note]["#{skin.id}"]
|
||||
modified_skin_titles << skin.title
|
||||
end
|
||||
|
||||
skins_to_unset.each do |skin|
|
||||
# Silently fail if the user doesn't have permission to update:
|
||||
next unless policy(skin).update?
|
||||
|
||||
case action
|
||||
when "official"
|
||||
skin.clear_cache! # no cache for unofficial skins
|
||||
skin.update_attribute(:official, false)
|
||||
skin.remove_me_from_preferences
|
||||
when "rejected"
|
||||
skin.update_attribute(:rejected, false)
|
||||
when "cached"
|
||||
next unless skin.official? && !skin.is_a?(WorkSkin)
|
||||
skin.clear_cache! if skin.cached?
|
||||
when "featured"
|
||||
next unless skin.official? && !skin.is_a?(WorkSkin)
|
||||
skin.update_attribute(:featured, false)
|
||||
when "in_chooser"
|
||||
next unless skin.official? && !skin.is_a?(WorkSkin)
|
||||
skin.update_attribute(:in_chooser, false)
|
||||
end
|
||||
modified_skin_titles << skin.title
|
||||
end
|
||||
end
|
||||
|
||||
flash[:notice] << ts("The following skins were updated: %{titles}", titles: modified_skin_titles.join(', '))
|
||||
|
||||
# set default
|
||||
if params[:set_default].present? && params[:set_default] != AdminSetting.default_skin&.title
|
||||
authorize Skin, :set_default?
|
||||
|
||||
skin = Skin.find_by(title: params[:set_default], official: true)
|
||||
@admin_setting = AdminSetting.first
|
||||
if skin && @admin_setting
|
||||
@admin_setting.default_skin = skin
|
||||
@admin_setting.last_updated_by = params[:last_updated_by]
|
||||
if @admin_setting.save
|
||||
flash[:notice] << ts("Default skin changed to %{title}", title: skin.title)
|
||||
else
|
||||
flash[:error] = ts("We couldn't save the default skin change.")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
redirect_to admin_skins_path
|
||||
end
|
||||
|
||||
end
|
||||
36
app/controllers/admin/spam_controller.rb
Normal file
36
app/controllers/admin/spam_controller.rb
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
class Admin::SpamController < Admin::BaseController
|
||||
def index
|
||||
authorize ModeratedWork
|
||||
|
||||
conditions = case params[:show]
|
||||
when "reviewed"
|
||||
{ reviewed: true, approved: false }
|
||||
when "approved"
|
||||
{ approved: true }
|
||||
else
|
||||
{ reviewed: false, approved: false }
|
||||
end
|
||||
@works = ModeratedWork.where(conditions)
|
||||
.joins(:work)
|
||||
.includes(:work)
|
||||
.order(:created_at)
|
||||
.page(params[:page])
|
||||
end
|
||||
|
||||
def bulk_update
|
||||
authorize ModeratedWork
|
||||
|
||||
if ModeratedWork.bulk_update(spam_params)
|
||||
flash[:notice] = "Works were successfully updated"
|
||||
else
|
||||
flash[:error] = "Sorry, please try again"
|
||||
end
|
||||
redirect_to admin_spam_index_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def spam_params
|
||||
params.slice(:spam, :ham)
|
||||
end
|
||||
end
|
||||
101
app/controllers/admin/user_creations_controller.rb
Normal file
101
app/controllers/admin/user_creations_controller.rb
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
class Admin::UserCreationsController < Admin::BaseController
|
||||
before_action :get_creation, only: [:hide, :set_spam, :destroy]
|
||||
before_action :can_be_marked_as_spam, only: [:set_spam]
|
||||
|
||||
def get_creation
|
||||
raise "Redshirt: Attempted to constantize invalid class initialize #{params[:creation_type]}" unless %w(Bookmark ExternalWork Series Work).include?(params[:creation_type])
|
||||
@creation_class = params[:creation_type].constantize
|
||||
@creation = @creation_class.find(params[:id])
|
||||
end
|
||||
|
||||
def can_be_marked_as_spam
|
||||
unless @creation_class && @creation_class == Work
|
||||
flash[:error] = ts("You can only mark works as spam currently.")
|
||||
redirect_to polymorphic_path(@creation) and return
|
||||
end
|
||||
end
|
||||
|
||||
# Removes an object from public view
|
||||
def hide
|
||||
authorize @creation
|
||||
@creation.hidden_by_admin = (params[:hidden] == "true")
|
||||
@creation.save(validate: false)
|
||||
action = @creation.hidden_by_admin? ? "hide" : "unhide"
|
||||
AdminActivity.log_action(current_admin, @creation, action: action)
|
||||
flash[:notice] = @creation.hidden_by_admin? ?
|
||||
ts("Item has been hidden.") :
|
||||
ts("Item is no longer hidden.")
|
||||
if @creation_class == ExternalWork || @creation_class == Bookmark
|
||||
redirect_to(request.env["HTTP_REFERER"] || root_path)
|
||||
else
|
||||
redirect_to polymorphic_path(@creation)
|
||||
end
|
||||
end
|
||||
|
||||
def set_spam
|
||||
authorize @creation
|
||||
action = "mark as " + (params[:spam] == "true" ? "spam" : "not spam")
|
||||
AdminActivity.log_action(current_admin, @creation, action: action, summary: @creation.inspect)
|
||||
if params[:spam] == "true"
|
||||
unless @creation.hidden_by_admin
|
||||
@creation.notify_of_hiding_for_spam if @creation_class == Work
|
||||
@creation.hidden_by_admin = true
|
||||
end
|
||||
@creation.mark_as_spam!
|
||||
flash[:notice] = ts("Work was marked as spam and hidden.")
|
||||
else
|
||||
@creation.mark_as_ham!
|
||||
@creation.update_attribute(:hidden_by_admin, false)
|
||||
flash[:notice] = ts("Work was marked not spam and unhidden.")
|
||||
end
|
||||
redirect_to polymorphic_path(@creation)
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @creation
|
||||
AdminActivity.log_action(current_admin, @creation, action: "destroy", summary: @creation.inspect)
|
||||
@creation.destroy
|
||||
flash[:notice] = ts("Item was successfully deleted.")
|
||||
if @creation_class == Bookmark || @creation_class == ExternalWork
|
||||
redirect_to bookmarks_path
|
||||
else
|
||||
redirect_to works_path
|
||||
end
|
||||
end
|
||||
|
||||
def confirm_remove_pseud
|
||||
@work = authorize Work.find(params[:id])
|
||||
|
||||
@orphan_pseuds = @work.orphan_pseuds
|
||||
return unless @orphan_pseuds.empty?
|
||||
|
||||
flash[:error] = t(".must_have_orphan_pseuds")
|
||||
redirect_to work_path(@work) and return
|
||||
end
|
||||
|
||||
def remove_pseud
|
||||
@work = authorize Work.find(params[:id])
|
||||
|
||||
pseuds = params[:pseuds]
|
||||
orphan_account = User.orphan_account
|
||||
if pseuds.blank?
|
||||
pseuds = @work.orphan_pseuds
|
||||
if pseuds.length > 1
|
||||
flash[:error] = t(".must_select_pseud")
|
||||
redirect_to work_path(@work) and return
|
||||
end
|
||||
else
|
||||
pseuds = Pseud.find(pseuds).select { |p| p.user_id == orphan_account.id }
|
||||
end
|
||||
|
||||
orphan_pseud = orphan_account.default_pseud
|
||||
pseuds.each do |pseud|
|
||||
pseud.change_ownership(@work, orphan_pseud)
|
||||
end
|
||||
unless pseuds.empty?
|
||||
AdminActivity.log_action(current_admin, @work, action: "remove orphan_account pseuds")
|
||||
flash[:notice] = t(".success", pseuds: pseuds.map(&:byline).to_sentence, count: pseuds.length)
|
||||
end
|
||||
redirect_to work_path(@work)
|
||||
end
|
||||
end
|
||||
101
app/controllers/admin_posts_controller.rb
Normal file
101
app/controllers/admin_posts_controller.rb
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
class AdminPostsController < Admin::BaseController
|
||||
|
||||
before_action :admin_only, except: [:index, :show]
|
||||
before_action :load_languages, except: [:show, :destroy]
|
||||
|
||||
# GET /admin_posts
|
||||
def index
|
||||
if params[:tag]
|
||||
@tag = AdminPostTag.find_by(id: params[:tag])
|
||||
if @tag
|
||||
@admin_posts = @tag.admin_posts
|
||||
end
|
||||
end
|
||||
@admin_posts ||= AdminPost
|
||||
if params[:language_id].present? && (@language = Language.find_by(short: params[:language_id]))
|
||||
@admin_posts = @admin_posts.where(language_id: @language.id)
|
||||
@tags = AdminPostTag.distinct.joins(:admin_posts).where(admin_posts: { language_id: @language.id }).order(:name)
|
||||
else
|
||||
@admin_posts = @admin_posts.non_translated
|
||||
@tags = AdminPostTag.order(:name)
|
||||
end
|
||||
@admin_posts = @admin_posts.order('created_at DESC').page(params[:page])
|
||||
end
|
||||
|
||||
# GET /admin_posts/1
|
||||
def show
|
||||
admin_posts = AdminPost.non_translated
|
||||
@admin_post = AdminPost.find_by(id: params[:id])
|
||||
unless @admin_post
|
||||
raise ActiveRecord::RecordNotFound, "Couldn't find admin post '#{params[:id]}'"
|
||||
end
|
||||
@admin_posts = admin_posts.order('created_at DESC').limit(8)
|
||||
@previous_admin_post = admin_posts.order('created_at DESC').where('created_at < ?', @admin_post.created_at).first
|
||||
@next_admin_post = admin_posts.order('created_at ASC').where('created_at > ?', @admin_post.created_at).first
|
||||
@page_subtitle = @admin_post.title.html_safe
|
||||
respond_to do |format|
|
||||
format.html # show.html.erb
|
||||
format.js
|
||||
end
|
||||
end
|
||||
|
||||
# GET /admin_posts/new
|
||||
# GET /admin_posts/new.xml
|
||||
def new
|
||||
@admin_post = AdminPost.new
|
||||
authorize @admin_post
|
||||
end
|
||||
|
||||
# GET /admin_posts/1/edit
|
||||
def edit
|
||||
@admin_post = AdminPost.find(params[:id])
|
||||
authorize @admin_post
|
||||
end
|
||||
|
||||
# POST /admin_posts
|
||||
def create
|
||||
@admin_post = AdminPost.new(admin_post_params)
|
||||
authorize @admin_post
|
||||
if @admin_post.save
|
||||
flash[:notice] = ts("Admin Post was successfully created.")
|
||||
redirect_to(@admin_post)
|
||||
else
|
||||
render action: "new"
|
||||
end
|
||||
end
|
||||
|
||||
# PUT /admin_posts/1
|
||||
def update
|
||||
@admin_post = AdminPost.find(params[:id])
|
||||
authorize @admin_post
|
||||
if @admin_post.update(admin_post_params)
|
||||
flash[:notice] = ts("Admin Post was successfully updated.")
|
||||
redirect_to(@admin_post)
|
||||
else
|
||||
render action: "edit"
|
||||
end
|
||||
end
|
||||
|
||||
# DELETE /admin_posts/1
|
||||
def destroy
|
||||
@admin_post = AdminPost.find(params[:id])
|
||||
authorize @admin_post
|
||||
@admin_post.destroy
|
||||
redirect_to(admin_posts_path)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def load_languages
|
||||
@news_languages = Language.where(id: Locale.all.map(&:language_id)).default_order
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def admin_post_params
|
||||
params.require(:admin_post).permit(
|
||||
:admin_id, :title, :content, :translated_post_id, :language_id, :tag_list,
|
||||
:comment_permissions, :moderated_commenting_enabled
|
||||
)
|
||||
end
|
||||
end
|
||||
4
app/controllers/admins_controller.rb
Normal file
4
app/controllers/admins_controller.rb
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
class AdminsController < Admin::BaseController
|
||||
def index
|
||||
end
|
||||
end
|
||||
63
app/controllers/api/v2/base_controller.rb
Normal file
63
app/controllers/api/v2/base_controller.rb
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
# Version the API explicitly in the URL to allow different versions with breaking changes to co-exist if necessary.
|
||||
# The roll over to the next number should happen when code written against the old version will not work
|
||||
# with the new version.
|
||||
module V2
|
||||
class BaseController < ApplicationController
|
||||
skip_before_action :verify_authenticity_token
|
||||
before_action :restrict_access
|
||||
|
||||
# Prevent unhandled errors from returning the normal HTML page
|
||||
rescue_from StandardError, with: :render_standard_error_response
|
||||
|
||||
private
|
||||
|
||||
# Look for a token in the Authorization header only and check that the token isn't currently banned
|
||||
def restrict_access
|
||||
authenticate_or_request_with_http_token do |token, _|
|
||||
ApiKey.exists?(access_token: token) && !ApiKey.find_by(access_token: token).banned?
|
||||
end
|
||||
end
|
||||
|
||||
# Top-level error handling: returns a 403 forbidden if a valid archivist isn't supplied and a 400
|
||||
# if no works are supplied. If there is neither a valid archivist nor valid works, a 400 is returned
|
||||
# with both errors as a message
|
||||
def batch_errors(archivist, import_items)
|
||||
errors = []
|
||||
status = ""
|
||||
|
||||
if archivist&.is_archivist?
|
||||
if import_items.nil? || import_items.empty?
|
||||
status = :empty_request
|
||||
errors << "No items to import were provided."
|
||||
elsif import_items.size >= ArchiveConfig.IMPORT_MAX_WORKS_BY_ARCHIVIST
|
||||
status = :too_many_request
|
||||
errors << "This request contains too many items to import. A maximum of #{ArchiveConfig.IMPORT_MAX_WORKS_BY_ARCHIVIST} " \
|
||||
"items can be imported at one time by an archivist."
|
||||
end
|
||||
else
|
||||
status = :forbidden
|
||||
errors << "The \"archivist\" field must specify the name of an Archive user with archivist privileges."
|
||||
end
|
||||
|
||||
status = :ok if errors.empty?
|
||||
[status, errors]
|
||||
end
|
||||
|
||||
# Return a standard HTTP + Json envelope for all API responses
|
||||
def render_api_response(status, messages, response = {})
|
||||
# It's a bad request unless it's ok or an authorisation error
|
||||
http_status = %i[forbidden ok].include?(status) ? status : :bad_request
|
||||
render status: http_status, json: { status: status, messages: messages }.merge(response)
|
||||
end
|
||||
|
||||
# Return a standard HTTP + Json envelope for errors that drop through other handling
|
||||
def render_standard_error_response(exception)
|
||||
message = "An error occurred in the Archive code: #{exception.message}"
|
||||
render status: :internal_server_error, json: { status: :internal_server_error, messages: [message] }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
229
app/controllers/api/v2/bookmarks_controller.rb
Normal file
229
app/controllers/api/v2/bookmarks_controller.rb
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
class Api::V2::BookmarksController < Api::V2::BaseController
|
||||
respond_to :json
|
||||
|
||||
# POST - search for bookmarks for this archivist
|
||||
def search
|
||||
archivist = User.find_by(login: params[:archivist])
|
||||
bookmarks = params[:bookmarks]
|
||||
# check for top-level errors (not an archivist, no bookmarks...)
|
||||
status, messages = batch_errors(archivist, bookmarks)
|
||||
results = []
|
||||
|
||||
if status == :ok
|
||||
archivist_bookmarks = Bookmark.where(pseud_id: archivist.default_pseud.id)
|
||||
results = bookmarks.map do |bookmark|
|
||||
found_result = {}
|
||||
found_result = check_archivist_bookmark(archivist, bookmark[:url], archivist_bookmarks) unless archivist_bookmarks.empty?
|
||||
|
||||
bookmark_response(
|
||||
status: found_result[:bookmark_status] || :not_found,
|
||||
bookmark_url: found_result[:bookmark_url] || "",
|
||||
bookmark_id: bookmark[:id],
|
||||
original_url: bookmark[:url],
|
||||
messages: found_result[:bookmark_messages] || ["No bookmark found for archivist \"#{archivist.login}\" and URL \"#{bookmark[:url]}\""]
|
||||
)
|
||||
end
|
||||
messages = ["Successfully searched bookmarks for archivist '#{archivist.login}'"]
|
||||
end
|
||||
|
||||
render_api_response(status, messages, bookmarks: results)
|
||||
end
|
||||
|
||||
# POST - create a bookmark for this archivist
|
||||
def create
|
||||
archivist = User.find_by(login: params[:archivist])
|
||||
bookmarks = params[:bookmarks]
|
||||
bookmarks_responses = []
|
||||
@bookmarks = []
|
||||
|
||||
# check for top-level errors (not an archivist, no bookmarks...)
|
||||
status, messages = batch_errors(archivist, bookmarks)
|
||||
|
||||
if status == :ok
|
||||
# Flag error and successes
|
||||
@some_errors = @some_success = false
|
||||
|
||||
# Process the bookmarks
|
||||
archivist_bookmarks = Bookmark.where(pseud_id: archivist.default_pseud_id, bookmarkable_type: "ExternalWork")
|
||||
bookmarks.each do |bookmark|
|
||||
bookmarks_responses << create_bookmark(archivist, bookmark, archivist_bookmarks)
|
||||
end
|
||||
|
||||
# set final response code and message depending on the flags
|
||||
status = :bad_request if bookmarks_responses.any? { |r| [:ok, :created, :found].exclude?(r[:status]) }
|
||||
messages = response_message(messages)
|
||||
end
|
||||
|
||||
render_api_response(status, messages, bookmarks: bookmarks_responses)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Find bookmarks for this archivist
|
||||
def check_archivist_bookmark(archivist, current_bookmark_url, archivist_bookmarks)
|
||||
archivist_bookmarks = archivist_bookmarks
|
||||
.select { |b| b&.bookmarkable.is_a?(ExternalWork) ? b&.bookmarkable&.url == current_bookmark_url : false }
|
||||
.map { |b| [b, b.bookmarkable] }
|
||||
|
||||
if archivist_bookmarks.present?
|
||||
archivist_bookmark, archivist_bookmarkable = archivist_bookmarks.first
|
||||
find_bookmark_response(
|
||||
bookmarkable: archivist_bookmarkable,
|
||||
bookmark_status: :found,
|
||||
bookmark_message: "There is already a bookmark for #{archivist.login} and the URL #{current_bookmark_url}",
|
||||
bookmark_url: bookmark_url(archivist_bookmark)
|
||||
)
|
||||
else
|
||||
find_bookmark_response(
|
||||
bookmarkable: nil,
|
||||
bookmark_status: :not_found,
|
||||
bookmark_message: "There is no bookmark for #{archivist.login} and the URL #{current_bookmark_url}",
|
||||
bookmark_url: ""
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Create a bookmark for this archivist using the Bookmark model
|
||||
def create_bookmark(archivist, params, archivist_bookmarks)
|
||||
found_result = {}
|
||||
bookmark_attributes = bookmark_attributes(archivist, params)
|
||||
external_work_attributes = external_work_attributes(params)
|
||||
bookmark_status, bookmark_messages = external_work_errors(external_work_attributes)
|
||||
bookmark_url = nil
|
||||
original_url = nil
|
||||
bookmarkable = nil
|
||||
@some_errors = true
|
||||
if bookmark_status == :ok
|
||||
begin
|
||||
|
||||
# Check if this bookmark is already imported by filtering the archivist's bookmarks
|
||||
unless archivist_bookmarks.empty?
|
||||
found_result = check_archivist_bookmark(archivist, external_work_attributes[:url], archivist_bookmarks)
|
||||
bookmarkable = found_result[:bookmarkable]
|
||||
end
|
||||
|
||||
if found_result[:bookmark_status] == :found
|
||||
found_result[:bookmark_status] = :already_imported
|
||||
else
|
||||
bookmarkable = ExternalWork.new(external_work_attributes)
|
||||
bookmark = bookmarkable.bookmarks.build(bookmark_attributes)
|
||||
if bookmarkable.save && bookmark.save
|
||||
@bookmarks << bookmark
|
||||
@some_success = true
|
||||
@some_errors = false
|
||||
bookmark_status = :created
|
||||
bookmark_url = bookmark_url(bookmark)
|
||||
bookmark_messages << "Successfully created bookmark for \"" + bookmarkable.title + "\"."
|
||||
else
|
||||
bookmark_status = :unprocessable_entity
|
||||
bookmark_messages << bookmarkable.errors.full_messages + bookmark.errors.full_messages
|
||||
end
|
||||
end
|
||||
rescue StandardError => exception
|
||||
bookmark_status = :unprocessable_entity
|
||||
bookmark_messages << exception.message
|
||||
end
|
||||
original_url = bookmarkable.url if bookmarkable
|
||||
end
|
||||
|
||||
bookmark_response(
|
||||
status: bookmark_status || found_result[:bookmark_status],
|
||||
bookmark_url: bookmark_url || found_result[:bookmark_url],
|
||||
bookmark_id: params[:id],
|
||||
original_url: original_url,
|
||||
messages: bookmark_messages.flatten || found_result[:bookmark_messages]
|
||||
)
|
||||
end
|
||||
|
||||
# Error handling
|
||||
|
||||
# Set messages based on success and error flags
|
||||
def response_message(messages)
|
||||
messages << if @some_success && @some_errors
|
||||
"At least one bookmark was not created. Please check the individual bookmark results for further information."
|
||||
elsif !@some_success && @some_errors
|
||||
"None of the bookmarks were created. Please check the individual bookmark results for further information."
|
||||
else
|
||||
"All bookmarks were successfully created."
|
||||
end
|
||||
messages
|
||||
end
|
||||
|
||||
# Handling for incomplete requests
|
||||
def external_work_errors(external_work_attributes)
|
||||
status = :bad_request
|
||||
errors = []
|
||||
|
||||
# Perform basic validation which the ExternalWork model doesn't do or returns strange messages for
|
||||
# (title is validated correctly in the model and so isn't checked here)
|
||||
url = external_work_attributes[:url]
|
||||
author = external_work_attributes[:author]
|
||||
fandom = external_work_attributes[:fandom_string]
|
||||
|
||||
if url.nil?
|
||||
# Unreachable and AO3 URLs are handled in the ExternalWork model
|
||||
errors << "This bookmark does not contain a URL to an external site. Please specify a valid, non-AO3 URL."
|
||||
end
|
||||
|
||||
if author.nil? || author == ""
|
||||
errors << "This bookmark does not contain an external author name. Please specify an author."
|
||||
end
|
||||
|
||||
if fandom.nil? || fandom == ""
|
||||
errors << "This bookmark does not contain a fandom. Please specify a fandom."
|
||||
end
|
||||
|
||||
status = :ok if errors.empty?
|
||||
[status, errors]
|
||||
end
|
||||
|
||||
# Request and response hashes
|
||||
|
||||
# Map JSON request to attributes for bookmark
|
||||
def bookmark_attributes(archivist, params)
|
||||
{
|
||||
pseud_id: archivist.default_pseud_id,
|
||||
bookmarker_notes: params[:bookmarker_notes],
|
||||
tag_string: params[:tag_string] || "",
|
||||
collection_names: params[:collection_names],
|
||||
private: params[:private].blank? ? false : params[:private],
|
||||
rec: params[:recommendation].blank? ? false : params[:recommendation]
|
||||
}
|
||||
end
|
||||
|
||||
# Map JSON request to attributes for external work
|
||||
def external_work_attributes(params)
|
||||
{
|
||||
url: params[:url],
|
||||
author: params[:author],
|
||||
title: params[:title],
|
||||
summary: params[:summary],
|
||||
fandom_string: params[:fandom_string] || "",
|
||||
rating_string: params[:rating_string] || "",
|
||||
category_string: params[:category_string] ? params[:category_string].to_s.split(",") : [], # category is actually an array on bookmarks
|
||||
relationship_string: params[:relationship_string] || "",
|
||||
character_string: params[:character_string] || ""
|
||||
}
|
||||
end
|
||||
|
||||
def bookmark_response(status:, bookmark_url:, bookmark_id:, original_url:, messages:)
|
||||
messages = [messages] unless messages.respond_to?('each')
|
||||
{
|
||||
status: status,
|
||||
archive_url: bookmark_url,
|
||||
original_id: bookmark_id,
|
||||
original_url: original_url,
|
||||
messages: messages
|
||||
}
|
||||
end
|
||||
|
||||
def find_bookmark_response(bookmarkable:, bookmark_status:, bookmark_message:, bookmark_url:)
|
||||
bookmark_status = :not_found unless [:found, :not_found].include?(bookmark_status)
|
||||
{
|
||||
bookmarkable: bookmarkable,
|
||||
bookmark_status: bookmark_status,
|
||||
bookmark_messages: bookmark_message,
|
||||
bookmark_url: bookmark_url
|
||||
}
|
||||
end
|
||||
end
|
||||
225
app/controllers/api/v2/works_controller.rb
Normal file
225
app/controllers/api/v2/works_controller.rb
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
class Api::V2::WorksController < Api::V2::BaseController
|
||||
respond_to :json
|
||||
|
||||
# POST - search for works based on imported url
|
||||
def search
|
||||
works = params[:works]
|
||||
original_urls = works.map { |w| w[:original_urls] }.flatten
|
||||
|
||||
results = []
|
||||
messages = []
|
||||
if original_urls.nil? || original_urls.blank? || original_urls.empty?
|
||||
status = :empty_request
|
||||
messages << "Please provide a list of URLs to find."
|
||||
elsif original_urls.size >= ArchiveConfig.IMPORT_MAX_CHAPTERS
|
||||
status = :too_many_request
|
||||
messages << "Please provide no more than #{ArchiveConfig.IMPORT_MAX_CHAPTERS} URLs to find."
|
||||
else
|
||||
status = :ok
|
||||
results = find_existing_works(original_urls)
|
||||
messages << "Successfully searched all provided URLs."
|
||||
end
|
||||
render_api_response(status, messages, works: results)
|
||||
end
|
||||
|
||||
# POST - create a work and invite authors to claim
|
||||
def create
|
||||
archivist = User.find_by(login: params[:archivist])
|
||||
external_works = params[:items] || params[:works]
|
||||
works_responses = []
|
||||
|
||||
# check for top-level errors (not an archivist, no works...)
|
||||
status, messages = batch_errors(archivist, external_works)
|
||||
|
||||
if status == :ok
|
||||
# Process the works, updating the flags
|
||||
works_responses = external_works.map { |external_work| import_work(archivist, external_work.merge(params.permit!)) }
|
||||
success_responses, error_responses = works_responses.partition { |r| [:ok, :created, :found].include?(r[:status]) }
|
||||
|
||||
# Send claim notification emails for successful works
|
||||
if params[:send_claim_emails] && success_responses.present?
|
||||
notified_authors = notify_and_return_authors(success_responses.map { |r| r[:work] }, archivist)
|
||||
messages << "Claim emails sent to #{notified_authors.to_sentence}."
|
||||
end
|
||||
|
||||
# set final status code and message depending on the flags
|
||||
status = :bad_request if error_responses.present?
|
||||
messages = response_message(messages, success_responses.present?, error_responses.present?)
|
||||
end
|
||||
render_api_response(status, messages, works: works_responses.map { |r| r.except!(:work) })
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Set messages based on success and error flags
|
||||
def response_message(messages, any_success, any_errors)
|
||||
messages << if any_success && any_errors
|
||||
"At least one work was not imported. Please check individual work responses for further information."
|
||||
elsif !any_success && any_errors
|
||||
"None of the works were imported. Please check individual work responses for further information."
|
||||
else
|
||||
"All works were successfully imported."
|
||||
end
|
||||
messages
|
||||
end
|
||||
|
||||
# Work-level error handling for requests that are incomplete or too large
|
||||
def work_errors(work)
|
||||
status = :bad_request
|
||||
errors = []
|
||||
urls = work[:chapter_urls]
|
||||
if urls.nil? || urls.empty?
|
||||
status = :empty_request
|
||||
errors << "This work doesn't contain any chapter URLs. " +
|
||||
"Works can only be imported from publicly-accessible chapter URLs."
|
||||
elsif urls.length >= ArchiveConfig.IMPORT_MAX_CHAPTERS
|
||||
status = :too_many_requests
|
||||
errors << "This work contains too many chapter URLs. A maximum of #{ArchiveConfig.IMPORT_MAX_CHAPTERS} " \
|
||||
"chapters can be imported per work."
|
||||
end
|
||||
status = :ok if errors.empty?
|
||||
[status, errors]
|
||||
end
|
||||
|
||||
# Search for works imported from the provided URLs
|
||||
def find_existing_works(original_urls)
|
||||
results = []
|
||||
messages = []
|
||||
original_urls.each do |original|
|
||||
original_id = ""
|
||||
if original.class == String
|
||||
original_url = original
|
||||
else
|
||||
original_id = original[:id]
|
||||
original_url = original[:url]
|
||||
end
|
||||
|
||||
# Search for works - there may be duplicates
|
||||
search_results = find_work_by_import_url(original_url)
|
||||
if search_results[:works].empty?
|
||||
results << { status: :not_found,
|
||||
original_id: original_id,
|
||||
original_url: original_url,
|
||||
messages: [search_results[:message]] }
|
||||
else
|
||||
work_results = search_results[:works].map do |work|
|
||||
archive_url = work_url(work)
|
||||
message = "Work \"#{work.title}\", created on #{work.created_at.to_date.to_fs(:iso_date)} was found at \"#{archive_url}\"."
|
||||
messages << message
|
||||
{ archive_url: archive_url,
|
||||
created: work.created_at,
|
||||
message: message }
|
||||
end
|
||||
results << { status: :found,
|
||||
original_id: original_id,
|
||||
original_url: original_url,
|
||||
search_results: work_results,
|
||||
messages: messages
|
||||
}
|
||||
end
|
||||
end
|
||||
results
|
||||
end
|
||||
|
||||
def find_work_by_import_url(original_url)
|
||||
works = nil
|
||||
if original_url.blank?
|
||||
message = "Please provide the original URL for the work."
|
||||
else
|
||||
# We know the url will be identical no need for a call to find_by_url
|
||||
works = Work.where(imported_from_url: original_url)
|
||||
message = if works.empty?
|
||||
"No work has been imported from \"" + original_url + "\"."
|
||||
else
|
||||
"Found #{works.size} work(s) imported from \"" + original_url + "\"."
|
||||
end
|
||||
end
|
||||
{
|
||||
original_url: original_url,
|
||||
works: works,
|
||||
message: message
|
||||
}
|
||||
end
|
||||
|
||||
# Use the story parser to scrape works from the chapter URLs
|
||||
def import_work(archivist, external_work)
|
||||
work_status, work_messages = work_errors(external_work)
|
||||
work_url = ""
|
||||
original_url = ""
|
||||
if work_status == :ok
|
||||
urls = external_work[:chapter_urls]
|
||||
original_url = urls.first
|
||||
storyparser = StoryParser.new
|
||||
options = story_parser_options(archivist, external_work)
|
||||
begin
|
||||
response = storyparser.import_chapters_into_story(urls, options)
|
||||
work = response[:work]
|
||||
work_status = response[:status]
|
||||
|
||||
work.save if work_status == :created
|
||||
work_url = work_url(work)
|
||||
work_messages << response[:message]
|
||||
rescue => exception
|
||||
Rails.logger.error("------- API v2: error: #{exception.inspect}")
|
||||
work_status = :unprocessable_entity
|
||||
work_messages << "Unable to import this work."
|
||||
work_messages << exception.message
|
||||
end
|
||||
end
|
||||
|
||||
{
|
||||
status: work_status,
|
||||
archive_url: work_url,
|
||||
original_id: external_work[:id],
|
||||
original_url: original_url,
|
||||
messages: work_messages,
|
||||
work: work
|
||||
}
|
||||
end
|
||||
|
||||
# Send invitations to external authors for a given set of works
|
||||
def notify_and_return_authors(success_works, archivist)
|
||||
notified_authors = []
|
||||
external_authors = success_works.map(&:external_authors).flatten.uniq
|
||||
external_authors&.each do |external_author|
|
||||
external_author.find_or_invite(archivist)
|
||||
# One of the external author pseuds is its email address so filter that one out
|
||||
author_names = external_author.names.reject { |a| a.name == external_author.email }.map(&:name).flatten.join(", ")
|
||||
notified_authors << author_names
|
||||
end
|
||||
notified_authors
|
||||
end
|
||||
|
||||
# Request and response hashes
|
||||
|
||||
# Create options map for StoryParser
|
||||
def story_parser_options(archivist, work_params)
|
||||
{
|
||||
archivist: archivist,
|
||||
import_multiple: "chapters",
|
||||
importing_for_others: true,
|
||||
do_not_set_current_author: true,
|
||||
post_without_preview: work_params[:post_without_preview].blank? ? true : work_params[:post_without_preview],
|
||||
restricted: work_params[:restricted],
|
||||
override_tags: work_params[:override_tags].nil? ? true : work_params[:override_tags],
|
||||
detect_tags: work_params[:detect_tags].nil? ? true : work_params[:detect_tags],
|
||||
collection_names: work_params[:collection_names],
|
||||
title: work_params[:title],
|
||||
fandom: work_params[:fandoms],
|
||||
archive_warning: work_params[:warnings],
|
||||
character: work_params[:characters],
|
||||
rating: work_params[:rating],
|
||||
relationship: work_params[:relationships],
|
||||
category: work_params[:categories],
|
||||
freeform: work_params[:additional_tags],
|
||||
language_id: Language.find_by(short: work_params[:language_code])&.id,
|
||||
summary: work_params[:summary],
|
||||
notes: work_params[:notes],
|
||||
encoding: work_params[:encoding],
|
||||
external_author_name: work_params[:external_author_name],
|
||||
external_author_email: work_params[:external_author_email],
|
||||
external_coauthor_name: work_params[:external_coauthor_name],
|
||||
external_coauthor_email: work_params[:external_coauthor_email]
|
||||
}
|
||||
end
|
||||
end
|
||||
564
app/controllers/application_controller.rb
Normal file
564
app/controllers/application_controller.rb
Normal file
|
|
@ -0,0 +1,564 @@
|
|||
class ApplicationController < ActionController::Base
|
||||
include ActiveStorage::SetCurrent
|
||||
include Pundit::Authorization
|
||||
protect_from_forgery with: :exception, prepend: true
|
||||
rescue_from ActionController::InvalidAuthenticityToken, with: :display_auth_error
|
||||
|
||||
rescue_from Pundit::NotAuthorizedError do
|
||||
admin_only_access_denied
|
||||
end
|
||||
|
||||
# sets admin user for pundit policies
|
||||
def pundit_user
|
||||
current_admin
|
||||
end
|
||||
|
||||
rescue_from ActionController::UnknownFormat, with: :raise_not_found
|
||||
rescue_from Elastic::Transport::Transport::Errors::ServiceUnavailable do
|
||||
# Non-standard code to distinguish Elasticsearch errors from standard 503s.
|
||||
# We can't use 444 because nginx will close connections without sending
|
||||
# response headers.
|
||||
head 445
|
||||
end
|
||||
|
||||
def raise_not_found
|
||||
redirect_to '/404'
|
||||
end
|
||||
|
||||
rescue_from Rack::Timeout::RequestTimeoutException, with: :raise_timeout
|
||||
|
||||
def raise_timeout
|
||||
redirect_to timeout_error_path
|
||||
end
|
||||
|
||||
helper :all # include all helpers, all the time
|
||||
|
||||
include HtmlCleaner
|
||||
before_action :sanitize_ac_params
|
||||
|
||||
# sanitize_params works best with a hash, and will convert
|
||||
# ActionController::Parameters to a hash in order to work with them anyway.
|
||||
#
|
||||
# Controllers need to deal with ActionController::Parameters, not hashes.
|
||||
# These methods hand the params as a hash to sanitize_params, and then
|
||||
# transforms the results back into ActionController::Parameters.
|
||||
def sanitize_ac_params
|
||||
sanitize_params(params.to_unsafe_h).each do |key, value|
|
||||
params[key] = transform_sanitized_hash_to_ac_params(key, value)
|
||||
end
|
||||
end
|
||||
|
||||
include Pagy::Backend
|
||||
def pagy(collection, **vars)
|
||||
pagy_overflow_handler do
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def pagy_query_result(query_result, **vars)
|
||||
pagy_overflow_handler do
|
||||
Pagy.new(
|
||||
count: query_result.total_entries,
|
||||
page: query_result.current_page,
|
||||
limit: query_result.per_page,
|
||||
**vars
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def pagy_overflow_handler(*)
|
||||
yield
|
||||
rescue Pagy::OverflowError
|
||||
nil
|
||||
end
|
||||
|
||||
def display_auth_error
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
redirect_to auth_error_path
|
||||
end
|
||||
format.any(:js, :json) do
|
||||
render json: {
|
||||
errors: {
|
||||
auth_error: "Your current session has expired and we can't authenticate your request. Try logging in again, refreshing the page, or <a href='http://kb.iu.edu/data/ahic.html'>clearing your cache</a> if you continue to experience problems.".html_safe
|
||||
}
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def transform_sanitized_hash_to_ac_params(key, value)
|
||||
if value.is_a?(Hash)
|
||||
ActionController::Parameters.new(value)
|
||||
elsif value.is_a?(Array)
|
||||
value.map.with_index do |val, index|
|
||||
value[index] = transform_sanitized_hash_to_ac_params(key, val)
|
||||
end
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
helper_method :current_user
|
||||
helper_method :current_admin
|
||||
helper_method :logged_in?
|
||||
helper_method :logged_in_as_admin?
|
||||
helper_method :guest?
|
||||
|
||||
# Title helpers
|
||||
helper_method :process_title
|
||||
|
||||
# clear out the flash-being-set
|
||||
before_action :clear_flash_cookie
|
||||
def clear_flash_cookie
|
||||
cookies.delete(:flash_is_set)
|
||||
end
|
||||
|
||||
after_action :check_for_flash
|
||||
def check_for_flash
|
||||
cookies[:flash_is_set] = 1 unless flash.empty?
|
||||
end
|
||||
|
||||
# Override redirect_to so that if it's called in a before_action hook, it'll
|
||||
# still call check_for_flash after it runs.
|
||||
def redirect_to(*args, **kwargs)
|
||||
super.tap do
|
||||
check_for_flash
|
||||
end
|
||||
end
|
||||
|
||||
after_action :ensure_admin_credentials
|
||||
def ensure_admin_credentials
|
||||
if logged_in_as_admin?
|
||||
# if we are logged in as an admin and we don't have the admin_credentials
|
||||
# set then set that cookie
|
||||
cookies[:admin_credentials] = { value: 1, expires: 1.year.from_now } unless cookies[:admin_credentials]
|
||||
else
|
||||
# if we are NOT logged in as an admin and we have the admin_credentials
|
||||
# set then delete that cookie
|
||||
cookies.delete :admin_credentials unless cookies[:admin_credentials].nil?
|
||||
end
|
||||
end
|
||||
|
||||
# If there is no user_credentials cookie and the user appears to be logged in,
|
||||
# redirect to the lost cookie page. Needs to be before the code to fix
|
||||
# the user_credentials cookie or it won't fire.
|
||||
before_action :logout_if_not_user_credentials
|
||||
def logout_if_not_user_credentials
|
||||
if logged_in? && cookies[:user_credentials].nil? && controller_name != "sessions"
|
||||
logger.error "Forcing logout"
|
||||
sign_out
|
||||
redirect_to '/lost_cookie' and return
|
||||
end
|
||||
end
|
||||
|
||||
# The user_credentials cookie is used by nginx to figure out whether or not
|
||||
# to cache the page, so we want to make sure that it's set when the user is
|
||||
# logged in, and cleared when the user is logged out.
|
||||
after_action :ensure_user_credentials
|
||||
def ensure_user_credentials
|
||||
if logged_in?
|
||||
cookies[:user_credentials] = { value: 1, expires: 1.year.from_now } unless cookies[:user_credentials]
|
||||
else
|
||||
cookies.delete :user_credentials unless cookies[:user_credentials].nil?
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def logged_in?
|
||||
user_signed_in?
|
||||
end
|
||||
|
||||
def logged_in_as_admin?
|
||||
admin_signed_in?
|
||||
end
|
||||
|
||||
def guest?
|
||||
!(logged_in? || logged_in_as_admin?)
|
||||
end
|
||||
|
||||
def process_title(string)
|
||||
string = string.humanize.titleize
|
||||
|
||||
string = string.sub("Faq", "FAQ")
|
||||
string = string.sub("Tos", "TOS")
|
||||
string = string.sub("Dmca", "DMCA")
|
||||
return string
|
||||
end
|
||||
|
||||
public
|
||||
|
||||
before_action :load_admin_banner
|
||||
def load_admin_banner
|
||||
if Rails.env.development?
|
||||
@admin_banner = AdminBanner.where(active: true).last
|
||||
else
|
||||
# http://stackoverflow.com/questions/12891790/will-returning-a-nil-value-from-a-block-passed-to-rails-cache-fetch-clear-it
|
||||
# Basically we need to store a nil separately.
|
||||
@admin_banner = Rails.cache.fetch("admin_banner") do
|
||||
banner = AdminBanner.where(active: true).last
|
||||
banner.nil? ? "" : banner
|
||||
end
|
||||
@admin_banner = nil if @admin_banner == ""
|
||||
end
|
||||
end
|
||||
|
||||
before_action :load_tos_popup
|
||||
def load_tos_popup
|
||||
# Integers only, YYYY-MM-DD format of date Board approved TOS
|
||||
@current_tos_version = 2024_11_19 # rubocop:disable Style/NumericLiterals
|
||||
end
|
||||
|
||||
# store previous page in session to make redirecting back possible
|
||||
# if already redirected once, don't redirect again.
|
||||
before_action :store_location
|
||||
def store_location
|
||||
if session[:return_to] == "redirected"
|
||||
session.delete(:return_to)
|
||||
elsif request.fullpath.length > 200
|
||||
# Sessions are stored in cookies, which has a 4KB size limit.
|
||||
# Don't store paths that are too long (e.g. filters with lots of exclusions).
|
||||
# Also remove the previous stored path.
|
||||
session.delete(:return_to)
|
||||
else
|
||||
session[:return_to] = request.fullpath
|
||||
end
|
||||
end
|
||||
|
||||
# Redirect to the URI stored by the most recent store_location call or
|
||||
# to the passed default.
|
||||
def redirect_back_or_default(default = root_path)
|
||||
back = session[:return_to]
|
||||
session.delete(:return_to)
|
||||
if back
|
||||
session[:return_to] = "redirected"
|
||||
redirect_to(back) and return
|
||||
else
|
||||
redirect_to(default) and return
|
||||
end
|
||||
end
|
||||
|
||||
def after_sign_in_path_for(resource)
|
||||
if resource.is_a?(Admin)
|
||||
admins_path
|
||||
else
|
||||
back = session[:return_to]
|
||||
session.delete(:return_to)
|
||||
|
||||
back || user_path(current_user)
|
||||
end
|
||||
end
|
||||
|
||||
def authenticate_admin!
|
||||
if admin_signed_in?
|
||||
super
|
||||
else
|
||||
redirect_to root_path, notice: "I'm sorry, only an admin can look at that area"
|
||||
## if you want render 404 page
|
||||
## render file: File.join(Rails.root, 'public/404'), formats: [:html], status: 404, layout: false
|
||||
end
|
||||
end
|
||||
|
||||
# Filter method - keeps users out of admin areas
|
||||
def admin_only
|
||||
authenticate_admin! || admin_only_access_denied
|
||||
end
|
||||
|
||||
# Filter method to prevent admin users from accessing certain actions
|
||||
def users_only
|
||||
logged_in? || access_denied
|
||||
end
|
||||
|
||||
# Filter method - requires user to have opendoors privs
|
||||
def opendoors_only
|
||||
(logged_in? && permit?("opendoors")) || access_denied
|
||||
end
|
||||
|
||||
# Redirect as appropriate when an access request fails.
|
||||
#
|
||||
# The default action is to redirect to the login screen.
|
||||
#
|
||||
# Override this method in your controllers if you want to have special
|
||||
# behavior in case the user is not authorized
|
||||
# to access the requested action. For example, a popup window might
|
||||
# simply close itself.
|
||||
def access_denied(options ={})
|
||||
store_location
|
||||
if logged_in?
|
||||
destination = options[:redirect].blank? ? user_path(current_user) : options[:redirect]
|
||||
# i18n-tasks-use t('users.reconfirm_email.access_denied.logged_in')
|
||||
flash[:error] = t(".access_denied.logged_in", default: t("application.access_denied.access_denied.logged_in")) # rubocop:disable I18n/DefaultTranslation
|
||||
redirect_to destination
|
||||
else
|
||||
destination = options[:redirect].blank? ? new_user_session_path : options[:redirect]
|
||||
flash[:error] = ts "Sorry, you don't have permission to access the page you were trying to reach. Please log in."
|
||||
redirect_to destination
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
def admin_only_access_denied
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
flash[:error] = t("admin.access.page_access_denied")
|
||||
redirect_to root_path
|
||||
end
|
||||
format.json do
|
||||
errors = [t("admin.access.action_access_denied")]
|
||||
render json: { errors: errors }, status: :forbidden
|
||||
end
|
||||
format.js do
|
||||
flash[:error] = t("admin.access.page_access_denied")
|
||||
render js: "window.location.href = '#{root_path}';"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Filter method - prevents users from logging in as admin
|
||||
def user_logout_required
|
||||
if logged_in?
|
||||
flash[:notice] = 'Please log out of your user account first!'
|
||||
redirect_to root_path
|
||||
end
|
||||
end
|
||||
|
||||
# Prevents admin from logging in as users
|
||||
def admin_logout_required
|
||||
if logged_in_as_admin?
|
||||
flash[:notice] = 'Please log out of your admin account first!'
|
||||
redirect_to root_path
|
||||
end
|
||||
end
|
||||
|
||||
# Hide admin banner via cookies
|
||||
before_action :hide_banner
|
||||
def hide_banner
|
||||
if params[:hide_banner]
|
||||
session[:hide_banner] = true
|
||||
end
|
||||
end
|
||||
|
||||
# Store the current user as a class variable in the User class,
|
||||
# so other models can access it with "User.current_user"
|
||||
around_action :set_current_user
|
||||
def set_current_user
|
||||
User.current_user = logged_in_as_admin? ? current_admin : current_user
|
||||
@current_user = current_user
|
||||
unless current_user.nil?
|
||||
@current_user_subscriptions_count, @current_user_visible_work_count, @current_user_bookmarks_count, @current_user_owned_collections_count, @current_user_challenge_signups_count, @current_user_offer_assignments, @current_user_unposted_works_size=
|
||||
Rails.cache.fetch("user_menu_counts_#{current_user.id}",
|
||||
expires_in: 2.hours,
|
||||
race_condition_ttl: 5) { "#{current_user.subscriptions.count}, #{current_user.visible_work_count}, #{current_user.bookmarks.count}, #{current_user.owned_collections.count}, #{current_user.challenge_signups.count}, #{current_user.offer_assignments.undefaulted.count + current_user.pinch_hit_assignments.undefaulted.count}, #{current_user.unposted_works.size}" }.split(",").map(&:to_i)
|
||||
end
|
||||
|
||||
yield
|
||||
|
||||
User.current_user = nil
|
||||
@current_user = nil
|
||||
end
|
||||
|
||||
def load_collection
|
||||
@collection = Collection.find_by(name: params[:collection_id]) if params[:collection_id]
|
||||
end
|
||||
|
||||
def collection_maintainers_only
|
||||
logged_in? && @collection && @collection.user_is_maintainer?(current_user) || access_denied
|
||||
end
|
||||
|
||||
def collection_owners_only
|
||||
logged_in? && @collection && @collection.user_is_owner?(current_user) || access_denied
|
||||
end
|
||||
|
||||
def not_allowed(fallback=nil)
|
||||
flash[:error] = ts("Sorry, you're not allowed to do that.")
|
||||
redirect_to (fallback || root_path) rescue redirect_to '/'
|
||||
end
|
||||
|
||||
|
||||
def get_page_title(fandom, author, title, options = {})
|
||||
# truncate any piece that is over 15 chars long to the nearest word
|
||||
if options[:truncate]
|
||||
fandom = fandom.gsub(/^(.{15}[\w.]*)(.*)/) {$2.empty? ? $1 : $1 + '...'}
|
||||
author = author.gsub(/^(.{15}[\w.]*)(.*)/) {$2.empty? ? $1 : $1 + '...'}
|
||||
title = title.gsub(/^(.{15}[\w.]*)(.*)/) {$2.empty? ? $1 : $1 + '...'}
|
||||
end
|
||||
|
||||
@page_title = ""
|
||||
if logged_in? && !current_user.preference.try(:work_title_format).blank?
|
||||
@page_title = current_user.preference.work_title_format.dup
|
||||
@page_title.gsub!(/FANDOM/, fandom)
|
||||
@page_title.gsub!(/AUTHOR/, author)
|
||||
@page_title.gsub!(/TITLE/, title)
|
||||
else
|
||||
@page_title = title + " - " + author + " - " + fandom
|
||||
end
|
||||
|
||||
@page_title += " [#{ArchiveConfig.APP_NAME}]" unless options[:omit_archive_name]
|
||||
@page_title.html_safe
|
||||
end
|
||||
|
||||
public
|
||||
|
||||
#### -- AUTHORIZATION -- ####
|
||||
|
||||
# It is just much easier to do this here than to try to stuff variable values into a constant in environment.rb
|
||||
|
||||
def is_registered_user?
|
||||
logged_in? || logged_in_as_admin?
|
||||
end
|
||||
|
||||
def is_admin?
|
||||
logged_in_as_admin?
|
||||
end
|
||||
|
||||
def see_adult?
|
||||
params[:anchor] = "comments" if (params[:show_comments] && params[:anchor].blank?)
|
||||
return true if cookies[:view_adult] || logged_in_as_admin?
|
||||
return false unless current_user
|
||||
return true if current_user.is_author_of?(@work)
|
||||
return true if current_user.preference && current_user.preference.adult
|
||||
return false
|
||||
end
|
||||
|
||||
def use_caching?
|
||||
%w(staging production test).include?(Rails.env) && AdminSetting.current.enable_test_caching?
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Prevents banned and suspended users from adding/editing content
|
||||
def check_user_status
|
||||
if current_user.is_a?(User) && (current_user.suspended? || current_user.banned?)
|
||||
if current_user.suspended?
|
||||
suspension_end = current_user.suspended_until
|
||||
|
||||
# Unban threshold is 6:51pm, 12 hours after the unsuspend_users rake task located in schedule.rb is run at 6:51am
|
||||
unban_theshold = DateTime.new(suspension_end.year, suspension_end.month, suspension_end.day, 18, 51, 0, "+00:00")
|
||||
|
||||
# If the stated suspension end date is after the unban threshold we need to advance a day
|
||||
suspension_end = suspension_end.next_day(1) if suspension_end > unban_theshold
|
||||
localized_suspension_end = view_context.date_in_zone(suspension_end)
|
||||
flash[:error] = t("users.status.suspension_notice_html", suspended_until: localized_suspension_end, contact_abuse_link: view_context.link_to(t("users.contact_abuse"), new_abuse_report_path))
|
||||
|
||||
else
|
||||
flash[:error] = t("users.status.ban_notice_html", contact_abuse_link: view_context.link_to(t("users.contact_abuse"), new_abuse_report_path))
|
||||
end
|
||||
redirect_to current_user
|
||||
end
|
||||
end
|
||||
|
||||
# Prevents temporarily suspended users from deleting content
|
||||
def check_user_not_suspended
|
||||
return unless current_user.is_a?(User) && current_user.suspended?
|
||||
|
||||
suspension_end = current_user.suspended_until
|
||||
|
||||
# Unban threshold is 6:51pm, 12 hours after the unsuspend_users rake task located in schedule.rb is run at 6:51am
|
||||
unban_theshold = DateTime.new(suspension_end.year, suspension_end.month, suspension_end.day, 18, 51, 0, "+00:00")
|
||||
|
||||
# If the stated suspension end date is after the unban threshold we need to advance a day
|
||||
suspension_end = suspension_end.next_day(1) if suspension_end > unban_theshold
|
||||
localized_suspension_end = view_context.date_in_zone(suspension_end)
|
||||
|
||||
flash[:error] = t("users.status.suspension_notice_html", suspended_until: localized_suspension_end, contact_abuse_link: view_context.link_to(t("users.contact_abuse"), new_abuse_report_path))
|
||||
|
||||
redirect_to current_user
|
||||
end
|
||||
|
||||
# Does the current user own a specific object?
|
||||
def current_user_owns?(item)
|
||||
!item.nil? && current_user.is_a?(User) && (item.is_a?(User) ? current_user == item : current_user.is_author_of?(item))
|
||||
end
|
||||
|
||||
# Make sure a specific object belongs to the current user and that they have permission
|
||||
# to view, edit or delete it
|
||||
def check_ownership
|
||||
access_denied(redirect: @check_ownership_of) unless current_user_owns?(@check_ownership_of)
|
||||
end
|
||||
def check_ownership_or_admin
|
||||
return true if logged_in_as_admin?
|
||||
access_denied(redirect: @check_ownership_of) unless current_user_owns?(@check_ownership_of)
|
||||
end
|
||||
|
||||
# Make sure the user is allowed to see a specific page
|
||||
# includes a special case for restricted works and series, since we want to encourage people to sign up to read them
|
||||
def check_visibility
|
||||
if @check_visibility_of.respond_to?(:restricted) && @check_visibility_of.restricted && User.current_user.nil?
|
||||
redirect_to new_user_session_path(restricted: true)
|
||||
elsif @check_visibility_of.is_a? Skin
|
||||
access_denied unless logged_in_as_admin? || current_user_owns?(@check_visibility_of) || @check_visibility_of.official?
|
||||
else
|
||||
is_hidden = (@check_visibility_of.respond_to?(:visible) && !@check_visibility_of.visible) ||
|
||||
(@check_visibility_of.respond_to?(:visible?) && !@check_visibility_of.visible?) ||
|
||||
(@check_visibility_of.respond_to?(:hidden_by_admin?) && @check_visibility_of.hidden_by_admin?)
|
||||
can_view_hidden = logged_in_as_admin? || current_user_owns?(@check_visibility_of)
|
||||
access_denied if (is_hidden && !can_view_hidden)
|
||||
end
|
||||
end
|
||||
|
||||
# Make sure user is allowed to access tag wrangling pages
|
||||
def check_permission_to_wrangle
|
||||
if AdminSetting.current.tag_wrangling_off? && !logged_in_as_admin?
|
||||
flash[:error] = "Wrangling is disabled at the moment. Please check back later."
|
||||
redirect_to root_path
|
||||
else
|
||||
logged_in_as_admin? || permit?("tag_wrangler") || access_denied
|
||||
end
|
||||
end
|
||||
|
||||
# Checks if user is allowed to see related page if parent item is hidden or in unrevealed collection
|
||||
# Checks if user is logged in if parent item is restricted
|
||||
def check_visibility_for(parent)
|
||||
return if logged_in_as_admin? || current_user_owns?(parent) # Admins and the owner can see all related pages
|
||||
|
||||
access_denied(redirect: root_path) if parent.try(:hidden_by_admin) || parent.try(:in_unrevealed_collection) || (parent.respond_to?(:visible?) && !parent.visible?)
|
||||
end
|
||||
|
||||
public
|
||||
|
||||
def valid_sort_column(param, model='work')
|
||||
allowed = []
|
||||
if model.to_s.downcase == 'work'
|
||||
allowed = %w(author title date created_at word_count hit_count)
|
||||
elsif model.to_s.downcase == 'tag'
|
||||
allowed = %w[name created_at taggings_count_cache uses]
|
||||
elsif model.to_s.downcase == 'collection'
|
||||
allowed = %w(collections.title collections.created_at)
|
||||
elsif model.to_s.downcase == 'prompt'
|
||||
allowed = %w(fandom created_at prompter)
|
||||
elsif model.to_s.downcase == 'claim'
|
||||
allowed = %w(created_at claimer)
|
||||
end
|
||||
!param.blank? && allowed.include?(param.to_s.downcase)
|
||||
end
|
||||
|
||||
def set_sort_order
|
||||
# sorting
|
||||
@sort_column = (valid_sort_column(params[:sort_column],"prompt") ? params[:sort_column] : 'id')
|
||||
@sort_direction = (valid_sort_direction(params[:sort_direction]) ? params[:sort_direction] : 'DESC')
|
||||
if !params[:sort_direction].blank? && !valid_sort_direction(params[:sort_direction])
|
||||
params[:sort_direction] = 'DESC'
|
||||
end
|
||||
@sort_order = @sort_column + " " + @sort_direction
|
||||
end
|
||||
|
||||
def valid_sort_direction(param)
|
||||
!param.blank? && %w(asc desc).include?(param.to_s.downcase)
|
||||
end
|
||||
|
||||
def flash_search_warnings(result)
|
||||
if result.respond_to?(:error) && result.error
|
||||
flash.now[:error] = result.error
|
||||
elsif result.respond_to?(:notice) && result.notice
|
||||
flash.now[:notice] = result.notice
|
||||
end
|
||||
end
|
||||
|
||||
# Don't get unnecessary data for json requests
|
||||
|
||||
skip_before_action :load_admin_banner,
|
||||
:store_location,
|
||||
if: proc { %w(js json).include?(request.format) }
|
||||
|
||||
end
|
||||
195
app/controllers/archive_faqs_controller.rb
Normal file
195
app/controllers/archive_faqs_controller.rb
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
class ArchiveFaqsController < ApplicationController
|
||||
before_action :admin_only, except: [:index, :show]
|
||||
before_action :set_locale
|
||||
before_action :validate_locale, if: :logged_in_as_admin?
|
||||
before_action :require_language_id
|
||||
before_action :default_locale_only, only: [:new, :create, :manage, :update_positions, :confirm_delete, :destroy]
|
||||
around_action :with_locale
|
||||
|
||||
# GET /archive_faqs
|
||||
def index
|
||||
@archive_faqs = ArchiveFaq.order("position ASC")
|
||||
unless logged_in_as_admin?
|
||||
@archive_faqs = @archive_faqs.with_translations(I18n.locale)
|
||||
end
|
||||
respond_to do |format|
|
||||
format.html # index.html.erb
|
||||
end
|
||||
end
|
||||
|
||||
# GET /archive_faqs/1
|
||||
def show
|
||||
@questions = []
|
||||
@archive_faq = ArchiveFaq.find_by!(slug: params[:id])
|
||||
if params[:language_id] == "en"
|
||||
@questions = @archive_faq.questions
|
||||
else
|
||||
@archive_faq.questions.each do |question|
|
||||
question.translations.each do |translation|
|
||||
if translation.is_translated == "1" && params[:language_id].to_s == translation.locale.to_s
|
||||
@questions << question
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@page_subtitle = @archive_faq.title + ts(" FAQ")
|
||||
|
||||
respond_to do |format|
|
||||
format.html # show.html.erb
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def build_questions
|
||||
notice = ""
|
||||
num_to_build = params["num_questions"] ? params["num_questions"].to_i : @archive_faq.questions.count
|
||||
if num_to_build < @archive_faq.questions.count
|
||||
notice += ts("There are currently %{num} questions. You can only submit a number equal to or greater than %{num}. ", num: @archive_faq.questions.count)
|
||||
num_to_build = @archive_faq.questions.count
|
||||
elsif params["num_questions"]
|
||||
notice += ts("Set up %{num} questions. ", num: num_to_build)
|
||||
end
|
||||
num_existing = @archive_faq.questions.count
|
||||
num_existing.upto(num_to_build-1) do
|
||||
@archive_faq.questions.build
|
||||
end
|
||||
unless notice.blank?
|
||||
flash[:notice] = notice
|
||||
end
|
||||
end
|
||||
|
||||
public
|
||||
|
||||
# GET /archive_faqs/new
|
||||
def new
|
||||
@archive_faq = authorize ArchiveFaq.new
|
||||
1.times { @archive_faq.questions.build(attributes: { question: "This is a temporary question", content: "This is temporary content", anchor: "ThisIsATemporaryAnchor"})}
|
||||
respond_to do |format|
|
||||
format.html # new.html.erb
|
||||
end
|
||||
end
|
||||
|
||||
# GET /archive_faqs/1/edit
|
||||
def edit
|
||||
@archive_faq = authorize ArchiveFaq.find_by(slug: params[:id])
|
||||
authorize :archive_faq, :full_access? if default_locale?
|
||||
build_questions
|
||||
end
|
||||
|
||||
# GET /archive_faqs/manage
|
||||
def manage
|
||||
@archive_faqs = authorize ArchiveFaq.order("position ASC")
|
||||
end
|
||||
|
||||
# POST /archive_faqs
|
||||
def create
|
||||
@archive_faq = authorize ArchiveFaq.new(archive_faq_params)
|
||||
if @archive_faq.save
|
||||
flash[:notice] = t(".success")
|
||||
redirect_to(@archive_faq)
|
||||
else
|
||||
render action: "new"
|
||||
end
|
||||
end
|
||||
|
||||
# PUT /archive_faqs/1
|
||||
def update
|
||||
@archive_faq = authorize ArchiveFaq.find_by(slug: params[:id])
|
||||
authorize :archive_faq, :full_access? if default_locale?
|
||||
|
||||
if @archive_faq.update(archive_faq_params)
|
||||
flash[:notice] = t(".success")
|
||||
redirect_to(@archive_faq)
|
||||
else
|
||||
render action: "edit"
|
||||
end
|
||||
end
|
||||
|
||||
# reorder FAQs
|
||||
def update_positions
|
||||
authorize :archive_faq
|
||||
if params[:archive_faqs]
|
||||
@archive_faqs = ArchiveFaq.reorder_list(params[:archive_faqs])
|
||||
flash[:notice] = t(".success")
|
||||
elsif params[:archive_faq]
|
||||
params[:archive_faq].each_with_index do |id, position|
|
||||
ArchiveFaq.update(id, position: position + 1)
|
||||
(@archive_faqs ||= []) << ArchiveFaq.find(id)
|
||||
end
|
||||
end
|
||||
respond_to do |format|
|
||||
format.html { redirect_to(archive_faqs_path) }
|
||||
format.js { render nothing: true }
|
||||
end
|
||||
end
|
||||
|
||||
# The ?language_id=somelanguage needs to persist throughout URL changes
|
||||
# Get the value from set_locale to make sure there's no problem with order
|
||||
def default_url_options
|
||||
{ language_id: set_locale.to_s }
|
||||
end
|
||||
|
||||
# Set the locale as an instance variable first
|
||||
def set_locale
|
||||
session[:language_id] = params[:language_id] if Locale.exists?(iso: params[:language_id])
|
||||
|
||||
if current_user.present?
|
||||
@i18n_locale = session[:language_id].presence || current_user.preference.locale.iso
|
||||
else
|
||||
@i18n_locale = session[:language_id].presence || I18n.default_locale
|
||||
end
|
||||
end
|
||||
|
||||
def validate_locale
|
||||
return if params[:language_id].blank? || Locale.exists?(iso: params[:language_id])
|
||||
|
||||
flash[:error] = "The specified locale does not exist."
|
||||
redirect_to url_for(request.query_parameters.merge(language_id: I18n.default_locale))
|
||||
end
|
||||
|
||||
def require_language_id
|
||||
return if params[:language_id].present? && Locale.exists?(iso: params[:language_id])
|
||||
|
||||
redirect_to url_for(request.query_parameters.merge(language_id: @i18n_locale.to_s))
|
||||
end
|
||||
|
||||
def default_locale_only
|
||||
return if default_locale?
|
||||
|
||||
flash[:error] = t("archive_faqs.default_locale_only")
|
||||
redirect_to archive_faqs_path
|
||||
end
|
||||
|
||||
# Setting I18n.locale directly is not thread safe
|
||||
def with_locale
|
||||
I18n.with_locale(@i18n_locale) { yield }
|
||||
end
|
||||
|
||||
# GET /archive_faqs/1/confirm_delete
|
||||
def confirm_delete
|
||||
@archive_faq = authorize ArchiveFaq.find_by(slug: params[:id])
|
||||
end
|
||||
|
||||
# DELETE /archive_faqs/1
|
||||
def destroy
|
||||
@archive_faq = authorize ArchiveFaq.find_by(slug: params[:id])
|
||||
@archive_faq.destroy
|
||||
redirect_to(archive_faqs_path)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def default_locale?
|
||||
@i18n_locale.to_s == I18n.default_locale.to_s
|
||||
end
|
||||
|
||||
def archive_faq_params
|
||||
params.require(:archive_faq).permit(
|
||||
:title,
|
||||
questions_attributes: [
|
||||
:id, :question, :anchor, :content, :screencast, :_destroy, :is_translated
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
224
app/controllers/autocomplete_controller.rb
Normal file
224
app/controllers/autocomplete_controller.rb
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
class AutocompleteController < ApplicationController
|
||||
respond_to :json
|
||||
|
||||
skip_before_action :store_location
|
||||
skip_around_action :set_current_user, except: [:collection_parent_name, :owned_tag_sets, :site_skins]
|
||||
skip_before_action :sanitize_ac_params # can we dare!
|
||||
|
||||
#### DO WE NEED THIS AT ALL? IF IT FIRES WITHOUT A TERM AND 500s BECAUSE USER DID SOMETHING WACKY SO WHAT
|
||||
# # If you have an autocomplete that should fire without a term add it here
|
||||
# before_action :require_term, except: [:tag_in_fandom, :relationship_in_fandom, :character_in_fandom, :nominated_parents]
|
||||
#
|
||||
# def require_term
|
||||
# if params[:term].blank?
|
||||
# flash[:error] = ts("What were you trying to autocomplete?")
|
||||
# redirect_to(request.env["HTTP_REFERER"] || root_path) and return
|
||||
# end
|
||||
# end
|
||||
#
|
||||
#########################################
|
||||
############# LOOKUP ACTIONS GO HERE
|
||||
|
||||
# PSEUDS
|
||||
def pseud
|
||||
return if params[:term].blank?
|
||||
|
||||
render_output(Pseud.autocomplete_lookup(search_param: params[:term], autocomplete_prefix: "autocomplete_pseud").map { |res| Pseud.fullname_from_autocomplete(res) })
|
||||
end
|
||||
|
||||
## TAGS
|
||||
private
|
||||
def tag_output(search_param, tag_type)
|
||||
tags = Tag.autocomplete_lookup(search_param: search_param, autocomplete_prefix: "autocomplete_tag_#{tag_type}")
|
||||
render_output tags.map {|r| Tag.name_from_autocomplete(r)}
|
||||
end
|
||||
public
|
||||
# these are all basically duplicates but make our calls to autocomplete more readable
|
||||
def tag; tag_output(params[:term], params[:type] || "all"); end
|
||||
def fandom; tag_output(params[:term], "fandom"); end
|
||||
def character; tag_output(params[:term], "character"); end
|
||||
def relationship; tag_output(params[:term], "relationship"); end
|
||||
def freeform; tag_output(params[:term], "freeform"); end
|
||||
|
||||
|
||||
## TAGS IN FANDOMS
|
||||
private
|
||||
def tag_in_fandom_output(params)
|
||||
render_output(Tag.autocomplete_fandom_lookup(params).map {|r| Tag.name_from_autocomplete(r)})
|
||||
end
|
||||
public
|
||||
def character_in_fandom; tag_in_fandom_output(params.merge({tag_type: "character"})); end
|
||||
def relationship_in_fandom; tag_in_fandom_output(params.merge({tag_type: "relationship"})); end
|
||||
|
||||
|
||||
## TAGS IN SETS
|
||||
#
|
||||
# Note that only tagsets in OwnedTagSets are in autocomplete
|
||||
#
|
||||
|
||||
# expects the following params:
|
||||
# :tag_set - tag set ids comma-separated
|
||||
# :tag_type - tag type as a string unless "all" desired
|
||||
# :in_any - set to false if only want tags in ALL specified sets
|
||||
# :term - the search term
|
||||
def tags_in_sets
|
||||
results = TagSet.autocomplete_lookup(params)
|
||||
render_output(results.map {|r| Tag.name_from_autocomplete(r)})
|
||||
end
|
||||
|
||||
# expects the following params:
|
||||
# :fandom - fandom name(s) as an array or a comma-separated string
|
||||
# :tag_set - tag set id(s) as an array or a comma-separated string
|
||||
# :tag_type - tag type as a string unless "all" desired
|
||||
# :include_wrangled - set to false if you only want tags from the set associations and NOT tags wrangled into the fandom
|
||||
# :fallback - set to false to NOT do
|
||||
# :term - the search term
|
||||
def associated_tags
|
||||
if params[:fandom].blank?
|
||||
render_output([ts("Please select a fandom first!")])
|
||||
else
|
||||
results = TagSetAssociation.autocomplete_lookup(params)
|
||||
render_output(results.map {|r| Tag.name_from_autocomplete(r)})
|
||||
end
|
||||
end
|
||||
|
||||
## NONCANONICAL TAGS
|
||||
def noncanonical_tag
|
||||
search_param = Query.new.escape_reserved_characters(params[:term])
|
||||
raise "Redshirt: Attempted to constantize invalid class initialize noncanonical_tag #{params[:type].classify}" unless Tag::TYPES.include?(params[:type].classify)
|
||||
|
||||
tag_class = params[:type].classify.constantize
|
||||
one_tag = tag_class.find_by(canonical: false, name: params[:term]) if params[:term].present?
|
||||
# If there is an exact match in the database, ensure it is the first thing suggested.
|
||||
match = if one_tag
|
||||
[one_tag.name]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
# As explained in https://stackoverflow.com/a/54080114, the Elasticsearch suggestion suggester does not support
|
||||
# matches in the middle of a series of words. Therefore, we break the autocomplete query into its individual
|
||||
# words – based on whitespace – except for the last word, which could be incomplete, so a prefix match is
|
||||
# appropriate. This emulates the behavior of SQL `LIKE '%text%'.
|
||||
|
||||
word_list = search_param.split
|
||||
last_word = word_list.pop
|
||||
search_list = word_list.map { |w| { term: { name: { value: w, case_insensitive: true } } } } + [{ prefix: { name: { value: last_word, case_insensitive: true } } }]
|
||||
begin
|
||||
# Size is chosen so we get enough search results from each shard.
|
||||
search_results = $elasticsearch.search(
|
||||
index: TagIndexer.index_name,
|
||||
body: { size: "100", query: { bool: { filter: [{ match: { tag_type: params[:type].capitalize } }, { match: { canonical: false } }], must: search_list } } }
|
||||
)
|
||||
render_output((match + search_results["hits"]["hits"].first(10).map { |t| t["_source"]["name"] }).uniq)
|
||||
rescue Elastic::Transport::Transport::Errors::BadRequest
|
||||
render_output(match)
|
||||
end
|
||||
end
|
||||
|
||||
# more-specific autocompletes should be added below here when they can't be avoided
|
||||
|
||||
# look up collections ranked by number of items they contain
|
||||
|
||||
def collection_fullname
|
||||
results = Collection.autocomplete_lookup(search_param: params[:term], autocomplete_prefix: "autocomplete_collection_all").map {|res| Collection.fullname_from_autocomplete(res)}
|
||||
render_output(results)
|
||||
end
|
||||
|
||||
# return collection names
|
||||
|
||||
def open_collection_names
|
||||
# in this case we want different ids from names so we can display the title but only put in the name
|
||||
results = Collection.autocomplete_lookup(search_param: params[:term], autocomplete_prefix: "autocomplete_collection_open").map do |str|
|
||||
{id: (whole_name = Collection.name_from_autocomplete(str)), name: Collection.title_from_autocomplete(str) + " (#{whole_name})" }
|
||||
end
|
||||
respond_with(results)
|
||||
end
|
||||
|
||||
# For creating collections, autocomplete the name of a parent collection owned by the user only
|
||||
def collection_parent_name
|
||||
render_output(current_user.maintained_collections.top_level.with_name_like(params[:term]).pluck(:name).sort)
|
||||
end
|
||||
|
||||
# for looking up existing urls for external works to avoid duplication
|
||||
def external_work
|
||||
render_output(ExternalWork.where(["url LIKE ?", '%' + params[:term] + '%']).limit(10).order(:url).pluck(:url))
|
||||
end
|
||||
|
||||
# the pseuds of the potential matches who could fulfill the requests in the given signup
|
||||
def potential_offers
|
||||
potential_matches(false)
|
||||
end
|
||||
|
||||
# the pseuds of the potential matches who want the offers in the given signup
|
||||
def potential_requests
|
||||
potential_matches(true)
|
||||
end
|
||||
|
||||
# Return matching potential requests or offers
|
||||
def potential_matches(return_requests=true)
|
||||
search_param = params[:term]
|
||||
signup_id = params[:signup_id]
|
||||
signup = ChallengeSignup.find(signup_id)
|
||||
pmatches = return_requests ?
|
||||
signup.offer_potential_matches.sort.reverse.map {|pm| pm.request_signup.pseud.byline} :
|
||||
signup.request_potential_matches.sort.reverse.map {|pm| pm.offer_signup.pseud.byline}
|
||||
pmatches.select! { |pm| pm.match(/#{Regexp.escape(search_param)}/) } if search_param.present?
|
||||
render_output(pmatches)
|
||||
end
|
||||
|
||||
# owned tag sets that are usable by all
|
||||
def owned_tag_sets
|
||||
if params[:term].length > 0
|
||||
search_param = '%' + params[:term] + '%'
|
||||
render_output(OwnedTagSet.limit(10).order(:title).usable.where("owned_tag_sets.title LIKE ?", search_param).collect(&:title))
|
||||
end
|
||||
end
|
||||
|
||||
# skins for parenting
|
||||
def site_skins
|
||||
if params[:term].present?
|
||||
search_param = '%' + params[:term] + '%'
|
||||
query = Skin.site_skins.where("title LIKE ?", search_param).limit(15).sort_by_recent
|
||||
if logged_in?
|
||||
query = query.approved_or_owned_by(current_user)
|
||||
else
|
||||
query = query.approved_skins
|
||||
end
|
||||
render_output(query.pluck(:title))
|
||||
end
|
||||
end
|
||||
|
||||
# admin posts for translations, formatted as Admin Post Title (Post #id)
|
||||
def admin_posts
|
||||
if params[:term].present?
|
||||
search_param = '%' + params[:term] + '%'
|
||||
results = AdminPost.non_translated.where("title LIKE ?", search_param).limit(ArchiveConfig.MAX_RECENT).map do |result|
|
||||
{id: (post_id = result.id),
|
||||
name: result.title + " (Post ##{post_id})" }
|
||||
end
|
||||
respond_with(results)
|
||||
end
|
||||
end
|
||||
|
||||
def admin_post_tags
|
||||
if params[:term].present?
|
||||
search_param = '%' + params[:term].strip + '%'
|
||||
query = AdminPostTag.where("name LIKE ?", search_param).limit(ArchiveConfig.MAX_RECENT)
|
||||
render_output(query.pluck(:name))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Because of the respond_to :json at the top of the controller, this will return a JSON-encoded
|
||||
# response which the autocomplete javascript on the other end should be able to handle :)
|
||||
def render_output(result_strings)
|
||||
if result_strings.first.is_a?(String)
|
||||
respond_with(result_strings.map {|str| {id: str, name: str}})
|
||||
else
|
||||
respond_with(result_strings)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
89
app/controllers/blocked/users_controller.rb
Normal file
89
app/controllers/blocked/users_controller.rb
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
module Blocked
|
||||
class UsersController < ApplicationController
|
||||
before_action :set_user
|
||||
|
||||
before_action :check_ownership, except: :index
|
||||
before_action :check_ownership_or_admin, only: :index
|
||||
before_action :check_admin_permissions, only: :index
|
||||
|
||||
before_action :build_block, only: [:confirm_block, :create]
|
||||
before_action :set_block, only: [:confirm_unblock, :destroy]
|
||||
|
||||
# GET /users/:user_id/blocked/users
|
||||
def index
|
||||
@blocks = @user.blocks_as_blocker
|
||||
.joins(:blocked)
|
||||
.includes(blocked: [:pseuds, { default_pseud: { icon_attachment: { blob: {
|
||||
variant_records: { image_attachment: :blob },
|
||||
preview_image_attachment: { blob: { variant_records: { image_attachment: :blob } } }
|
||||
} } } }])
|
||||
.order(created_at: :desc).order(id: :desc).page(params[:page])
|
||||
|
||||
@pseuds = @blocks.map { |b| b.blocked.default_pseud }
|
||||
@rec_counts = Pseud.rec_counts_for_pseuds(@pseuds)
|
||||
@work_counts = Pseud.work_counts_for_pseuds(@pseuds)
|
||||
|
||||
@page_subtitle = "Blocked Users"
|
||||
end
|
||||
|
||||
# GET /users/:user_id/blocked/users/confirm_block
|
||||
def confirm_block
|
||||
@hide_dashboard = true
|
||||
|
||||
return if @block.valid?
|
||||
|
||||
# We can't block this user for whatever reason
|
||||
flash[:error] = @block.errors.full_messages.first
|
||||
redirect_to user_blocked_users_path(@user)
|
||||
end
|
||||
|
||||
# GET /users/:user_id/blocked/users/confirm_unblock
|
||||
def confirm_unblock
|
||||
@hide_dashboard = true
|
||||
|
||||
@blocked = @block.blocked
|
||||
end
|
||||
|
||||
# POST /users/:user_id/blocked/users
|
||||
def create
|
||||
if @block.save
|
||||
flash[:notice] = t(".blocked", name: @block.blocked.login)
|
||||
else
|
||||
# We can't block this user for whatever reason
|
||||
flash[:error] = @block.errors.full_messages.first
|
||||
end
|
||||
|
||||
redirect_to user_blocked_users_path(@user)
|
||||
end
|
||||
|
||||
# DELETE /users/:user_id/blocked/users/:id
|
||||
def destroy
|
||||
@block.destroy
|
||||
flash[:notice] = t(".unblocked", name: @block.blocked.login)
|
||||
redirect_to user_blocked_users_path(@user)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Sets the user whose blocks we're viewing/modifying.
|
||||
def set_user
|
||||
@user = User.find_by!(login: params[:user_id])
|
||||
@check_ownership_of = @user
|
||||
end
|
||||
|
||||
# Builds (but doesn't save) a block matching the desired params:
|
||||
def build_block
|
||||
blocked_byline = params.fetch(:blocked_id, "")
|
||||
@block = @user.blocks_as_blocker.build(blocked_byline: blocked_byline)
|
||||
@blocked = @block.blocked
|
||||
end
|
||||
|
||||
def set_block
|
||||
@block = @user.blocks_as_blocker.find(params[:id])
|
||||
end
|
||||
|
||||
def check_admin_permissions
|
||||
authorize Block if logged_in_as_admin?
|
||||
end
|
||||
end
|
||||
end
|
||||
397
app/controllers/bookmarks_controller.rb
Normal file
397
app/controllers/bookmarks_controller.rb
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
class BookmarksController < ApplicationController
|
||||
before_action :load_collection
|
||||
before_action :load_owner, only: [:index]
|
||||
before_action :load_bookmarkable, only: [:index, :new, :create]
|
||||
before_action :users_only, only: [:new, :create, :edit, :update]
|
||||
before_action :check_user_status, only: [:new, :create, :edit, :update]
|
||||
before_action :check_parent_visible, only: [:index, :new]
|
||||
before_action :load_bookmark, only: [:show, :edit, :update, :destroy, :confirm_delete, :share]
|
||||
before_action :check_visibility, only: [:show, :share]
|
||||
before_action :check_ownership, only: [:edit, :update, :destroy, :confirm_delete, :share]
|
||||
|
||||
before_action :check_pseud_ownership, only: [:create, :update]
|
||||
|
||||
skip_before_action :store_location, only: [:share]
|
||||
|
||||
def check_pseud_ownership
|
||||
if params[:bookmark][:pseud_id]
|
||||
pseud = Pseud.find(bookmark_params[:pseud_id])
|
||||
unless pseud && current_user && current_user.pseuds.include?(pseud)
|
||||
flash[:error] = ts("You can't bookmark with that pseud.")
|
||||
redirect_to root_path and return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def check_parent_visible
|
||||
check_visibility_for(@bookmarkable)
|
||||
end
|
||||
|
||||
# get the parent
|
||||
def load_bookmarkable
|
||||
if params[:work_id]
|
||||
@bookmarkable = Work.find(params[:work_id])
|
||||
elsif params[:external_work_id]
|
||||
@bookmarkable = ExternalWork.find(params[:external_work_id])
|
||||
elsif params[:series_id]
|
||||
@bookmarkable = Series.find(params[:series_id])
|
||||
end
|
||||
end
|
||||
|
||||
def load_bookmark
|
||||
@bookmark = Bookmark.find(params[:id])
|
||||
@check_ownership_of = @bookmark
|
||||
@check_visibility_of = @bookmark
|
||||
end
|
||||
|
||||
def search
|
||||
@languages = Language.default_order
|
||||
options = params[:bookmark_search].present? ? bookmark_search_params : {}
|
||||
options.merge!(page: params[:page]) if params[:page].present?
|
||||
options[:show_private] = false
|
||||
options[:show_restricted] = logged_in? || logged_in_as_admin?
|
||||
@search = BookmarkSearchForm.new(options)
|
||||
@page_subtitle = ts("Search Bookmarks")
|
||||
if params[:bookmark_search].present? && params[:edit_search].blank?
|
||||
if @search.query.present?
|
||||
@page_subtitle = ts("Bookmarks Matching '%{query}'", query: @search.query)
|
||||
end
|
||||
@bookmarks = @search.search_results.scope(:for_blurb)
|
||||
flash_search_warnings(@bookmarks)
|
||||
set_own_bookmarks
|
||||
render 'search_results'
|
||||
end
|
||||
end
|
||||
|
||||
def index
|
||||
if @bookmarkable
|
||||
@bookmarks = @bookmarkable.bookmarks.not_private.order_by_created_at.paginate(page: params[:page], per_page: ArchiveConfig.ITEMS_PER_PAGE)
|
||||
@bookmarks = @bookmarks.where(hidden_by_admin: false) unless logged_in_as_admin?
|
||||
else
|
||||
base_options = {
|
||||
show_private: (@user.present? && @user == current_user),
|
||||
show_restricted: logged_in? || logged_in_as_admin?,
|
||||
page: params[:page]
|
||||
}
|
||||
|
||||
options = params[:bookmark_search].present? ? bookmark_search_params : {}
|
||||
|
||||
if params[:include_bookmark_search].present?
|
||||
params[:include_bookmark_search].keys.each do |key|
|
||||
options[key] ||= []
|
||||
options[key] << params[:include_bookmark_search][key]
|
||||
options[key].flatten!
|
||||
end
|
||||
end
|
||||
|
||||
if params[:exclude_bookmark_search].present?
|
||||
params[:exclude_bookmark_search].keys.each do |key|
|
||||
# Keep bookmarker tags separate, so we can search for them on bookmarks
|
||||
# and search for the rest on bookmarkables
|
||||
options_key = key == "tag_ids" ? :excluded_bookmark_tag_ids : :excluded_tag_ids
|
||||
options[options_key] ||= []
|
||||
options[options_key] << params[:exclude_bookmark_search][key]
|
||||
options[options_key].flatten!
|
||||
end
|
||||
end
|
||||
|
||||
options.merge!(base_options)
|
||||
@page_subtitle = index_page_title
|
||||
|
||||
if @owner.present?
|
||||
@search = BookmarkSearchForm.new(options.merge(faceted: true, parent: @owner))
|
||||
|
||||
if @user.blank?
|
||||
# When it's not a particular user's bookmarks, we want
|
||||
# to list *bookmarkable* items to avoid duplication
|
||||
@bookmarkable_items = @search.bookmarkable_search_results.scope(:for_blurb)
|
||||
flash_search_warnings(@bookmarkable_items)
|
||||
@facets = @bookmarkable_items.facets
|
||||
else
|
||||
# We're looking at a particular user's bookmarks, so
|
||||
# just retrieve the standard search results and their facets.
|
||||
@bookmarks = @search.search_results.scope(:for_blurb)
|
||||
flash_search_warnings(@bookmarks)
|
||||
@facets = @bookmarks.facets
|
||||
end
|
||||
|
||||
if @search.options[:excluded_tag_ids].present? || @search.options[:excluded_bookmark_tag_ids].present?
|
||||
# Excluded tags do not appear in search results, so we need to generate empty facets
|
||||
# to keep them as checkboxes on the filters.
|
||||
excluded_tag_ids = @search.options[:excluded_tag_ids] || []
|
||||
excluded_bookmark_tag_ids = @search.options[:excluded_bookmark_tag_ids] || []
|
||||
|
||||
# It's possible to determine the tag types by looking at
|
||||
# the original parameters params[:exclude_bookmark_search],
|
||||
# but we need the tag names too, so a database query is unavoidable.
|
||||
tags = Tag.where(id: excluded_tag_ids + excluded_bookmark_tag_ids)
|
||||
tags.each do |tag|
|
||||
if excluded_tag_ids.include?(tag.id.to_s)
|
||||
key = tag.class.to_s.underscore
|
||||
@facets[key] ||= []
|
||||
@facets[key] << QueryFacet.new(tag.id, tag.name, 0)
|
||||
end
|
||||
if excluded_bookmark_tag_ids.include?(tag.id.to_s)
|
||||
key = 'tag'
|
||||
@facets[key] ||= []
|
||||
@facets[key] << QueryFacet.new(tag.id, tag.name, 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
elsif use_caching?
|
||||
@bookmarks = Rails.cache.fetch("bookmarks/index/latest/v2_true", expires_in: ArchiveConfig.SECONDS_UNTIL_BOOKMARK_INDEX_EXPIRE.seconds) do
|
||||
search = BookmarkSearchForm.new(show_private: false, show_restricted: false, sort_column: 'created_at')
|
||||
results = search.search_results.scope(:for_blurb)
|
||||
flash_search_warnings(results)
|
||||
results.to_a
|
||||
end
|
||||
else
|
||||
@bookmarks = Bookmark.latest.for_blurb.to_a
|
||||
end
|
||||
end
|
||||
set_own_bookmarks
|
||||
|
||||
@pagy =
|
||||
if @bookmarks.respond_to?(:total_pages)
|
||||
pagy_query_result(@bookmarks)
|
||||
elsif @bookmarkable_items.respond_to?(:total_pages)
|
||||
pagy_query_result(@bookmarkable_items)
|
||||
end
|
||||
end
|
||||
|
||||
# GET /:locale/bookmark/:id
|
||||
# GET /:locale/users/:user_id/bookmarks/:id
|
||||
# GET /:locale/works/:work_id/bookmark/:id
|
||||
# GET /:locale/external_works/:external_work_id/bookmark/:id
|
||||
def show
|
||||
end
|
||||
|
||||
# GET /bookmarks/new
|
||||
# GET /bookmarks/new.xml
|
||||
def new
|
||||
@bookmark = Bookmark.new
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.js {
|
||||
@button_name = ts("Create")
|
||||
@action = :create
|
||||
render action: "bookmark_form_dynamic"
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# GET /bookmarks/1/edit
|
||||
def edit
|
||||
@bookmarkable = @bookmark.bookmarkable
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.js {
|
||||
@button_name = ts("Update")
|
||||
@action = :update
|
||||
render action: "bookmark_form_dynamic"
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# POST /bookmarks
|
||||
# POST /bookmarks.xml
|
||||
def create
|
||||
@bookmarkable ||= ExternalWork.new(external_work_params)
|
||||
@bookmark = Bookmark.new(bookmark_params.merge(bookmarkable: @bookmarkable))
|
||||
if @bookmark.errors.empty? && @bookmark.save
|
||||
flash[:notice] = ts("Bookmark was successfully created. It should appear in bookmark listings within the next few minutes.")
|
||||
redirect_to(bookmark_path(@bookmark))
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
# PUT /bookmarks/1
|
||||
# PUT /bookmarks/1.xml
|
||||
def update
|
||||
new_collections = []
|
||||
unapproved_collections = []
|
||||
errors = []
|
||||
bookmark_params[:collection_names]&.split(",")&.map(&:strip)&.uniq&.each do |collection_name|
|
||||
collection = Collection.find_by(name: collection_name)
|
||||
if collection.nil?
|
||||
errors << ts("%{name} does not exist.", name: collection_name)
|
||||
else
|
||||
if @bookmark.collections.include?(collection)
|
||||
next
|
||||
elsif collection.closed? && !collection.user_is_maintainer?(User.current_user)
|
||||
errors << ts("%{title} is closed to new submissions.", title: collection.title)
|
||||
elsif @bookmark.add_to_collection(collection) && @bookmark.save
|
||||
if @bookmark.approved_collections.include?(collection)
|
||||
new_collections << collection
|
||||
else
|
||||
unapproved_collections << collection
|
||||
end
|
||||
else
|
||||
errors << ts("Something went wrong trying to add collection %{title}, sorry!", title: collection.title)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# messages to the user
|
||||
unless errors.empty?
|
||||
flash[:error] = ts("We couldn't add your submission to the following collections: ") + errors.join("<br />")
|
||||
end
|
||||
|
||||
unless new_collections.empty?
|
||||
flash[:notice] = ts("Added to collection(s): %{collections}.",
|
||||
collections: new_collections.collect(&:title).join(", "))
|
||||
end
|
||||
unless unapproved_collections.empty?
|
||||
flash[:notice] = flash[:notice] ? flash[:notice] + " " : ""
|
||||
flash[:notice] += if unapproved_collections.size > 1
|
||||
ts("You have submitted your bookmark to moderated collections (%{all_collections}). It will not become a part of those collections until it has been approved by a moderator.", all_collections: unapproved_collections.map(&:title).join(", "))
|
||||
else
|
||||
ts("You have submitted your bookmark to the moderated collection '%{collection}'. It will not become a part of the collection until it has been approved by a moderator.", collection: unapproved_collections.first.title)
|
||||
end
|
||||
end
|
||||
|
||||
flash[:notice] = (flash[:notice]).html_safe unless flash[:notice].blank?
|
||||
flash[:error] = (flash[:error]).html_safe unless flash[:error].blank?
|
||||
|
||||
if @bookmark.update(bookmark_params) && errors.empty?
|
||||
flash[:notice] = flash[:notice] ? " " + flash[:notice] : ""
|
||||
flash[:notice] = ts("Bookmark was successfully updated.").html_safe + flash[:notice]
|
||||
flash[:notice] = flash[:notice].html_safe
|
||||
redirect_to(@bookmark)
|
||||
else
|
||||
@bookmarkable = @bookmark.bookmarkable
|
||||
render :edit and return
|
||||
end
|
||||
end
|
||||
|
||||
# GET /bookmarks/1/share
|
||||
def share
|
||||
if request.xhr?
|
||||
if @bookmark.bookmarkable.is_a?(Work) && @bookmark.bookmarkable.unrevealed?
|
||||
render template: "errors/404", status: :not_found
|
||||
else
|
||||
render layout: false
|
||||
end
|
||||
else
|
||||
# Avoid getting an unstyled page if JavaScript is disabled
|
||||
flash[:error] = ts("Sorry, you need to have JavaScript enabled for this.")
|
||||
redirect_back(fallback_location: root_path)
|
||||
end
|
||||
end
|
||||
|
||||
def confirm_delete
|
||||
end
|
||||
|
||||
# DELETE /bookmarks/1
|
||||
# DELETE /bookmarks/1.xml
|
||||
def destroy
|
||||
@bookmark.destroy
|
||||
flash[:notice] = ts("Bookmark was successfully deleted.")
|
||||
redirect_to user_bookmarks_path(current_user)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def load_owner
|
||||
if params[:user_id].present?
|
||||
@user = User.find_by(login: params[:user_id])
|
||||
unless @user
|
||||
raise ActiveRecord::RecordNotFound, "Couldn't find user named '#{params[:user_id]}'"
|
||||
end
|
||||
if params[:pseud_id].present?
|
||||
@pseud = @user.pseuds.find_by(name: params[:pseud_id])
|
||||
unless @pseud
|
||||
raise ActiveRecord::RecordNotFound, "Couldn't find pseud named '#{params[:pseud_id]}'"
|
||||
end
|
||||
end
|
||||
end
|
||||
if params[:tag_id]
|
||||
@tag = Tag.find_by_name(params[:tag_id])
|
||||
unless @tag
|
||||
raise ActiveRecord::RecordNotFound, "Couldn't find tag named '#{params[:tag_id]}'"
|
||||
end
|
||||
unless @tag.canonical?
|
||||
if @tag.merger.present?
|
||||
redirect_to tag_bookmarks_path(@tag.merger) and return
|
||||
else
|
||||
redirect_to tag_path(@tag) and return
|
||||
end
|
||||
end
|
||||
end
|
||||
@owner = @bookmarkable || @pseud || @user || @collection || @tag
|
||||
end
|
||||
|
||||
def index_page_title
|
||||
if @owner.present?
|
||||
owner_name = case @owner.class.to_s
|
||||
when 'Pseud'
|
||||
@owner.name
|
||||
when 'User'
|
||||
@owner.login
|
||||
when 'Collection'
|
||||
@owner.title
|
||||
else
|
||||
@owner.try(:name)
|
||||
end
|
||||
"#{owner_name} - Bookmarks".html_safe
|
||||
else
|
||||
"Latest Bookmarks"
|
||||
end
|
||||
end
|
||||
|
||||
def set_own_bookmarks
|
||||
return unless @bookmarks
|
||||
@own_bookmarks = []
|
||||
if current_user.is_a?(User)
|
||||
pseud_ids = current_user.pseuds.pluck(:id)
|
||||
@own_bookmarks = @bookmarks.select do |b|
|
||||
pseud_ids.include?(b.pseud_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def bookmark_params
|
||||
params.require(:bookmark).permit(
|
||||
:pseud_id, :bookmarker_notes, :tag_string, :collection_names, :private, :rec
|
||||
)
|
||||
end
|
||||
|
||||
def external_work_params
|
||||
params.require(:external_work).permit(
|
||||
:url, :author, :title, :fandom_string, :rating_string, :relationship_string,
|
||||
:character_string, :summary, category_strings: []
|
||||
)
|
||||
end
|
||||
|
||||
def bookmark_search_params
|
||||
params.require(:bookmark_search).permit(
|
||||
:bookmark_query,
|
||||
:bookmarkable_query,
|
||||
:bookmarker,
|
||||
:bookmark_notes,
|
||||
:rec,
|
||||
:with_notes,
|
||||
:bookmarkable_type,
|
||||
:language_id,
|
||||
:date,
|
||||
:bookmarkable_date,
|
||||
:sort_column,
|
||||
:other_tag_names,
|
||||
:excluded_tag_names,
|
||||
:other_bookmark_tag_names,
|
||||
:excluded_bookmark_tag_names,
|
||||
rating_ids: [],
|
||||
warning_ids: [], # backwards compatibility
|
||||
archive_warning_ids: [],
|
||||
category_ids: [],
|
||||
fandom_ids: [],
|
||||
character_ids: [],
|
||||
relationship_ids: [],
|
||||
freeform_ids: [],
|
||||
tag_ids: [],
|
||||
)
|
||||
end
|
||||
end
|
||||
109
app/controllers/challenge/gift_exchange_controller.rb
Normal file
109
app/controllers/challenge/gift_exchange_controller.rb
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
class Challenge::GiftExchangeController < ChallengesController
|
||||
|
||||
before_action :users_only
|
||||
before_action :load_collection
|
||||
before_action :load_challenge, except: [:new, :create]
|
||||
before_action :collection_owners_only, only: [:new, :create, :edit, :update, :destroy]
|
||||
|
||||
# ACTIONS
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def new
|
||||
if (@collection.challenge)
|
||||
flash[:notice] = ts("There is already a challenge set up for this collection.")
|
||||
# TODO this will break if the challenge isn't a gift exchange
|
||||
redirect_to edit_collection_gift_exchange_path(@collection)
|
||||
else
|
||||
@challenge = GiftExchange.new
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def create
|
||||
@challenge = GiftExchange.new(gift_exchange_params)
|
||||
if @challenge.save
|
||||
@collection.challenge = @challenge
|
||||
@collection.save
|
||||
flash[:notice] = ts('Challenge was successfully created.')
|
||||
redirect_to collection_profile_path(@collection)
|
||||
else
|
||||
render action: :new
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @challenge.update(gift_exchange_params)
|
||||
flash[:notice] = ts('Challenge was successfully updated.')
|
||||
|
||||
# expire the cache on the signup form
|
||||
ActionController::Base.new.expire_fragment('challenge_signups/new')
|
||||
|
||||
# see if we initialized the tag set
|
||||
redirect_to collection_profile_path(@collection)
|
||||
else
|
||||
render action: :edit
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@challenge.destroy
|
||||
flash[:notice] = 'Challenge settings were deleted.'
|
||||
redirect_to @collection
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def gift_exchange_params
|
||||
params.require(:gift_exchange).permit(
|
||||
:signup_open, :time_zone, :signups_open_at_string, :signups_close_at_string,
|
||||
:assignments_due_at_string, :requests_summary_visible, :requests_num_required,
|
||||
:requests_num_allowed, :offers_num_required, :offers_num_allowed,
|
||||
:signup_instructions_general, :signup_instructions_requests,
|
||||
:signup_instructions_offers, :request_url_label, :offer_url_label,
|
||||
:offer_description_label, :request_description_label, :works_reveal_at_string,
|
||||
:authors_reveal_at_string,
|
||||
request_restriction_attributes: [
|
||||
:id, :optional_tags_allowed, :title_required, :title_allowed, :description_required,
|
||||
:description_allowed, :url_required, :url_allowed, :fandom_num_required,
|
||||
:fandom_num_allowed, :allow_any_fandom, :require_unique_fandom,
|
||||
:character_num_required, :character_num_allowed, :allow_any_character,
|
||||
:require_unique_character, :relationship_num_required, :relationship_num_allowed,
|
||||
:allow_any_relationship, :require_unique_relationship, :rating_num_required,
|
||||
:rating_num_allowed, :allow_any_rating, :require_unique_rating,
|
||||
:category_num_required, :category_num_allowed, :allow_any_category,
|
||||
:require_unique_category, :freeform_num_required, :freeform_num_allowed,
|
||||
:allow_any_freeform, :require_unique_freeform, :archive_warning_num_required,
|
||||
:archive_warning_num_allowed, :allow_any_archive_warning, :require_unique_archive_warning
|
||||
],
|
||||
offer_restriction_attributes: [
|
||||
:id, :optional_tags_allowed, :title_required, :title_allowed,
|
||||
:description_required, :description_allowed, :url_required, :url_allowed,
|
||||
:fandom_num_required, :fandom_num_allowed, :allow_any_fandom,
|
||||
:require_unique_fandom, :character_num_required, :character_num_allowed,
|
||||
:allow_any_character, :require_unique_character, :relationship_num_required,
|
||||
:relationship_num_allowed, :allow_any_relationship, :require_unique_relationship,
|
||||
:rating_num_required, :rating_num_allowed, :rating_num_required, :allow_any_rating, :require_unique_rating,
|
||||
:category_num_required, :category_num_allowed, :allow_any_category,
|
||||
:require_unique_category, :freeform_num_required, :freeform_num_allowed,
|
||||
:allow_any_freeform, :require_unique_freeform, :archive_warning_num_required,
|
||||
:archive_warning_num_allowed, :allow_any_archive_warning, :require_unique_archive_warning,
|
||||
:tag_sets_to_add, :character_restrict_to_fandom,
|
||||
:character_restrict_to_tag_set, :relationship_restrict_to_fandom,
|
||||
:relationship_restrict_to_tag_set,
|
||||
tag_sets_to_remove: []
|
||||
],
|
||||
potential_match_settings_attributes: [
|
||||
:id, :num_required_prompts, :num_required_fandoms, :num_required_characters,
|
||||
:num_required_relationships, :num_required_freeforms, :num_required_categories,
|
||||
:num_required_ratings, :num_required_archive_warnings, :include_optional_fandoms,
|
||||
:include_optional_characters, :include_optional_relationships,
|
||||
:include_optional_freeforms, :include_optional_categories, :include_optional_ratings,
|
||||
:include_optional_archive_warnings
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
80
app/controllers/challenge/prompt_meme_controller.rb
Normal file
80
app/controllers/challenge/prompt_meme_controller.rb
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
class Challenge::PromptMemeController < ChallengesController
|
||||
|
||||
before_action :users_only
|
||||
before_action :load_collection
|
||||
before_action :load_challenge, except: [:new, :create]
|
||||
before_action :collection_owners_only, only: [:new, :create, :edit, :update, :destroy]
|
||||
|
||||
# ACTIONS
|
||||
|
||||
# is actually a blank page - should it be redirected to collection profile?
|
||||
def show
|
||||
end
|
||||
|
||||
# The new form for prompt memes is actually the challenge settings page because challenges are always created in the context of a collection.
|
||||
def new
|
||||
if (@collection.challenge)
|
||||
flash[:notice] = ts("There is already a challenge set up for this collection.")
|
||||
redirect_to edit_collection_prompt_meme_path(@collection)
|
||||
else
|
||||
@challenge = PromptMeme.new
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def create
|
||||
@challenge = PromptMeme.new(prompt_meme_params)
|
||||
if @challenge.save
|
||||
@collection.challenge = @challenge
|
||||
@collection.save
|
||||
flash[:notice] = ts('Challenge was successfully created.')
|
||||
redirect_to collection_profile_path(@collection)
|
||||
else
|
||||
render action: :new
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @challenge.update(prompt_meme_params)
|
||||
flash[:notice] = 'Challenge was successfully updated.'
|
||||
# expire the cache on the signup form
|
||||
ActionController::Base.new.expire_fragment('challenge_signups/new')
|
||||
redirect_to @collection
|
||||
else
|
||||
render action: :edit
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@challenge.destroy
|
||||
flash[:notice] = 'Challenge settings were deleted.'
|
||||
redirect_to @collection
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def prompt_meme_params
|
||||
params.require(:prompt_meme).permit(
|
||||
:signup_open, :time_zone, :signups_open_at_string, :signups_close_at_string,
|
||||
:assignments_due_at_string, :anonymous, :requests_num_required, :requests_num_allowed,
|
||||
:signup_instructions_general, :signup_instructions_requests, :request_url_label,
|
||||
:request_description_label, :works_reveal_at_string, :authors_reveal_at_string,
|
||||
request_restriction_attributes: [ :id, :optional_tags_allowed, :title_required,
|
||||
:title_allowed, :description_required, :description_allowed, :url_required,
|
||||
:url_allowed, :fandom_num_required, :fandom_num_allowed, :require_unique_fandom,
|
||||
:character_num_required, :character_num_allowed, :category_num_required,
|
||||
:category_num_allowed, :require_unique_category, :require_unique_character,
|
||||
:relationship_num_required, :relationship_num_allowed, :require_unique_relationship,
|
||||
:rating_num_required, :rating_num_allowed, :require_unique_rating,
|
||||
:freeform_num_required, :freeform_num_allowed, :require_unique_freeform,
|
||||
:archive_warning_num_required, :archive_warning_num_allowed, :require_unique_archive_warning,
|
||||
:tag_sets_to_remove, :tag_sets_to_add, :character_restrict_to_fandom,
|
||||
:character_restrict_to_tag_set, :relationship_restrict_to_fandom,
|
||||
:relationship_restrict_to_tag_set, tag_sets_to_remove: []
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
end
|
||||
243
app/controllers/challenge_assignments_controller.rb
Normal file
243
app/controllers/challenge_assignments_controller.rb
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
class ChallengeAssignmentsController < ApplicationController
|
||||
before_action :users_only
|
||||
|
||||
before_action :load_collection, except: [:index]
|
||||
before_action :load_challenge, except: [:index]
|
||||
before_action :collection_owners_only, except: [:index, :show, :default]
|
||||
|
||||
before_action :load_assignment_from_id, only: [:show, :default]
|
||||
before_action :owner_only, only: [:default]
|
||||
|
||||
before_action :check_signup_closed, except: [:index]
|
||||
before_action :check_assignments_not_sent, only: [:generate, :set, :send_out]
|
||||
before_action :check_assignments_sent, only: [:default, :purge, :confirm_purge]
|
||||
|
||||
|
||||
# PERMISSIONS AND STATUS CHECKING
|
||||
|
||||
def load_challenge
|
||||
@challenge = @collection.challenge if @collection
|
||||
no_challenge unless @challenge
|
||||
end
|
||||
|
||||
def no_challenge
|
||||
flash[:error] = t('challenge_assignments.no_challenge', default: "What challenge did you want to work with?")
|
||||
redirect_to collection_path(@collection) rescue redirect_to '/'
|
||||
false
|
||||
end
|
||||
|
||||
def load_assignment_from_id
|
||||
@challenge_assignment = @collection.assignments.find(params[:id])
|
||||
end
|
||||
|
||||
def owner_only
|
||||
return if current_user == @challenge_assignment.offering_pseud.user
|
||||
|
||||
flash[:error] = t("challenge_assignments.validation.not_owner")
|
||||
redirect_to root_path
|
||||
end
|
||||
|
||||
def check_signup_closed
|
||||
signup_open and return unless !@challenge.signup_open
|
||||
end
|
||||
|
||||
def signup_open
|
||||
flash[:error] = t('challenge_assignments.signup_open', default: "Signup is currently open, you cannot make assignments now.")
|
||||
redirect_to @collection rescue redirect_to '/'
|
||||
false
|
||||
end
|
||||
|
||||
def check_assignments_not_sent
|
||||
assignments_sent and return unless @challenge.assignments_sent_at.nil?
|
||||
end
|
||||
|
||||
def assignments_sent
|
||||
flash[:error] = t('challenge_assignments.assignments_sent', default: "Assignments have already been sent! If necessary, you can purge them.")
|
||||
redirect_to collection_assignments_path(@collection) rescue redirect_to '/'
|
||||
false
|
||||
end
|
||||
|
||||
def check_assignments_sent
|
||||
assignments_not_sent and return unless @challenge.assignments_sent_at
|
||||
end
|
||||
|
||||
def assignments_not_sent
|
||||
flash[:error] = t('challenge_assignments.assignments_not_sent', default: "Assignments have not been sent! You might want matching instead.")
|
||||
redirect_to collection_path(@collection) rescue redirect_to '/'
|
||||
false
|
||||
end
|
||||
|
||||
|
||||
|
||||
# ACTIONS
|
||||
|
||||
def index
|
||||
if params[:user_id] && (@user = User.find_by(login: params[:user_id]))
|
||||
if current_user == @user
|
||||
if params[:collection_id] && (@collection = Collection.find_by(name: params[:collection_id]))
|
||||
@challenge_assignments = @user.offer_assignments.in_collection(@collection).undefaulted + @user.pinch_hit_assignments.in_collection(@collection).undefaulted
|
||||
else
|
||||
@challenge_assignments = @user.offer_assignments.undefaulted + @user.pinch_hit_assignments.undefaulted
|
||||
end
|
||||
else
|
||||
flash[:error] = ts("You aren't allowed to see that user's assignments.")
|
||||
redirect_to '/' and return
|
||||
end
|
||||
else
|
||||
# do error-checking for the collection case
|
||||
return unless load_collection
|
||||
@challenge = @collection.challenge if @collection
|
||||
signup_open and return unless !@challenge.signup_open
|
||||
access_denied and return unless @challenge.user_allowed_to_see_assignments?(current_user)
|
||||
|
||||
# we temporarily are ordering by requesting pseud to avoid left join
|
||||
@assignments = case
|
||||
when params[:pinch_hit]
|
||||
# order by pinch hitter name
|
||||
ChallengeAssignment.unfulfilled_in_collection(@collection).undefaulted.with_pinch_hitter.joins("INNER JOIN pseuds ON (challenge_assignments.pinch_hitter_id = pseuds.id)").order("pseuds.name")
|
||||
when params[:fulfilled]
|
||||
@collection.assignments.fulfilled.order_by_requesting_pseud
|
||||
when params[:unfulfilled]
|
||||
ChallengeAssignment.unfulfilled_in_collection(@collection).undefaulted.order_by_requesting_pseud
|
||||
else
|
||||
@collection.assignments.defaulted.uncovered.order_by_requesting_pseud
|
||||
end
|
||||
@assignments = @assignments.page(params[:page])
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
unless @challenge.user_allowed_to_see_assignments?(current_user) || @challenge_assignment.offering_pseud.user == current_user
|
||||
flash[:error] = ts("You aren't allowed to see that assignment!")
|
||||
redirect_to "/" and return
|
||||
end
|
||||
if @challenge_assignment.defaulted?
|
||||
flash[:notice] = ts("This assignment has been defaulted-on.")
|
||||
end
|
||||
end
|
||||
|
||||
def generate
|
||||
# regenerate assignments using the current potential matches
|
||||
ChallengeAssignment.generate(@collection)
|
||||
flash[:notice] = ts("Beginning regeneration of assignments. This may take some time, especially if your challenge is large.")
|
||||
redirect_to collection_potential_matches_path(@collection)
|
||||
end
|
||||
|
||||
def send_out
|
||||
no_giver_count = @collection.assignments.with_request.with_no_offer.count
|
||||
no_recip_count = @collection.assignments.with_offer.with_no_request.count
|
||||
if (no_giver_count + no_recip_count) > 0
|
||||
flash[:error] = ts("Some participants still aren't assigned. Please either delete them or match them to a placeholder before sending out assignments.")
|
||||
redirect_to collection_potential_matches_path(@collection)
|
||||
else
|
||||
ChallengeAssignment.send_out(@collection)
|
||||
flash[:notice] = "Assignments are now being sent out."
|
||||
redirect_to collection_assignments_path(@collection)
|
||||
end
|
||||
end
|
||||
|
||||
def set
|
||||
# update all the assignments
|
||||
all_assignment_params = challenge_assignment_params
|
||||
|
||||
@assignments = []
|
||||
|
||||
@collection.assignments.where(id: all_assignment_params.keys).each do |assignment|
|
||||
assignment_params = all_assignment_params[assignment.id.to_s]
|
||||
@assignments << assignment unless assignment.update(assignment_params)
|
||||
end
|
||||
|
||||
ChallengeAssignment.update_placeholder_assignments!(@collection)
|
||||
if @assignments.empty?
|
||||
flash[:notice] = "Assignments updated"
|
||||
redirect_to collection_potential_matches_path(@collection)
|
||||
else
|
||||
flash[:error] = ts("These assignments could not be saved because the two participants do not match. Did you mean to write in a giver?")
|
||||
render template: "potential_matches/index"
|
||||
end
|
||||
end
|
||||
|
||||
def confirm_purge
|
||||
end
|
||||
|
||||
def purge
|
||||
ChallengeAssignment.clear!(@collection)
|
||||
@challenge.assignments_sent_at = nil
|
||||
@challenge.save
|
||||
flash[:notice] = ts("Assignments purged!")
|
||||
redirect_to collection_path(@collection)
|
||||
end
|
||||
|
||||
def update_multiple
|
||||
@errors = []
|
||||
params.each_pair do |key, val|
|
||||
action, id = key.split(/_/)
|
||||
next unless %w(approve default undefault cover).include?(action)
|
||||
assignment = @collection.assignments.find_by(id: id)
|
||||
unless assignment
|
||||
@errors << ts("Couldn't find assignment with id #{id}!")
|
||||
next
|
||||
end
|
||||
case action
|
||||
when "default"
|
||||
# default_assignment_id = y/n
|
||||
assignment.default || (@errors << ts("We couldn't default the assignment for %{offer}", offer: assignment.offer_byline))
|
||||
when "undefault"
|
||||
# undefault_[assignment_id] = y/n - if set, undefault
|
||||
assignment.defaulted_at = nil
|
||||
assignment.save || (@errors << ts("We couldn't undefault the assignment covering %{request}.", request: assignment.request_byline))
|
||||
when "approve"
|
||||
assignment.get_collection_item.approve_by_collection if assignment.get_collection_item
|
||||
when "cover"
|
||||
# cover_[assignment_id] = pinch hitter pseud
|
||||
next if val.blank? || assignment.pinch_hitter.try(:byline) == val
|
||||
pseud = Pseud.parse_byline(val)
|
||||
if pseud.nil?
|
||||
@errors << ts("We couldn't find the user %{val} to assign that to.", val: val)
|
||||
else
|
||||
assignment.cover(pseud) || (@errors << ts("We couldn't assign %{val} to cover %{request}.", val: val, request: assignment.request_byline))
|
||||
end
|
||||
end
|
||||
end
|
||||
if @errors.empty?
|
||||
flash[:notice] = "Assignment updates complete!"
|
||||
redirect_to collection_assignments_path(@collection)
|
||||
else
|
||||
flash[:error] = @errors
|
||||
redirect_to collection_assignments_path(@collection)
|
||||
end
|
||||
end
|
||||
|
||||
def default_all
|
||||
# mark all unfulfilled assignments as defaulted
|
||||
unfulfilled_assignments = ChallengeAssignment.unfulfilled_in_collection(@collection).readonly(false)
|
||||
unfulfilled_assignments.update_all defaulted_at: Time.now
|
||||
flash[:notice] = "All unfulfilled assignments marked as defaulting."
|
||||
redirect_to collection_assignments_path(@collection)
|
||||
end
|
||||
|
||||
def default
|
||||
@challenge_assignment.defaulted_at = Time.now
|
||||
@challenge_assignment.save
|
||||
|
||||
assignments_page_url = collection_assignments_url(@challenge_assignment.collection)
|
||||
|
||||
@challenge_assignment.collection.notify_maintainers_challenge_default(@challenge_assignment, assignments_page_url)
|
||||
|
||||
flash[:notice] = "We have notified the collection maintainers that you had to default on your assignment."
|
||||
redirect_to user_assignments_path(current_user)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def challenge_assignment_params
|
||||
params.slice(:challenge_assignments).permit(
|
||||
challenge_assignments: [
|
||||
:request_signup_pseud,
|
||||
:offer_signup_pseud,
|
||||
:pinch_hitter_byline
|
||||
]
|
||||
).require(:challenge_assignments)
|
||||
end
|
||||
|
||||
end
|
||||
148
app/controllers/challenge_claims_controller.rb
Executable file
148
app/controllers/challenge_claims_controller.rb
Executable file
|
|
@ -0,0 +1,148 @@
|
|||
class ChallengeClaimsController < ApplicationController
|
||||
|
||||
before_action :users_only
|
||||
before_action :load_collection, except: [:index]
|
||||
before_action :collection_owners_only, except: [:index, :show, :create, :destroy]
|
||||
before_action :load_claim_from_id, only: [:show, :destroy]
|
||||
|
||||
before_action :load_challenge, except: [:index]
|
||||
|
||||
before_action :allowed_to_destroy, only: [:destroy]
|
||||
|
||||
|
||||
# PERMISSIONS AND STATUS CHECKING
|
||||
|
||||
def load_challenge
|
||||
if @collection
|
||||
@challenge = @collection.challenge
|
||||
elsif @challenge_claim
|
||||
@challenge = @challenge_claim.collection.challenge
|
||||
end
|
||||
no_challenge and return unless @challenge
|
||||
end
|
||||
|
||||
def no_challenge
|
||||
flash[:error] = ts("What challenge did you want to work with?")
|
||||
redirect_to collection_path(@collection) rescue redirect_to '/'
|
||||
false
|
||||
end
|
||||
|
||||
def load_claim_from_id
|
||||
@challenge_claim = ChallengeClaim.find(params[:id])
|
||||
no_claim and return unless @challenge_claim
|
||||
end
|
||||
|
||||
def no_claim
|
||||
flash[:error] = ts("What claim did you want to work on?")
|
||||
if @collection
|
||||
redirect_to collection_path(@collection) rescue redirect_to '/'
|
||||
else
|
||||
redirect_to user_path(@user) rescue redirect_to '/'
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
def load_user
|
||||
@user = User.find_by(login: params[:user_id]) if params[:user_id]
|
||||
no_user and return unless @user
|
||||
end
|
||||
|
||||
def no_user
|
||||
flash[:error] = ts("What user were you trying to work with?")
|
||||
redirect_to "/" and return
|
||||
false
|
||||
end
|
||||
|
||||
def owner_only
|
||||
unless @user == @challenge_claim.claiming_user
|
||||
flash[:error] = ts("You aren't the claimer of that prompt.")
|
||||
redirect_to "/" and return false
|
||||
end
|
||||
end
|
||||
|
||||
def allowed_to_destroy
|
||||
@challenge_claim.user_allowed_to_destroy?(current_user) || not_allowed(@collection)
|
||||
end
|
||||
|
||||
|
||||
# ACTIONS
|
||||
|
||||
def index
|
||||
if !(@collection = Collection.find_by(name: params[:collection_id])).nil? && @collection.closed? && !@collection.user_is_maintainer?(current_user)
|
||||
flash[:notice] = ts("This challenge is currently closed to new posts.")
|
||||
end
|
||||
if params[:collection_id]
|
||||
return unless load_collection
|
||||
|
||||
@challenge = @collection.challenge
|
||||
not_allowed(@collection) unless user_scoped? || @challenge.user_allowed_to_see_assignments?(current_user)
|
||||
|
||||
@claims = ChallengeClaim.unposted_in_collection(@collection)
|
||||
@claims = @claims.where(claiming_user_id: current_user.id) if user_scoped?
|
||||
|
||||
# sorting
|
||||
set_sort_order
|
||||
|
||||
if params[:sort] == "claimer"
|
||||
@claims = @claims.order_by_offering_pseud(@sort_direction)
|
||||
else
|
||||
@claims = @claims.order(@sort_order)
|
||||
end
|
||||
elsif params[:user_id] && (@user = User.find_by(login: params[:user_id]))
|
||||
if current_user == @user
|
||||
@claims = @user.request_claims.order_by_date.unposted
|
||||
if params[:posted]
|
||||
@claims = @user.request_claims.order_by_date.posted
|
||||
end
|
||||
if params[:collection_id] && (@collection = Collection.find_by(name: params[:collection_id]))
|
||||
@claims = @claims.in_collection(@collection)
|
||||
end
|
||||
else
|
||||
flash[:error] = ts("You aren't allowed to see that user's claims.")
|
||||
redirect_to '/' and return
|
||||
end
|
||||
end
|
||||
@claims = @claims.paginate page: params[:page], per_page: ArchiveConfig.ITEMS_PER_PAGE
|
||||
end
|
||||
|
||||
def show
|
||||
# this is here just as a failsafe, this path should not be used
|
||||
redirect_to collection_prompt_path(@collection, @challenge_claim.request_prompt)
|
||||
end
|
||||
|
||||
def create
|
||||
# create a new claim
|
||||
prompt = @collection.prompts.find(params[:prompt_id])
|
||||
claim = prompt.request_claims.build(claiming_user: current_user)
|
||||
if claim.save
|
||||
flash[:notice] = "New claim made."
|
||||
else
|
||||
flash[:error] = "We couldn't save the new claim."
|
||||
end
|
||||
redirect_to collection_claims_path(@collection, for_user: true)
|
||||
end
|
||||
|
||||
def destroy
|
||||
redirect_path = collection_claims_path(@collection)
|
||||
flash[:notice] = ts("The claim was deleted.")
|
||||
|
||||
if @challenge_claim.claiming_user == current_user
|
||||
redirect_path = collection_claims_path(@collection, for_user: true)
|
||||
flash[:notice] = ts("Your claim was deleted.")
|
||||
end
|
||||
|
||||
begin
|
||||
@challenge_claim.destroy
|
||||
rescue
|
||||
flash.delete(:notice)
|
||||
flash[:error] = ts("We couldn't delete that right now, sorry! Please try again later.")
|
||||
end
|
||||
redirect_to redirect_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user_scoped?
|
||||
params[:for_user].to_s.casecmp?("true")
|
||||
end
|
||||
end
|
||||
41
app/controllers/challenge_requests_controller.rb
Normal file
41
app/controllers/challenge_requests_controller.rb
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
class ChallengeRequestsController < ApplicationController
|
||||
|
||||
before_action :load_collection
|
||||
before_action :check_visibility
|
||||
|
||||
def check_visibility
|
||||
unless @collection
|
||||
flash.now[:notice] = ts("Collection could not be found")
|
||||
redirect_to '/' and return
|
||||
end
|
||||
unless @collection.challenge_type == "PromptMeme" || (@collection.challenge_type == "GiftExchange" && @collection.challenge.user_allowed_to_see_requests_summary?(current_user))
|
||||
flash.now[:notice] = ts("You are not allowed to view the requests summary!")
|
||||
redirect_to collection_path(@collection) and return
|
||||
end
|
||||
end
|
||||
|
||||
def index
|
||||
@show_request_fandom_tags = @collection.challenge.request_restriction.allowed("fandom").positive?
|
||||
|
||||
# sorting
|
||||
set_sort_order
|
||||
direction = (@sort_direction.casecmp("ASC").zero? ? "ASC" : "DESC")
|
||||
|
||||
# actual content, do the efficient method unless we need the full query
|
||||
|
||||
if @sort_column == "fandom"
|
||||
query = "SELECT prompts.*, GROUP_CONCAT(tags.name) AS tagnames FROM prompts INNER JOIN set_taggings ON prompts.tag_set_id = set_taggings.tag_set_id
|
||||
INNER JOIN tags ON tags.id = set_taggings.tag_id
|
||||
WHERE prompts.type = 'Request' AND tags.type = 'Fandom' AND prompts.collection_id = " + @collection.id.to_s + " GROUP BY prompts.id ORDER BY tagnames " + @sort_direction
|
||||
@requests = Prompt.paginate_by_sql(query, page: params[:page], per_page: ArchiveConfig.ITEMS_PER_PAGE)
|
||||
elsif @sort_column == "prompter" && !@collection.prompts.where(anonymous: true).exists?
|
||||
@requests = @collection.prompts.where("type = 'Request'").
|
||||
joins(challenge_signup: :pseud).
|
||||
order("pseuds.name #{direction}").
|
||||
paginate(page: params[:page])
|
||||
else
|
||||
@requests = @collection.prompts.where("type = 'Request'").order(@sort_order).paginate(page: params[:page], per_page: ArchiveConfig.ITEMS_PER_PAGE)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
396
app/controllers/challenge_signups_controller.rb
Normal file
396
app/controllers/challenge_signups_controller.rb
Normal file
|
|
@ -0,0 +1,396 @@
|
|||
# For exporting to Excel CSV format
|
||||
require 'csv'
|
||||
|
||||
class ChallengeSignupsController < ApplicationController
|
||||
include ExportsHelper
|
||||
|
||||
before_action :users_only, except: [:summary]
|
||||
before_action :load_collection, except: [:index]
|
||||
before_action :load_challenge, except: [:index]
|
||||
before_action :load_signup_from_id, only: [:show, :edit, :update, :destroy, :confirm_delete]
|
||||
before_action :allowed_to_destroy, only: [:destroy, :confirm_delete]
|
||||
before_action :signup_owner_only, only: [:edit, :update]
|
||||
before_action :maintainer_or_signup_owner_only, only: [:show]
|
||||
before_action :check_signup_open, only: [:new, :create, :edit, :update]
|
||||
before_action :check_pseud_ownership, only: [:create, :update]
|
||||
before_action :check_signup_in_collection, only: [:show, :edit, :update, :destroy, :confirm_delete]
|
||||
|
||||
def load_challenge
|
||||
@challenge = @collection.challenge
|
||||
no_challenge and return unless @challenge
|
||||
end
|
||||
|
||||
def no_challenge
|
||||
flash[:error] = ts("What challenge did you want to sign up for?")
|
||||
redirect_to collection_path(@collection) rescue redirect_to '/'
|
||||
false
|
||||
end
|
||||
|
||||
def check_signup_open
|
||||
signup_closed and return unless (@challenge.signup_open || @collection.user_is_maintainer?(current_user))
|
||||
end
|
||||
|
||||
def signup_closed
|
||||
flash[:error] = ts("Sign-up is currently closed: please contact a moderator for help.")
|
||||
redirect_to @collection rescue redirect_to '/'
|
||||
false
|
||||
end
|
||||
|
||||
def signup_closed_owner?
|
||||
@collection.challenge_type == "GiftExchange" && !@challenge.signup_open && @collection.user_is_owner?(current_user)
|
||||
end
|
||||
|
||||
def signup_owner_only
|
||||
not_signup_owner and return unless @challenge_signup.pseud.user == current_user || signup_closed_owner?
|
||||
end
|
||||
|
||||
def maintainer_or_signup_owner_only
|
||||
not_allowed(@collection) and return unless (@challenge_signup.pseud.user == current_user || @collection.user_is_maintainer?(current_user))
|
||||
end
|
||||
|
||||
def not_signup_owner
|
||||
flash[:error] = ts("You can't edit someone else's sign-up!")
|
||||
redirect_to @collection
|
||||
false
|
||||
end
|
||||
|
||||
def allowed_to_destroy
|
||||
@challenge_signup.user_allowed_to_destroy?(current_user) || not_allowed(@collection)
|
||||
end
|
||||
|
||||
def load_signup_from_id
|
||||
@challenge_signup = ChallengeSignup.find(params[:id])
|
||||
no_signup and return unless @challenge_signup
|
||||
end
|
||||
|
||||
def no_signup
|
||||
flash[:error] = ts("What sign-up did you want to work on?")
|
||||
redirect_to collection_path(@collection) rescue redirect_to '/'
|
||||
false
|
||||
end
|
||||
|
||||
def check_pseud_ownership
|
||||
if params[:challenge_signup][:pseud_id] && (pseud = Pseud.find(params[:challenge_signup][:pseud_id]))
|
||||
# either you have to own the pseud, OR you have to be a mod editing after signups are closed and NOT changing the pseud
|
||||
unless current_user.pseuds.include?(pseud) || (@challenge_signup && @challenge_signup.pseud == pseud && signup_closed_owner?)
|
||||
flash[:error] = ts("You can't sign up with that pseud.")
|
||||
redirect_to root_path and return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def check_signup_in_collection
|
||||
unless @challenge_signup.collection_id == @collection.id
|
||||
flash[:error] = ts("Sorry, that sign-up isn't associated with that collection.")
|
||||
redirect_to @collection
|
||||
end
|
||||
end
|
||||
|
||||
#### ACTIONS
|
||||
|
||||
def index
|
||||
if params[:user_id] && (@user = User.find_by(login: params[:user_id]))
|
||||
if current_user == @user
|
||||
@challenge_signups = @user.challenge_signups.order_by_date
|
||||
render action: :index and return
|
||||
else
|
||||
flash[:error] = ts("You aren't allowed to see that user's sign-ups.")
|
||||
redirect_to '/' and return
|
||||
end
|
||||
else
|
||||
load_collection
|
||||
load_challenge if @collection
|
||||
return false unless @challenge
|
||||
end
|
||||
|
||||
# using respond_to in order to provide Excel output
|
||||
# see ExportsHelper for export_csv method
|
||||
respond_to do |format|
|
||||
format.html {
|
||||
if @challenge.user_allowed_to_see_signups?(current_user)
|
||||
@challenge_signups = @collection.signups.joins(:pseud)
|
||||
if params[:query]
|
||||
@query = params[:query]
|
||||
@challenge_signups = @challenge_signups.where("pseuds.name LIKE ?", '%' + params[:query] + '%')
|
||||
end
|
||||
@challenge_signups = @challenge_signups.order("pseuds.name").paginate(page: params[:page], per_page: ArchiveConfig.ITEMS_PER_PAGE)
|
||||
elsif params[:user_id] && (@user = User.find_by(login: params[:user_id]))
|
||||
@challenge_signups = @collection.signups.by_user(current_user)
|
||||
else
|
||||
not_allowed(@collection)
|
||||
end
|
||||
}
|
||||
format.csv {
|
||||
if (@collection.gift_exchange? && @challenge.user_allowed_to_see_signups?(current_user)) ||
|
||||
(@collection.prompt_meme? && @collection.user_is_maintainer?(current_user))
|
||||
csv_data = self.send("#{@challenge.class.name.underscore}_to_csv")
|
||||
filename = "#{@collection.name}_signups_#{Time.now.strftime('%Y-%m-%d-%H%M')}.csv"
|
||||
send_csv_data(csv_data, filename)
|
||||
else
|
||||
flash[:error] = ts("You aren't allowed to see the CSV summary.")
|
||||
redirect_to collection_path(@collection) rescue redirect_to '/' and return
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def summary
|
||||
@summary = ChallengeSignupSummary.new(@collection)
|
||||
|
||||
if @collection.signups.count < (ArchiveConfig.ANONYMOUS_THRESHOLD_COUNT/2)
|
||||
flash.now[:notice] = ts("Summary does not appear until at least %{count} sign-ups have been made!", count: ((ArchiveConfig.ANONYMOUS_THRESHOLD_COUNT/2)))
|
||||
elsif @collection.signups.count > ArchiveConfig.MAX_SIGNUPS_FOR_LIVE_SUMMARY
|
||||
# too many signups in this collection to show the summary page "live"
|
||||
modification_time = @summary.cached_time
|
||||
|
||||
# The time is always written alongside the cache, so if the time is
|
||||
# missing, then the cache must be missing as well -- and we want to
|
||||
# generate it. We also want to generate it if signups are open and it was
|
||||
# last generated more than an hour ago.
|
||||
if modification_time.nil? ||
|
||||
(@collection.challenge.signup_open? && modification_time < 1.hour.ago)
|
||||
|
||||
# Touch the cache so that we don't try to generate the summary a second
|
||||
# time on subsequent page loads.
|
||||
@summary.touch_cache
|
||||
|
||||
# Generate the cache of the summary in the background.
|
||||
@summary.enqueue_for_generation
|
||||
end
|
||||
else
|
||||
# generate it on the fly
|
||||
@tag_type = @summary.tag_type
|
||||
@summary_tags = @summary.summary
|
||||
@generated_live = true
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
unless @challenge_signup.valid?
|
||||
flash[:error] = ts("This sign-up is invalid. Please check your sign-ups for a duplicate or edit to fix any other problems.")
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
def build_prompts
|
||||
notice = ""
|
||||
@challenge.class::PROMPT_TYPES.each do |prompt_type|
|
||||
num_to_build = params["num_#{prompt_type}"] ? params["num_#{prompt_type}"].to_i : @challenge.required(prompt_type)
|
||||
if num_to_build < @challenge.required(prompt_type)
|
||||
notice += ts("You must submit at least %{required} #{prompt_type}. ", required: @challenge.required(prompt_type))
|
||||
num_to_build = @challenge.required(prompt_type)
|
||||
elsif num_to_build > @challenge.allowed(prompt_type)
|
||||
notice += ts("You can only submit up to %{allowed} #{prompt_type}. ", allowed: @challenge.allowed(prompt_type))
|
||||
num_to_build = @challenge.allowed(prompt_type)
|
||||
elsif params["num_#{prompt_type}"]
|
||||
notice += ts("Set up %{num} #{prompt_type.pluralize}. ", num: num_to_build)
|
||||
end
|
||||
num_existing = @challenge_signup.send(prompt_type).count
|
||||
num_existing.upto(num_to_build-1) do
|
||||
@challenge_signup.send(prompt_type).build
|
||||
end
|
||||
end
|
||||
unless notice.blank?
|
||||
flash[:notice] = notice
|
||||
end
|
||||
end
|
||||
|
||||
public
|
||||
def new
|
||||
if (@challenge_signup = ChallengeSignup.in_collection(@collection).by_user(current_user).first)
|
||||
flash[:notice] = ts("You are already signed up for this challenge. You can edit your sign-up below.")
|
||||
redirect_to edit_collection_signup_path(@collection, @challenge_signup)
|
||||
else
|
||||
@challenge_signup = ChallengeSignup.new
|
||||
build_prompts
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
build_prompts
|
||||
end
|
||||
|
||||
def create
|
||||
@challenge_signup = ChallengeSignup.new(challenge_signup_params)
|
||||
|
||||
@challenge_signup.pseud = current_user.default_pseud unless @challenge_signup.pseud
|
||||
@challenge_signup.collection = @collection
|
||||
# we check validity first to prevent saving tag sets if invalid
|
||||
if @challenge_signup.valid? && @challenge_signup.save
|
||||
flash[:notice] = ts('Sign-up was successfully created.')
|
||||
redirect_to collection_signup_path(@collection, @challenge_signup)
|
||||
else
|
||||
render action: :new
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @challenge_signup.update(challenge_signup_params)
|
||||
flash[:notice] = ts('Sign-up was successfully updated.')
|
||||
redirect_to collection_signup_path(@collection, @challenge_signup)
|
||||
else
|
||||
render action: :edit
|
||||
end
|
||||
end
|
||||
|
||||
def confirm_delete
|
||||
end
|
||||
|
||||
def destroy
|
||||
unless @challenge.signup_open || @collection.user_is_maintainer?(current_user)
|
||||
flash[:error] = ts("You cannot delete your sign-up after sign-ups are closed. Please contact a moderator for help.")
|
||||
else
|
||||
@challenge_signup.destroy
|
||||
flash[:notice] = ts("Challenge sign-up was deleted.")
|
||||
end
|
||||
if @collection.user_is_maintainer?(current_user) && !@collection.prompt_meme?
|
||||
redirect_to collection_signups_path(@collection)
|
||||
elsif @collection.prompt_meme?
|
||||
redirect_to collection_requests_path(@collection)
|
||||
else
|
||||
redirect_to @collection
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
protected
|
||||
|
||||
def request_to_array(type, request)
|
||||
any_types = TagSet::TAG_TYPES.select {|type| request && request.send("any_#{type}")}
|
||||
any_types.map! { |type| ts("Any %{type}", type: type.capitalize) }
|
||||
tags = request.nil? ? [] : request.tag_set.tags.map {|tag| tag.name}
|
||||
rarray = [(tags + any_types).join(", ")]
|
||||
|
||||
if @challenge.send("#{type}_restriction").optional_tags_allowed
|
||||
rarray << (request.nil? ? "" : request.optional_tag_set.tags.map {|tag| tag.name}.join(", "))
|
||||
end
|
||||
|
||||
if @challenge.send("#{type}_restriction").title_allowed
|
||||
rarray << (request.nil? ? "" : sanitize_field(request, :title))
|
||||
end
|
||||
|
||||
if @challenge.send("#{type}_restriction").description_allowed
|
||||
description = (request.nil? ? "" : sanitize_field(request, :description))
|
||||
# Didn't find a way to get Excel 2007 to accept line breaks
|
||||
# withing a field; not even when the row delimiter is set to
|
||||
# \r\n and linebreaks within the field are only \n. :-(
|
||||
#
|
||||
# Thus stripping linebreaks.
|
||||
rarray << description.gsub(/[\n\r]/, " ")
|
||||
end
|
||||
|
||||
rarray << (request.nil? ? "" : request.url) if
|
||||
@challenge.send("#{type}_restriction").url_allowed
|
||||
|
||||
return rarray
|
||||
end
|
||||
|
||||
|
||||
def gift_exchange_to_csv
|
||||
header = ["Pseud", "Email", "Sign-up URL"]
|
||||
|
||||
%w(request offer).each do |type|
|
||||
@challenge.send("#{type.pluralize}_num_allowed").times do |i|
|
||||
header << "#{type.capitalize} #{i+1} Tags"
|
||||
header << "#{type.capitalize} #{i+1} Optional Tags" if
|
||||
@challenge.send("#{type}_restriction").optional_tags_allowed
|
||||
header << "#{type.capitalize} #{i+1} Title" if
|
||||
@challenge.send("#{type}_restriction").title_allowed
|
||||
header << "#{type.capitalize} #{i+1} Description" if
|
||||
@challenge.send("#{type}_restriction").description_allowed
|
||||
header << "#{type.capitalize} #{i+1} URL" if
|
||||
@challenge.send("#{type}_restriction").url_allowed
|
||||
end
|
||||
end
|
||||
|
||||
csv_array = []
|
||||
csv_array << header
|
||||
|
||||
@collection.signups.each do |signup|
|
||||
row = [signup.pseud.name, signup.pseud.user.email,
|
||||
collection_signup_url(@collection, signup)]
|
||||
|
||||
%w(request offer).each do |type|
|
||||
@challenge.send("#{type.pluralize}_num_allowed").times do |i|
|
||||
row += request_to_array(type, signup.send(type.pluralize)[i])
|
||||
end
|
||||
end
|
||||
csv_array << row
|
||||
end
|
||||
|
||||
csv_array
|
||||
end
|
||||
|
||||
|
||||
def prompt_meme_to_csv
|
||||
header = ["Pseud", "Sign-up URL", "Tags"]
|
||||
header << "Optional Tags" if @challenge.request_restriction.optional_tags_allowed
|
||||
header << "Title" if @challenge.request_restriction.title_allowed
|
||||
header << "Description" if @challenge.request_restriction.description_allowed
|
||||
header << "URL" if @challenge.request_restriction.url_allowed
|
||||
|
||||
csv_array = []
|
||||
csv_array << header
|
||||
@collection.prompts.where(type: "Request").each do |request|
|
||||
row =
|
||||
if request.anonymous?
|
||||
["(Anonymous)", ""]
|
||||
else
|
||||
[request.challenge_signup.pseud.name,
|
||||
collection_signup_url(@collection, request.challenge_signup)]
|
||||
end
|
||||
csv_array << (row + request_to_array("request", request))
|
||||
end
|
||||
|
||||
csv_array
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def challenge_signup_params
|
||||
params.require(:challenge_signup).permit(
|
||||
:pseud_id,
|
||||
requests_attributes: nested_prompt_params,
|
||||
offers_attributes: nested_prompt_params
|
||||
)
|
||||
end
|
||||
|
||||
def nested_prompt_params
|
||||
[
|
||||
:id,
|
||||
:title,
|
||||
:url,
|
||||
:any_fandom,
|
||||
:any_character,
|
||||
:any_relationship,
|
||||
:any_freeform,
|
||||
:any_category,
|
||||
:any_rating,
|
||||
:any_archive_warning,
|
||||
:anonymous,
|
||||
:description,
|
||||
:_destroy,
|
||||
tag_set_attributes: [
|
||||
:id,
|
||||
:updated_at,
|
||||
:character_tagnames,
|
||||
:relationship_tagnames,
|
||||
:freeform_tagnames,
|
||||
:category_tagnames,
|
||||
:rating_tagnames,
|
||||
:archive_warning_tagnames,
|
||||
:fandom_tagnames,
|
||||
character_tagnames: [],
|
||||
relationship_tagnames: [],
|
||||
freeform_tagnames: [],
|
||||
category_tagnames: [],
|
||||
rating_tagnames: [],
|
||||
archive_warning_tagnames: [],
|
||||
fandom_tagnames: [],
|
||||
],
|
||||
optional_tag_set_attributes: [
|
||||
:tagnames
|
||||
]
|
||||
]
|
||||
end
|
||||
end
|
||||
68
app/controllers/challenges_controller.rb
Normal file
68
app/controllers/challenges_controller.rb
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
class ChallengesController < ApplicationController
|
||||
# This is the PARENT controller for all the challenge controller classes
|
||||
#
|
||||
# Here's how challenges work:
|
||||
#
|
||||
# For each TYPE of challenge, you use the challenge generator:
|
||||
# script/generate challenge WhatEver -- eg, script/generate challenge
|
||||
# Flashfic, script/generate challenge BigBang, etc
|
||||
# This creates:
|
||||
# - a model called Challenge::WhatEver -- eg, Challenge::Flashfic,
|
||||
# Challenge::BigBang, etc which actually implements a challenge
|
||||
# - a controller Challenge::WhatEverController --
|
||||
# Challenge::FlashficController which goes in
|
||||
# controllers/challenge/flashfic_controller.rb
|
||||
# - a views subfolder called views/challenge/whatever --
|
||||
# eg, views/challenge/flashfic/, views/challenge/bigbang/
|
||||
#
|
||||
# Example:
|
||||
# script/generate challenge Flashfic creates:
|
||||
# - controllers/challenge/flashfics_controller.rb
|
||||
# - models/challenge/flashfic.rb
|
||||
# - views/challenges/flashfics/
|
||||
# - db/migrate/create_challenge_flashfics to create challenge_flashfics table
|
||||
#
|
||||
# Setting up an instance:
|
||||
# - create the collection SGA_Flashfic
|
||||
# - mark it as a challenge collection
|
||||
# - choose "Regular Single Prompt (flashfic-style)" as the challenge type
|
||||
#
|
||||
# This feeds us into challenges/new and creates a new empty Challenge object
|
||||
# with collection -> SGA_Flashfic
|
||||
# and implementation -> Challenge::Flashfic.new
|
||||
# We are then redirected to Challenge::FlashficController new ->
|
||||
# views/challenges/flashfic/new.html.erb
|
||||
#
|
||||
# These can have lots of options specific to flashfics, including eg how often
|
||||
# prompts are posted, if they should automatically
|
||||
# be closed, whether we take user suggestions for prompts, etc.
|
||||
#
|
||||
|
||||
before_action :load_collection
|
||||
|
||||
def no_collection
|
||||
flash[:error] = t('challenge.no_collection',
|
||||
default: 'What collection did you want to work with?')
|
||||
redirect_to(request.env['HTTP_REFERER'] || root_path)
|
||||
false
|
||||
end
|
||||
|
||||
def no_challenge
|
||||
flash[:error] = t('challenges.no_challenge',
|
||||
default: 'What challenge did you want to work on?')
|
||||
redirect_to collection_path(@collection) rescue redirect_to '/'
|
||||
false
|
||||
end
|
||||
|
||||
def load_collection
|
||||
@collection ||= Collection.find_by(name: params[:collection_id]) if
|
||||
params[:collection_id]
|
||||
no_collection && return unless @collection
|
||||
end
|
||||
|
||||
def load_challenge
|
||||
@challenge = @collection.challenge
|
||||
no_challenge and return unless @challenge
|
||||
end
|
||||
|
||||
end
|
||||
285
app/controllers/chapters_controller.rb
Normal file
285
app/controllers/chapters_controller.rb
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
class ChaptersController < ApplicationController
|
||||
# only registered users and NOT admin should be able to create new chapters
|
||||
before_action :users_only, except: [:index, :show, :destroy, :confirm_delete]
|
||||
before_action :check_user_status, only: [:new, :create, :update, :update_positions]
|
||||
before_action :check_user_not_suspended, only: [:edit, :confirm_delete, :destroy]
|
||||
before_action :load_work
|
||||
# only authors of a work should be able to edit its chapters
|
||||
before_action :check_ownership, except: [:index, :show]
|
||||
before_action :check_visibility, only: [:show]
|
||||
before_action :load_chapter, only: [:show, :edit, :update, :preview, :post, :confirm_delete, :destroy]
|
||||
|
||||
cache_sweeper :feed_sweeper
|
||||
|
||||
# GET /work/:work_id/chapters
|
||||
# GET /work/:work_id/chapters.xml
|
||||
def index
|
||||
# this route is never used
|
||||
redirect_to work_path(params[:work_id])
|
||||
end
|
||||
|
||||
# GET /work/:work_id/chapters/manage
|
||||
def manage
|
||||
@chapters = @work.chapters_in_order(include_content: false,
|
||||
include_drafts: true)
|
||||
end
|
||||
|
||||
# GET /work/:work_id/chapters/:id
|
||||
# GET /work/:work_id/chapters/:id.xml
|
||||
def show
|
||||
@tag_groups = @work.tag_groups
|
||||
|
||||
redirect_to url_for(controller: :chapters, action: :show, work_id: @work.id, id: params[:selected_id]) and return if params[:selected_id]
|
||||
|
||||
@chapters = @work.chapters_in_order(
|
||||
include_content: false,
|
||||
include_drafts: (logged_in_as_admin? ||
|
||||
@work.user_is_owner_or_invited?(current_user))
|
||||
)
|
||||
|
||||
unless @chapters.include?(@chapter)
|
||||
access_denied
|
||||
return
|
||||
end
|
||||
|
||||
chapter_position = @chapters.index(@chapter)
|
||||
if @chapters.length > 1
|
||||
@previous_chapter = @chapters[chapter_position - 1] unless chapter_position.zero?
|
||||
@next_chapter = @chapters[chapter_position + 1]
|
||||
end
|
||||
|
||||
if @work.unrevealed?
|
||||
@page_title = t(".unrevealed") + t(".chapter_position", position: @chapter.position.to_s)
|
||||
else
|
||||
fandoms = @tag_groups["Fandom"]
|
||||
fandom = fandoms.empty? ? t(".unspecified_fandom") : fandoms[0].name
|
||||
title_fandom = fandoms.size > 3 ? t(".multifandom") : fandom
|
||||
author = @work.anonymous? ? t(".anonymous") : @work.pseuds.sort.collect(&:byline).join(", ")
|
||||
@page_title = get_page_title(title_fandom, author, @work.title + t(".chapter_position", position: @chapter.position.to_s))
|
||||
end
|
||||
|
||||
if params[:view_adult]
|
||||
cookies[:view_adult] = "true"
|
||||
elsif @work.adult? && !see_adult?
|
||||
render "works/_adult", layout: "application" and return
|
||||
end
|
||||
|
||||
@kudos = @work.kudos.with_user.includes(:user)
|
||||
|
||||
if current_user.respond_to?(:subscriptions)
|
||||
@subscription = current_user.subscriptions.where(subscribable_id: @work.id,
|
||||
subscribable_type: "Work").first ||
|
||||
current_user.subscriptions.build(subscribable: @work)
|
||||
end
|
||||
# update the history.
|
||||
Reading.update_or_create(@work, current_user) if current_user
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.js
|
||||
end
|
||||
end
|
||||
|
||||
# GET /work/:work_id/chapters/new
|
||||
# GET /work/:work_id/chapters/new.xml
|
||||
def new
|
||||
@chapter = @work.chapters.build(position: @work.number_of_chapters + 1)
|
||||
end
|
||||
|
||||
# GET /work/:work_id/chapters/1/edit
|
||||
def edit
|
||||
return unless params["remove"] == "me"
|
||||
|
||||
@chapter.creatorships.for_user(current_user).destroy_all
|
||||
if @work.chapters.any? { |c| current_user.is_author_of?(c) }
|
||||
flash[:notice] = ts("You have been removed as a creator from the chapter.")
|
||||
redirect_to @work
|
||||
else # remove from work if no longer co-creator on any chapter
|
||||
redirect_to edit_work_path(@work, remove: "me")
|
||||
end
|
||||
end
|
||||
|
||||
def draft_flash_message(work)
|
||||
flash[:notice] = work.posted ? t("chapters.draft_flash.posted_work") : t("chapters.draft_flash.unposted_work_html", deletion_date: view_context.date_in_zone(work.created_at + 29.days)).html_safe
|
||||
end
|
||||
|
||||
# POST /work/:work_id/chapters
|
||||
# POST /work/:work_id/chapters.xml
|
||||
def create
|
||||
if params[:cancel_button]
|
||||
redirect_back_or_default(root_path)
|
||||
return
|
||||
end
|
||||
|
||||
@chapter = @work.chapters.build(chapter_params)
|
||||
@work.wip_length = params[:chapter][:wip_length]
|
||||
|
||||
if params[:edit_button] || chapter_cannot_be_saved?
|
||||
render :new
|
||||
else # :post_without_preview or :preview
|
||||
@chapter.posted = true if params[:post_without_preview_button]
|
||||
@work.set_revised_at_by_chapter(@chapter)
|
||||
if @chapter.save && @work.save
|
||||
if @chapter.posted
|
||||
post_chapter
|
||||
redirect_to [@work, @chapter]
|
||||
else
|
||||
draft_flash_message(@work)
|
||||
redirect_to preview_work_chapter_path(@work, @chapter)
|
||||
end
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# PUT /work/:work_id/chapters/1
|
||||
# PUT /work/:work_id/chapters/1.xml
|
||||
def update
|
||||
if params[:cancel_button]
|
||||
# Not quite working yet - should send the user back to wherever they were before they hit edit
|
||||
redirect_back_or_default(root_path)
|
||||
return
|
||||
end
|
||||
|
||||
@chapter.attributes = chapter_params
|
||||
@work.wip_length = params[:chapter][:wip_length]
|
||||
|
||||
if params[:edit_button] || chapter_cannot_be_saved?
|
||||
render :edit
|
||||
elsif params[:preview_button]
|
||||
@preview_mode = true
|
||||
if @chapter.posted?
|
||||
flash[:notice] = ts("This is a preview of what this chapter will look like after your changes have been applied. You should probably read the whole thing to check for problems before posting.")
|
||||
else
|
||||
draft_flash_message(@work)
|
||||
end
|
||||
render :preview
|
||||
else
|
||||
@chapter.posted = true if params[:post_button] || params[:post_without_preview_button]
|
||||
posted_changed = @chapter.posted_changed?
|
||||
@work.set_revised_at_by_chapter(@chapter)
|
||||
if @chapter.save && @work.save
|
||||
flash[:notice] = ts("Chapter was successfully #{posted_changed ? 'posted' : 'updated'}.")
|
||||
redirect_to work_chapter_path(@work, @chapter)
|
||||
else
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update_positions
|
||||
if params[:chapters]
|
||||
@work.reorder_list(params[:chapters])
|
||||
flash[:notice] = ts("Chapter order has been successfully updated.")
|
||||
elsif params[:chapter]
|
||||
params[:chapter].each_with_index do |id, position|
|
||||
@work.chapters.update(id, position: position + 1)
|
||||
(@chapters ||= []) << Chapter.find(id)
|
||||
end
|
||||
end
|
||||
respond_to do |format|
|
||||
format.html { redirect_to(@work) and return }
|
||||
format.js { head :ok }
|
||||
end
|
||||
end
|
||||
|
||||
# GET /chapters/1/preview
|
||||
def preview
|
||||
@preview_mode = true
|
||||
end
|
||||
|
||||
# POST /chapters/1/post
|
||||
def post
|
||||
if params[:cancel_button]
|
||||
redirect_to @work
|
||||
elsif params[:edit_button]
|
||||
redirect_to [:edit, @work, @chapter]
|
||||
else
|
||||
@chapter.posted = true
|
||||
@work.set_revised_at_by_chapter(@chapter)
|
||||
if @chapter.save && @work.save
|
||||
post_chapter
|
||||
redirect_to(@work)
|
||||
else
|
||||
render :preview
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# GET /work/:work_id/chapters/1/confirm_delete
|
||||
def confirm_delete
|
||||
end
|
||||
|
||||
# DELETE /work/:work_id/chapters/1
|
||||
# DELETE /work/:work_id/chapters/1.xml
|
||||
def destroy
|
||||
if @chapter.is_only_chapter? || @chapter.only_non_draft_chapter?
|
||||
flash[:error] = t(".only_chapter")
|
||||
redirect_to(edit_work_path(@work))
|
||||
return
|
||||
end
|
||||
|
||||
was_draft = !@chapter.posted?
|
||||
if @chapter.destroy
|
||||
@work.minor_version = @work.minor_version + 1 unless was_draft
|
||||
@work.set_revised_at
|
||||
@work.save
|
||||
flash[:notice] = ts("The chapter #{was_draft ? 'draft ' : ''}was successfully deleted.")
|
||||
else
|
||||
flash[:error] = ts("Something went wrong. Please try again.")
|
||||
end
|
||||
redirect_to controller: "works", action: "show", id: @work
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Check whether we should display :new or :edit instead of previewing or
|
||||
# saving the user's changes.
|
||||
def chapter_cannot_be_saved?
|
||||
# The chapter can only be saved if the work can be saved:
|
||||
if @work.invalid?
|
||||
@work.errors.full_messages.each do |message|
|
||||
@chapter.errors.add(:base, message)
|
||||
end
|
||||
end
|
||||
|
||||
@chapter.errors.any? || @chapter.invalid?
|
||||
end
|
||||
|
||||
# fetch work these chapters belong to from db
|
||||
def load_work
|
||||
@work = params[:work_id] ? Work.find_by(id: params[:work_id]) : Chapter.find_by(id: params[:id]).try(:work)
|
||||
if @work.blank?
|
||||
flash[:error] = ts("Sorry, we couldn't find the work you were looking for.")
|
||||
redirect_to root_path and return
|
||||
end
|
||||
@check_ownership_of = @work
|
||||
@check_visibility_of = @work
|
||||
end
|
||||
|
||||
# Loads the specified chapter from the database. Redirects to the work if no
|
||||
# chapter is specified, or if the specified chapter doesn't exist.
|
||||
def load_chapter
|
||||
@chapter = @work.chapters.find_by(id: params[:id])
|
||||
|
||||
return if @chapter
|
||||
|
||||
flash[:error] = ts("Sorry, we couldn't find the chapter you were looking for.")
|
||||
redirect_to work_path(@work)
|
||||
end
|
||||
|
||||
def post_chapter
|
||||
@work.update_attribute(:posted, true) unless @work.posted
|
||||
flash[:notice] = ts("Chapter has been posted!")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def chapter_params
|
||||
params.require(:chapter).permit(:title, :position, :wip_length, :"published_at(3i)",
|
||||
:"published_at(2i)", :"published_at(1i)", :summary,
|
||||
:notes, :endnotes, :content, :published_at,
|
||||
author_attributes: [:byline, ids: [], coauthors: []])
|
||||
end
|
||||
end
|
||||
229
app/controllers/collection_items_controller.rb
Normal file
229
app/controllers/collection_items_controller.rb
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
class CollectionItemsController < ApplicationController
|
||||
before_action :load_collection
|
||||
before_action :load_user, only: [:update_multiple]
|
||||
before_action :load_collectible_item, only: [:new, :create]
|
||||
before_action :check_parent_visible, only: [:new]
|
||||
before_action :users_only, only: [:new]
|
||||
|
||||
cache_sweeper :collection_sweeper
|
||||
|
||||
def index
|
||||
|
||||
# TODO: AO3-6507 Refactor to use send instead of case statements.
|
||||
if @collection && @collection.user_is_maintainer?(current_user)
|
||||
@collection_items = @collection.collection_items.include_for_works
|
||||
@collection_items = case params[:status]
|
||||
when "approved"
|
||||
@collection_items.approved_by_both
|
||||
when "rejected_by_collection"
|
||||
@collection_items.rejected_by_collection
|
||||
when "rejected_by_user"
|
||||
@collection_items.rejected_by_user
|
||||
when "unreviewed_by_user"
|
||||
@collection_items.invited_by_collection
|
||||
else
|
||||
@collection_items.unreviewed_by_collection
|
||||
end
|
||||
elsif params[:user_id] && (@user = User.find_by(login: params[:user_id])) && @user == current_user
|
||||
@collection_items = CollectionItem.for_user(@user).includes(:collection).merge(Collection.with_attached_icon)
|
||||
@collection_items = case params[:status]
|
||||
when "approved"
|
||||
@collection_items.approved_by_both
|
||||
when "rejected_by_collection"
|
||||
@collection_items.rejected_by_collection
|
||||
when "rejected_by_user"
|
||||
@collection_items.rejected_by_user
|
||||
when "unreviewed_by_collection"
|
||||
@collection_items.approved_by_user.unreviewed_by_collection
|
||||
else
|
||||
@collection_items.unreviewed_by_user
|
||||
end
|
||||
else
|
||||
flash[:error] = ts("You don't have permission to see that, sorry!")
|
||||
redirect_to collections_path and return
|
||||
end
|
||||
|
||||
sort = "created_at DESC"
|
||||
@collection_items = @collection_items.order(sort).paginate page: params[:page], per_page: ArchiveConfig.ITEMS_PER_PAGE
|
||||
end
|
||||
|
||||
def load_collectible_item
|
||||
if params[:work_id]
|
||||
@item = Work.find(params[:work_id])
|
||||
elsif params[:bookmark_id]
|
||||
@item = Bookmark.find(params[:bookmark_id])
|
||||
end
|
||||
end
|
||||
|
||||
def check_parent_visible
|
||||
check_visibility_for(@item)
|
||||
end
|
||||
|
||||
def load_user
|
||||
unless @collection
|
||||
@user = User.find_by(login: params[:user_id])
|
||||
end
|
||||
end
|
||||
|
||||
def new
|
||||
end
|
||||
|
||||
def create
|
||||
unless params[:collection_names]
|
||||
flash[:error] = ts("What collections did you want to add?")
|
||||
redirect_to(request.env["HTTP_REFERER"] || root_path) and return
|
||||
end
|
||||
unless @item
|
||||
flash[:error] = ts("What did you want to add to a collection?")
|
||||
redirect_to(request.env["HTTP_REFERER"] || root_path) and return
|
||||
end
|
||||
if !current_user.archivist && @item.respond_to?(:allow_collection_invitation?) && !@item.allow_collection_invitation?
|
||||
flash[:error] = t(".invitation_not_sent", default: "This item could not be invited.")
|
||||
redirect_to(@item) and return
|
||||
end
|
||||
# for each collection name
|
||||
# see if it exists, is open, and isn't already one of this item's collections
|
||||
# add the collection and save
|
||||
# if there are errors, add them to errors
|
||||
new_collections = []
|
||||
invited_collections = []
|
||||
unapproved_collections = []
|
||||
errors = []
|
||||
params[:collection_names].split(',').map {|name| name.strip}.uniq.each do |collection_name|
|
||||
collection = Collection.find_by(name: collection_name)
|
||||
if !collection
|
||||
errors << ts("%{name}, because we couldn't find a collection with that name. Make sure you are using the one-word name, and not the title.", name: collection_name)
|
||||
elsif @item.collections.include?(collection)
|
||||
if @item.rejected_collections.include?(collection)
|
||||
errors << ts("%{collection_title}, because the %{object_type}'s owner has rejected the invitation.", collection_title: collection.title, object_type: @item.class.name.humanize.downcase)
|
||||
else
|
||||
errors << ts("%{collection_title}, because this item has already been submitted.", collection_title: collection.title)
|
||||
end
|
||||
elsif collection.closed? && !collection.user_is_maintainer?(User.current_user)
|
||||
errors << ts("%{collection_title} is closed to new submissions.", collection_title: collection.title)
|
||||
elsif (collection.anonymous? || collection.unrevealed?) && !current_user.is_author_of?(@item)
|
||||
errors << ts("%{collection_title}, because you don't own this item and the collection is anonymous or unrevealed.", collection_title: collection.title)
|
||||
elsif !current_user.is_author_of?(@item) && !collection.user_is_maintainer?(current_user)
|
||||
errors << ts("%{collection_title}, either you don't own this item or are not a moderator of the collection.", collection_title: collection.title)
|
||||
elsif @item.is_a?(Work) && @item.anonymous? && !current_user.is_author_of?(@item)
|
||||
errors << ts("%{collection_title}, because you don't own this item and the item is anonymous.", collection_title: collection.title)
|
||||
# add the work to a collection, and try to save it
|
||||
elsif @item.add_to_collection(collection) && @item.save(validate: false)
|
||||
# approved_by_user? and approved_by_collection? are both true.
|
||||
# This is will be true for archivists adding works to collections they maintain
|
||||
# or creators adding their works to a collection with auto-approval.
|
||||
if @item.approved_collections.include?(collection)
|
||||
new_collections << collection
|
||||
# if the current_user is a maintainer of the collection then approved_by_user must have been false (which means
|
||||
# the current_user isn't the owner of the item), then the maintainer is attempting to invite this work to
|
||||
# their collection
|
||||
elsif collection.user_is_maintainer?(current_user)
|
||||
invited_collections << collection
|
||||
# otherwise the current_user is the owner of the item and approved_by_COLLECTION was false (which means the
|
||||
# current_user isn't a collection_maintainer), so the item owner is attempting to add their work to a moderated
|
||||
# collection
|
||||
else
|
||||
unapproved_collections << collection
|
||||
end
|
||||
else
|
||||
errors << ts("Something went wrong trying to add collection %{name}, sorry!", name: collection_name)
|
||||
end
|
||||
end
|
||||
|
||||
# messages to the user
|
||||
unless errors.empty?
|
||||
flash[:error] = ts("We couldn't add your submission to the following collection(s): ") + "<br><ul><li />" + errors.join("<li />") + "</ul>"
|
||||
end
|
||||
flash[:notice] = "" unless new_collections.empty? && unapproved_collections.empty?
|
||||
unless new_collections.empty?
|
||||
flash[:notice] = ts("Added to collection(s): %{collections}.",
|
||||
collections: new_collections.collect(&:title).join(", "))
|
||||
end
|
||||
unless invited_collections.empty?
|
||||
invited_collections.each do |needs_user_approval|
|
||||
flash[:notice] ||= ""
|
||||
flash[:notice] = t(".invited_to_collections_html",
|
||||
invited_link: view_context.link_to(t(".invited"),
|
||||
collection_items_path(needs_user_approval, status: :unreviewed_by_user)),
|
||||
collection_title: needs_user_approval.title)
|
||||
end
|
||||
end
|
||||
unless unapproved_collections.empty?
|
||||
flash[:notice] ||= ""
|
||||
flash[:notice] += ts(" You have submitted your work to #{unapproved_collections.size > 1 ? "moderated collections (%{all_collections}). It will not become a part of those collections" : "the moderated collection '%{all_collections}'. It will not become a part of the collection"} until it has been approved by a moderator.", all_collections: unapproved_collections.map { |f| f.title }.join(', '))
|
||||
end
|
||||
|
||||
flash[:notice] = (flash[:notice]).html_safe unless flash[:notice].blank?
|
||||
flash[:error] = (flash[:error]).html_safe unless flash[:error].blank?
|
||||
|
||||
redirect_to(@item)
|
||||
end
|
||||
|
||||
def update_multiple
|
||||
if @collection&.user_is_maintainer?(current_user)
|
||||
update_multiple_with_params(
|
||||
allowed_items: @collection.collection_items,
|
||||
update_params: collection_update_multiple_params,
|
||||
success_path: collection_items_path(@collection)
|
||||
)
|
||||
elsif @user && @user == current_user
|
||||
update_multiple_with_params(
|
||||
allowed_items: CollectionItem.for_user(@user),
|
||||
update_params: user_update_multiple_params,
|
||||
success_path: user_collection_items_path(@user)
|
||||
)
|
||||
else
|
||||
flash[:error] = ts("You don't have permission to do that, sorry!")
|
||||
redirect_to(@collection || @user)
|
||||
end
|
||||
end
|
||||
|
||||
# The main work performed by update_multiple. Uses the passed-in parameters
|
||||
# to update, and only updates items that can be found in allowed_items (which
|
||||
# should be a relation on CollectionItems). When all items are successfully
|
||||
# updated, redirects to success_path.
|
||||
def update_multiple_with_params(allowed_items:, update_params:, success_path:)
|
||||
# Collect any failures so that we can display errors:
|
||||
@collection_items = []
|
||||
|
||||
# Make sure that the keys are integers so that we can look up the
|
||||
# parameters by ID.
|
||||
update_params.transform_keys!(&:to_i)
|
||||
|
||||
# By using where() here and updating each item individually, instead of
|
||||
# using allowed_items.update(update_params.keys, update_params.values) --
|
||||
# which uses find() under the hood -- we ensure that we'll fail silently if
|
||||
# the user tries to update an item they're not allowed to.
|
||||
allowed_items.where(id: update_params.keys).each do |item|
|
||||
item_data = update_params[item.id]
|
||||
if item_data[:remove] == "1"
|
||||
next unless item.user_allowed_to_destroy?(current_user)
|
||||
|
||||
@collection_items << item unless item.destroy
|
||||
else
|
||||
@collection_items << item unless item.update(item_data)
|
||||
end
|
||||
end
|
||||
|
||||
if @collection_items.empty?
|
||||
flash[:notice] = ts("Collection status updated!")
|
||||
redirect_to success_path
|
||||
else
|
||||
render action: "index"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user_update_multiple_params
|
||||
allowed = %i[user_approval_status remove]
|
||||
params.slice(:collection_items).permit(collection_items: allowed).
|
||||
require(:collection_items)
|
||||
end
|
||||
|
||||
def collection_update_multiple_params
|
||||
allowed = %i[collection_approval_status unrevealed anonymous remove]
|
||||
params.slice(:collection_items).permit(collection_items: allowed).
|
||||
require(:collection_items)
|
||||
end
|
||||
end
|
||||
135
app/controllers/collection_participants_controller.rb
Normal file
135
app/controllers/collection_participants_controller.rb
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
class CollectionParticipantsController < ApplicationController
|
||||
before_action :users_only
|
||||
before_action :load_collection
|
||||
before_action :load_participant, only: [:update, :destroy]
|
||||
before_action :allowed_to_promote, only: [:update]
|
||||
before_action :allowed_to_destroy, only: [:destroy]
|
||||
before_action :has_other_owners, only: [:update, :destroy]
|
||||
before_action :collection_maintainers_only, only: [:index, :add, :update]
|
||||
|
||||
cache_sweeper :collection_sweeper
|
||||
|
||||
def owners_required
|
||||
flash[:error] = t("collection_participants.validation.owners_required")
|
||||
redirect_to collection_participants_path(@collection)
|
||||
false
|
||||
end
|
||||
|
||||
def load_collection
|
||||
@collection = Collection.find_by!(name: params[:collection_id])
|
||||
end
|
||||
|
||||
def load_participant
|
||||
@participant = @collection.collection_participants.find(params[:id])
|
||||
end
|
||||
|
||||
def allowed_to_promote
|
||||
@new_role = collection_participant_params[:participant_role]
|
||||
@participant.user_allowed_to_promote?(current_user, @new_role) || not_allowed(@collection)
|
||||
end
|
||||
|
||||
def allowed_to_destroy
|
||||
@participant.user_allowed_to_destroy?(current_user) || not_allowed(@collection)
|
||||
end
|
||||
|
||||
def has_other_owners
|
||||
!@participant.is_owner? || (@collection.owners != [@participant.pseud]) || owners_required
|
||||
end
|
||||
|
||||
## ACTIONS
|
||||
|
||||
def join
|
||||
unless @collection
|
||||
flash[:error] = t('no_collection', default: "Which collection did you want to join?")
|
||||
redirect_to(request.env["HTTP_REFERER"] || root_path) and return
|
||||
end
|
||||
participants = CollectionParticipant.in_collection(@collection).for_user(current_user) unless current_user.nil?
|
||||
if participants.empty?
|
||||
@participant = CollectionParticipant.new(
|
||||
collection: @collection,
|
||||
pseud: current_user.default_pseud,
|
||||
participant_role: CollectionParticipant::NONE
|
||||
)
|
||||
@participant.save
|
||||
flash[:notice] = t('applied_to_join_collection', default: "You have applied to join %{collection}.", collection: @collection.title)
|
||||
else
|
||||
participants.each do |participant|
|
||||
if participant.is_invited?
|
||||
participant.approve_membership!
|
||||
flash[:notice] = t('collection_participants.accepted_invite', default: "You are now a member of %{collection}.", collection: @collection.title)
|
||||
redirect_to(request.env["HTTP_REFERER"] || root_path) and return
|
||||
end
|
||||
end
|
||||
|
||||
flash[:notice] = t('collection_participants.no_invitation', default: "You have already joined (or applied to) this collection.")
|
||||
end
|
||||
|
||||
redirect_to(request.env["HTTP_REFERER"] || root_path)
|
||||
end
|
||||
|
||||
def index
|
||||
@collection_participants = @collection.collection_participants.reject {|p| p.pseud.nil?}.sort_by {|participant| participant.pseud.name.downcase }
|
||||
end
|
||||
|
||||
def update
|
||||
if @participant.update(collection_participant_params)
|
||||
flash[:notice] = t('collection_participants.update_success', default: "Updated %{participant}.", participant: @participant.pseud.name)
|
||||
else
|
||||
flash[:error] = t(".failure", participant: @participant.pseud.name)
|
||||
end
|
||||
redirect_to collection_participants_path(@collection)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@participant.destroy
|
||||
flash[:notice] = t('collection_participants.destroy', default: "Removed %{participant} from collection.", participant: @participant.pseud.name)
|
||||
redirect_to(request.env["HTTP_REFERER"] || root_path)
|
||||
end
|
||||
|
||||
def add
|
||||
@participants_added = []
|
||||
@participants_invited = []
|
||||
pseud_results = Pseud.parse_bylines(params[:participants_to_invite])
|
||||
pseud_results[:pseuds].each do |pseud|
|
||||
if @collection.participants.include?(pseud)
|
||||
participant = CollectionParticipant.where(collection_id: @collection.id, pseud_id: pseud.id).first
|
||||
if participant && participant.is_none?
|
||||
@participants_added << participant if participant.approve_membership!
|
||||
end
|
||||
else
|
||||
participant = CollectionParticipant.new(collection: @collection, pseud: pseud, participant_role: CollectionParticipant::MEMBER)
|
||||
@participants_invited << participant if participant.save
|
||||
end
|
||||
end
|
||||
|
||||
if @participants_invited.empty? && @participants_added.empty?
|
||||
if pseud_results[:banned_pseuds].present?
|
||||
flash[:error] = ts("%{name} cannot participate in challenges.",
|
||||
name: pseud_results[:banned_pseuds].to_sentence
|
||||
)
|
||||
else
|
||||
flash[:error] = ts("We couldn't find anyone new by that name to add.")
|
||||
end
|
||||
else
|
||||
flash[:notice] = ""
|
||||
end
|
||||
|
||||
unless @participants_invited.empty?
|
||||
@participants_invited = @participants_invited.sort_by {|participant| participant.pseud.name.downcase }
|
||||
flash[:notice] += ts("New members invited: ") + @participants_invited.collect(&:pseud).collect(&:byline).join(', ')
|
||||
end
|
||||
|
||||
unless @participants_added.empty?
|
||||
@participants_added = @participants_added.sort_by {|participant| participant.pseud.name.downcase }
|
||||
flash[:notice] += ts("Members added: ") + @participants_added.collect(&:pseud).collect(&:byline).join(', ')
|
||||
end
|
||||
|
||||
redirect_to collection_participants_path(@collection)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def collection_participant_params
|
||||
params.require(:collection_participant).permit(:participant_role)
|
||||
end
|
||||
end
|
||||
13
app/controllers/collection_profile_controller.rb
Normal file
13
app/controllers/collection_profile_controller.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
class CollectionProfileController < ApplicationController
|
||||
|
||||
before_action :load_collection
|
||||
|
||||
def show
|
||||
unless @collection
|
||||
flash[:error] = "What collection did you want to look at?"
|
||||
redirect_to collections_path and return
|
||||
end
|
||||
@page_subtitle = t(".page_title", collection_title: @collection.title)
|
||||
end
|
||||
|
||||
end
|
||||
214
app/controllers/collections_controller.rb
Normal file
214
app/controllers/collections_controller.rb
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
class CollectionsController < ApplicationController
|
||||
before_action :users_only, only: [:new, :edit, :create, :update]
|
||||
before_action :load_collection_from_id, only: [:show, :edit, :update, :destroy, :confirm_delete]
|
||||
before_action :collection_owners_only, only: [:edit, :update, :destroy, :confirm_delete]
|
||||
before_action :check_user_status, only: [:new, :create, :edit, :update, :destroy]
|
||||
before_action :validate_challenge_type
|
||||
before_action :check_parent_visible, only: [:index]
|
||||
cache_sweeper :collection_sweeper
|
||||
|
||||
# Lazy fix to prevent passing unsafe values to eval via challenge_type
|
||||
# In both CollectionsController#create and CollectionsController#update there are a vulnerable usages of eval
|
||||
# For now just make sure the values passed to it are safe
|
||||
def validate_challenge_type
|
||||
if params[:challenge_type] and not ["", "GiftExchange", "PromptMeme"].include?(params[:challenge_type])
|
||||
return render status: :bad_request, text: "invalid challenge_type"
|
||||
end
|
||||
end
|
||||
|
||||
def load_collection_from_id
|
||||
@collection = Collection.find_by(name: params[:id])
|
||||
unless @collection
|
||||
raise ActiveRecord::RecordNotFound, "Couldn't find collection named '#{params[:id]}'"
|
||||
end
|
||||
end
|
||||
|
||||
def check_parent_visible
|
||||
return unless params[:work_id] && (@work = Work.find_by(id: params[:work_id]))
|
||||
|
||||
check_visibility_for(@work)
|
||||
end
|
||||
|
||||
def index
|
||||
if params[:work_id]
|
||||
@work = Work.find(params[:work_id])
|
||||
@collections = @work.approved_collections
|
||||
.by_title
|
||||
.for_blurb
|
||||
.paginate(page: params[:page])
|
||||
elsif params[:collection_id]
|
||||
@collection = Collection.find_by!(name: params[:collection_id])
|
||||
@collections = @collection.children
|
||||
.by_title
|
||||
.for_blurb
|
||||
.paginate(page: params[:page])
|
||||
@page_subtitle = t(".subcollections_page_title", collection_title: @collection.title)
|
||||
elsif params[:user_id]
|
||||
@user = User.find_by!(login: params[:user_id])
|
||||
@collections = @user.maintained_collections
|
||||
.by_title
|
||||
.for_blurb
|
||||
.paginate(page: params[:page])
|
||||
@page_subtitle = ts("%{username} - Collections", username: @user.login)
|
||||
else
|
||||
@sort_and_filter = true
|
||||
params[:collection_filters] ||= {}
|
||||
params[:sort_column] = "collections.created_at" if !valid_sort_column(params[:sort_column], 'collection')
|
||||
params[:sort_direction] = 'DESC' if !valid_sort_direction(params[:sort_direction])
|
||||
sort = params[:sort_column] + " " + params[:sort_direction]
|
||||
@collections = Collection.sorted_and_filtered(sort, params[:collection_filters], params[:page])
|
||||
end
|
||||
end
|
||||
|
||||
# display challenges that are currently taking signups
|
||||
def list_challenges
|
||||
@page_subtitle = "Open Challenges"
|
||||
@hide_dashboard = true
|
||||
@challenge_collections = (Collection.signup_open("GiftExchange").limit(15) + Collection.signup_open("PromptMeme").limit(15))
|
||||
end
|
||||
|
||||
def list_ge_challenges
|
||||
@page_subtitle = "Open Gift Exchange Challenges"
|
||||
@challenge_collections = Collection.signup_open("GiftExchange").limit(15)
|
||||
end
|
||||
|
||||
def list_pm_challenges
|
||||
@page_subtitle = "Open Prompt Meme Challenges"
|
||||
@challenge_collections = Collection.signup_open("PromptMeme").limit(15)
|
||||
end
|
||||
|
||||
def show
|
||||
@page_subtitle = @collection.title
|
||||
|
||||
if @collection.collection_preference.show_random? || params[:show_random]
|
||||
# show a random selection of works/bookmarks
|
||||
@works = WorkQuery.new(
|
||||
collection_ids: [@collection.id], show_restricted: is_registered_user?
|
||||
).sample(count: ArchiveConfig.NUMBER_OF_ITEMS_VISIBLE_IN_DASHBOARD)
|
||||
|
||||
@bookmarks = BookmarkQuery.new(
|
||||
collection_ids: [@collection.id], show_restricted: is_registered_user?
|
||||
).sample(count: ArchiveConfig.NUMBER_OF_ITEMS_VISIBLE_IN_DASHBOARD)
|
||||
else
|
||||
# show recent
|
||||
@works = WorkQuery.new(
|
||||
collection_ids: [@collection.id], show_restricted: is_registered_user?,
|
||||
sort_column: "revised_at",
|
||||
per_page: ArchiveConfig.NUMBER_OF_ITEMS_VISIBLE_IN_DASHBOARD
|
||||
).search_results
|
||||
|
||||
@bookmarks = BookmarkQuery.new(
|
||||
collection_ids: [@collection.id], show_restricted: is_registered_user?,
|
||||
sort_column: "created_at",
|
||||
per_page: ArchiveConfig.NUMBER_OF_ITEMS_VISIBLE_IN_DASHBOARD
|
||||
).search_results
|
||||
end
|
||||
end
|
||||
|
||||
def new
|
||||
@hide_dashboard = true
|
||||
@collection = Collection.new
|
||||
if params[:collection_id] && (@collection_parent = Collection.find_by(name: params[:collection_id]))
|
||||
@collection.parent_name = @collection_parent.name
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def create
|
||||
@hide_dashboard = true
|
||||
@collection = Collection.new(collection_params)
|
||||
|
||||
# add the owner
|
||||
owner_attributes = []
|
||||
(params[:owner_pseuds] || [current_user.default_pseud_id]).each do |pseud_id|
|
||||
pseud = Pseud.find(pseud_id)
|
||||
owner_attributes << {pseud: pseud, participant_role: CollectionParticipant::OWNER} if pseud
|
||||
end
|
||||
@collection.collection_participants.build(owner_attributes)
|
||||
|
||||
if @collection.save
|
||||
flash[:notice] = ts('Collection was successfully created.')
|
||||
unless params[:challenge_type].blank?
|
||||
if params[:challenge_type] == "PromptMeme"
|
||||
redirect_to new_collection_prompt_meme_path(@collection) and return
|
||||
elsif params[:challenge_type] == "GiftExchange"
|
||||
redirect_to new_collection_gift_exchange_path(@collection) and return
|
||||
end
|
||||
else
|
||||
redirect_to collection_path(@collection)
|
||||
end
|
||||
else
|
||||
@challenge_type = params[:challenge_type]
|
||||
render action: "new"
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @collection.update(collection_params)
|
||||
flash[:notice] = ts('Collection was successfully updated.')
|
||||
if params[:challenge_type].blank?
|
||||
if @collection.challenge
|
||||
# trying to destroy an existing challenge
|
||||
flash[:error] = ts("Note: if you want to delete an existing challenge, please do so on the challenge page.")
|
||||
end
|
||||
else
|
||||
if @collection.challenge
|
||||
if @collection.challenge.class.name != params[:challenge_type]
|
||||
flash[:error] = ts("Note: if you want to change the type of challenge, first please delete the existing challenge on the challenge page.")
|
||||
else
|
||||
if params[:challenge_type] == "PromptMeme"
|
||||
redirect_to edit_collection_prompt_meme_path(@collection) and return
|
||||
elsif params[:challenge_type] == "GiftExchange"
|
||||
redirect_to edit_collection_gift_exchange_path(@collection) and return
|
||||
end
|
||||
end
|
||||
else
|
||||
if params[:challenge_type] == "PromptMeme"
|
||||
redirect_to new_collection_prompt_meme_path(@collection) and return
|
||||
elsif params[:challenge_type] == "GiftExchange"
|
||||
redirect_to new_collection_gift_exchange_path(@collection) and return
|
||||
end
|
||||
end
|
||||
end
|
||||
redirect_to collection_path(@collection)
|
||||
else
|
||||
render action: "edit"
|
||||
end
|
||||
end
|
||||
|
||||
def confirm_delete
|
||||
end
|
||||
|
||||
def destroy
|
||||
@hide_dashboard = true
|
||||
@collection = Collection.find_by(name: params[:id])
|
||||
begin
|
||||
@collection.destroy
|
||||
flash[:notice] = ts("Collection was successfully deleted.")
|
||||
rescue
|
||||
flash[:error] = ts("We couldn't delete that right now, sorry! Please try again later.")
|
||||
end
|
||||
redirect_to(collections_path)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def collection_params
|
||||
params.require(:collection).permit(
|
||||
:name, :title, :email, :header_image_url, :description,
|
||||
:parent_name, :challenge_type, :icon, :delete_icon,
|
||||
:icon_alt_text, :icon_comment_text,
|
||||
collection_profile_attributes: [
|
||||
:id, :intro, :faq, :rules,
|
||||
:gift_notification, :assignment_notification
|
||||
],
|
||||
collection_preference_attributes: [
|
||||
:id, :moderated, :closed, :unrevealed, :anonymous,
|
||||
:gift_exchange, :show_random, :prompt_meme, :email_notify
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
end
|
||||
700
app/controllers/comments_controller.rb
Normal file
700
app/controllers/comments_controller.rb
Normal file
|
|
@ -0,0 +1,700 @@
|
|||
class CommentsController < ApplicationController
|
||||
skip_before_action :store_location, except: [:show, :index, :new]
|
||||
before_action :load_commentable,
|
||||
only: [:index, :new, :create, :edit, :update, :show_comments,
|
||||
:hide_comments, :add_comment_reply,
|
||||
:cancel_comment_reply, :delete_comment,
|
||||
:cancel_comment_delete, :unreviewed, :review_all]
|
||||
before_action :check_user_status, only: [:new, :create, :edit, :update, :destroy]
|
||||
before_action :load_comment, only: [:show, :edit, :update, :delete_comment, :destroy, :cancel_comment_edit, :cancel_comment_delete, :review, :approve, :reject, :freeze, :unfreeze, :hide, :unhide]
|
||||
before_action :check_visibility, only: [:show]
|
||||
before_action :check_if_restricted
|
||||
before_action :check_tag_wrangler_access
|
||||
before_action :check_parent_visible
|
||||
before_action :check_modify_parent,
|
||||
only: [:new, :create, :edit, :update, :add_comment_reply,
|
||||
:cancel_comment_reply, :cancel_comment_edit]
|
||||
before_action :check_pseud_ownership, only: [:create, :update]
|
||||
before_action :check_ownership, only: [:edit, :update, :cancel_comment_edit]
|
||||
before_action :check_permission_to_edit, only: [:edit, :update ]
|
||||
before_action :check_permission_to_delete, only: [:delete_comment, :destroy]
|
||||
before_action :check_guest_comment_admin_setting, only: [:new, :create, :add_comment_reply]
|
||||
before_action :check_parent_comment_permissions, only: [:new, :create, :add_comment_reply]
|
||||
before_action :check_unreviewed, only: [:add_comment_reply]
|
||||
before_action :check_frozen, only: [:new, :create, :add_comment_reply]
|
||||
before_action :check_hidden_by_admin, only: [:new, :create, :add_comment_reply]
|
||||
before_action :check_not_replying_to_spam, only: [:new, :create, :add_comment_reply]
|
||||
before_action :check_guest_replies_preference, only: [:new, :create, :add_comment_reply]
|
||||
before_action :check_permission_to_review, only: [:unreviewed]
|
||||
before_action :check_permission_to_access_single_unreviewed, only: [:show]
|
||||
before_action :check_permission_to_moderate, only: [:approve, :reject]
|
||||
before_action :check_permission_to_modify_frozen_status, only: [:freeze, :unfreeze]
|
||||
before_action :check_permission_to_modify_hidden_status, only: [:hide, :unhide]
|
||||
before_action :admin_logout_required, only: [:new, :create, :add_comment_reply]
|
||||
|
||||
include BlockHelper
|
||||
|
||||
before_action :check_blocked, only: [:new, :create, :add_comment_reply, :edit, :update]
|
||||
def check_blocked
|
||||
parent = find_parent
|
||||
|
||||
if blocked_by?(parent)
|
||||
flash[:comment_error] = t("comments.check_blocked.parent")
|
||||
redirect_to_all_comments(parent, show_comments: true)
|
||||
elsif @comment && blocked_by_comment?(@comment.commentable)
|
||||
# edit and update set @comment to the comment being edited
|
||||
flash[:comment_error] = t("comments.check_blocked.reply")
|
||||
redirect_to_all_comments(parent, show_comments: true)
|
||||
elsif @comment.nil? && blocked_by_comment?(@commentable)
|
||||
# new, create, and add_comment_reply don't set @comment, but do set @commentable
|
||||
flash[:comment_error] = t("comments.check_blocked.reply")
|
||||
redirect_to_all_comments(parent, show_comments: true)
|
||||
end
|
||||
end
|
||||
|
||||
def check_pseud_ownership
|
||||
return unless params[:comment][:pseud_id]
|
||||
pseud = Pseud.find(params[:comment][:pseud_id])
|
||||
return if pseud && current_user && current_user.pseuds.include?(pseud)
|
||||
flash[:error] = ts("You can't comment with that pseud.")
|
||||
redirect_to root_path
|
||||
end
|
||||
|
||||
def load_comment
|
||||
@comment = Comment.find(params[:id])
|
||||
@check_ownership_of = @comment
|
||||
@check_visibility_of = @comment
|
||||
end
|
||||
|
||||
def check_parent_visible
|
||||
check_visibility_for(find_parent)
|
||||
end
|
||||
|
||||
def check_modify_parent
|
||||
parent = find_parent
|
||||
# No one can create or update comments on something hidden by an admin.
|
||||
if parent.respond_to?(:hidden_by_admin) && parent.hidden_by_admin
|
||||
flash[:error] = ts("Sorry, you can't add or edit comments on a hidden work.")
|
||||
redirect_to work_path(parent)
|
||||
end
|
||||
# No one can create or update comments on unrevealed works.
|
||||
if parent.respond_to?(:in_unrevealed_collection) && parent.in_unrevealed_collection
|
||||
flash[:error] = ts("Sorry, you can't add or edit comments on an unrevealed work.")
|
||||
redirect_to work_path(parent)
|
||||
end
|
||||
end
|
||||
|
||||
def find_parent
|
||||
if @comment.present?
|
||||
@comment.ultimate_parent
|
||||
elsif @commentable.is_a?(Comment)
|
||||
@commentable.ultimate_parent
|
||||
elsif @commentable.present? && @commentable.respond_to?(:work)
|
||||
@commentable.work
|
||||
else
|
||||
@commentable
|
||||
end
|
||||
end
|
||||
|
||||
# Check to see if the ultimate_parent is a Work, and if so, if it's restricted
|
||||
def check_if_restricted
|
||||
parent = find_parent
|
||||
|
||||
return unless parent.respond_to?(:restricted) && parent.restricted? && !(logged_in? || logged_in_as_admin?)
|
||||
redirect_to new_user_session_path(restricted_commenting: true)
|
||||
end
|
||||
|
||||
# Check to see if the ultimate_parent is a Work or AdminPost, and if so, if it allows
|
||||
# comments for the current user.
|
||||
def check_parent_comment_permissions
|
||||
parent = find_parent
|
||||
if parent.is_a?(Work)
|
||||
translation_key = "work"
|
||||
elsif parent.is_a?(AdminPost)
|
||||
translation_key = "admin_post"
|
||||
else
|
||||
return
|
||||
end
|
||||
|
||||
if parent.disable_all_comments?
|
||||
flash[:error] = t("comments.commentable.permissions.#{translation_key}.disable_all")
|
||||
redirect_to parent
|
||||
elsif parent.disable_anon_comments? && !logged_in?
|
||||
flash[:error] = t("comments.commentable.permissions.#{translation_key}.disable_anon")
|
||||
redirect_to parent
|
||||
end
|
||||
end
|
||||
|
||||
def check_guest_comment_admin_setting
|
||||
admin_settings = AdminSetting.current
|
||||
|
||||
return unless admin_settings.guest_comments_off? && guest?
|
||||
|
||||
flash[:error] = t("comments.commentable.guest_comments_disabled")
|
||||
redirect_back(fallback_location: root_path)
|
||||
end
|
||||
|
||||
def check_guest_replies_preference
|
||||
return unless guest? && @commentable.respond_to?(:guest_replies_disallowed?) && @commentable.guest_replies_disallowed?
|
||||
|
||||
flash[:error] = t("comments.check_guest_replies_preference.error")
|
||||
redirect_back(fallback_location: root_path)
|
||||
end
|
||||
|
||||
def check_unreviewed
|
||||
return unless @commentable.respond_to?(:unreviewed?) && @commentable.unreviewed?
|
||||
|
||||
flash[:error] = ts("Sorry, you cannot reply to an unapproved comment.")
|
||||
redirect_to logged_in? ? root_path : new_user_session_path
|
||||
end
|
||||
|
||||
def check_frozen
|
||||
return unless @commentable.respond_to?(:iced?) && @commentable.iced?
|
||||
|
||||
flash[:error] = t("comments.check_frozen.error")
|
||||
redirect_back(fallback_location: root_path)
|
||||
end
|
||||
|
||||
def check_hidden_by_admin
|
||||
return unless @commentable.respond_to?(:hidden_by_admin?) && @commentable.hidden_by_admin?
|
||||
|
||||
flash[:error] = t("comments.check_hidden_by_admin.error")
|
||||
redirect_back(fallback_location: root_path)
|
||||
end
|
||||
|
||||
def check_not_replying_to_spam
|
||||
return unless @commentable.respond_to?(:approved?) && !@commentable.approved?
|
||||
|
||||
flash[:error] = t("comments.check_not_replying_to_spam.error")
|
||||
redirect_back(fallback_location: root_path)
|
||||
end
|
||||
|
||||
def check_permission_to_review
|
||||
parent = find_parent
|
||||
return if logged_in_as_admin? || current_user_owns?(parent)
|
||||
flash[:error] = ts("Sorry, you don't have permission to see those unreviewed comments.")
|
||||
redirect_to logged_in? ? root_path : new_user_session_path
|
||||
end
|
||||
|
||||
def check_permission_to_access_single_unreviewed
|
||||
return unless @comment.unreviewed?
|
||||
parent = find_parent
|
||||
return if logged_in_as_admin? || current_user_owns?(parent) || current_user_owns?(@comment)
|
||||
flash[:error] = ts("Sorry, that comment is currently in moderation.")
|
||||
redirect_to logged_in? ? root_path : new_user_session_path
|
||||
end
|
||||
|
||||
def check_permission_to_moderate
|
||||
parent = find_parent
|
||||
unless logged_in_as_admin? || current_user_owns?(parent)
|
||||
flash[:error] = ts("Sorry, you don't have permission to moderate that comment.")
|
||||
redirect_to(logged_in? ? root_path : new_user_session_path)
|
||||
end
|
||||
end
|
||||
|
||||
def check_tag_wrangler_access
|
||||
if @commentable.is_a?(Tag) || (@comment&.parent&.is_a?(Tag))
|
||||
logged_in_as_admin? || permit?("tag_wrangler") || access_denied
|
||||
end
|
||||
end
|
||||
|
||||
# Must be able to delete other people's comments on owned works, not just owned comments!
|
||||
def check_permission_to_delete
|
||||
access_denied(redirect: @comment) unless logged_in_as_admin? || current_user_owns?(@comment) || current_user_owns?(@comment.ultimate_parent)
|
||||
end
|
||||
|
||||
# Comments cannot be edited after they've been replied to or if they are frozen.
|
||||
def check_permission_to_edit
|
||||
if @comment&.iced?
|
||||
flash[:error] = t("comments.check_permission_to_edit.error.frozen")
|
||||
redirect_back(fallback_location: root_path)
|
||||
elsif !@comment&.count_all_comments&.zero?
|
||||
flash[:error] = ts("Comments with replies cannot be edited")
|
||||
redirect_back(fallback_location: root_path)
|
||||
end
|
||||
end
|
||||
|
||||
# Comments on works can be frozen or unfrozen by admins with proper
|
||||
# authorization or the work creator.
|
||||
# Comments on tags can be frozen or unfrozen by admins with proper
|
||||
# authorization.
|
||||
# Comments on admin posts can be frozen or unfrozen by any admin.
|
||||
def check_permission_to_modify_frozen_status
|
||||
return if permission_to_modify_frozen_status
|
||||
|
||||
# i18n-tasks-use t('comments.freeze.permission_denied')
|
||||
# i18n-tasks-use t('comments.unfreeze.permission_denied')
|
||||
flash[:error] = t("comments.#{action_name}.permission_denied")
|
||||
redirect_back(fallback_location: root_path)
|
||||
end
|
||||
|
||||
def check_permission_to_modify_hidden_status
|
||||
return if policy(@comment).can_hide_comment?
|
||||
|
||||
# i18n-tasks-use t('comments.hide.permission_denied')
|
||||
# i18n-tasks-use t('comments.unhide.permission_denied')
|
||||
flash[:error] = t("comments.#{action_name}.permission_denied")
|
||||
redirect_back(fallback_location: root_path)
|
||||
end
|
||||
|
||||
# Get the thing the user is trying to comment on
|
||||
def load_commentable
|
||||
@thread_view = false
|
||||
if params[:comment_id]
|
||||
@thread_view = true
|
||||
if params[:id]
|
||||
@commentable = Comment.find(params[:id])
|
||||
@thread_root = Comment.find(params[:comment_id])
|
||||
else
|
||||
@commentable = Comment.find(params[:comment_id])
|
||||
@thread_root = @commentable
|
||||
end
|
||||
elsif params[:chapter_id]
|
||||
@commentable = Chapter.find(params[:chapter_id])
|
||||
elsif params[:work_id]
|
||||
@commentable = Work.find(params[:work_id])
|
||||
elsif params[:admin_post_id]
|
||||
@commentable = AdminPost.find(params[:admin_post_id])
|
||||
elsif params[:tag_id]
|
||||
@commentable = Tag.find_by_name(params[:tag_id])
|
||||
@page_subtitle = @commentable.try(:name)
|
||||
end
|
||||
end
|
||||
|
||||
def index
|
||||
return raise_not_found if @commentable.blank?
|
||||
|
||||
return unless @commentable.class == Comment
|
||||
|
||||
# we link to the parent object at the top
|
||||
@commentable = @commentable.ultimate_parent
|
||||
end
|
||||
|
||||
def unreviewed
|
||||
@comments = @commentable.find_all_comments
|
||||
.unreviewed_only
|
||||
.for_display
|
||||
.page(params[:page])
|
||||
end
|
||||
|
||||
# GET /comments/1
|
||||
# GET /comments/1.xml
|
||||
def show
|
||||
@comments = CommentDecorator.wrap_comments([@comment])
|
||||
@thread_view = true
|
||||
@thread_root = @comment
|
||||
params[:comment_id] = params[:id]
|
||||
end
|
||||
|
||||
# GET /comments/new
|
||||
def new
|
||||
if @commentable.nil?
|
||||
flash[:error] = ts("What did you want to comment on?")
|
||||
redirect_back_or_default(root_path)
|
||||
else
|
||||
@comment = Comment.new
|
||||
@controller_name = params[:controller_name] if params[:controller_name]
|
||||
@name =
|
||||
case @commentable.class.name
|
||||
when /Work/
|
||||
@commentable.title
|
||||
when /Chapter/
|
||||
@commentable.work.title
|
||||
when /Tag/
|
||||
@commentable.name
|
||||
when /AdminPost/
|
||||
@commentable.title
|
||||
when /Comment/
|
||||
ts("Previous Comment")
|
||||
else
|
||||
@commentable.class.name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# GET /comments/1/edit
|
||||
def edit
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.js
|
||||
end
|
||||
end
|
||||
|
||||
# POST /comments
|
||||
# POST /comments.xml
|
||||
def create
|
||||
if @commentable.nil?
|
||||
flash[:error] = ts("What did you want to comment on?")
|
||||
redirect_back_or_default(root_path)
|
||||
else
|
||||
@comment = Comment.new(comment_params)
|
||||
@comment.ip_address = request.remote_ip
|
||||
@comment.user_agent = request.env["HTTP_USER_AGENT"]&.to(499)
|
||||
@comment.commentable = Comment.commentable_object(@commentable)
|
||||
@controller_name = params[:controller_name]
|
||||
|
||||
# First, try saving the comment
|
||||
if @comment.save
|
||||
flash[:comment_notice] = if @comment.unreviewed?
|
||||
# i18n-tasks-use t("comments.create.success.moderated.admin_post")
|
||||
# i18n-tasks-use t("comments.create.success.moderated.work")
|
||||
t("comments.create.success.moderated.#{@comment.ultimate_parent.model_name.i18n_key}")
|
||||
else
|
||||
t("comments.create.success.not_moderated")
|
||||
end
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
if request.referer&.match(/inbox/)
|
||||
redirect_to user_inbox_path(current_user, filters: filter_params[:filters], page: params[:page])
|
||||
elsif request.referer&.match(/new/) || (@comment.unreviewed? && current_user)
|
||||
# If the referer is the new comment page, go to the comment's page
|
||||
# instead of reloading the full work.
|
||||
# If the comment is unreviewed and commenter is logged in, take
|
||||
# them to the comment's page so they can access the edit and
|
||||
# delete options for the comment, since unreviewed comments don't
|
||||
# appear on the commentable.
|
||||
redirect_to comment_path(@comment)
|
||||
elsif request.referer == root_url
|
||||
# replying on the homepage
|
||||
redirect_to root_path
|
||||
elsif @comment.unreviewed?
|
||||
redirect_to_all_comments(@commentable)
|
||||
else
|
||||
redirect_to_comment(@comment, { view_full_work: (params[:view_full_work] == "true"), page: params[:page] })
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
flash[:error] = ts("Couldn't save comment!")
|
||||
render action: "new"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# PUT /comments/1
|
||||
# PUT /comments/1.xml
|
||||
def update
|
||||
updated_comment_params = comment_params.merge(edited_at: Time.current)
|
||||
if @comment.update(updated_comment_params)
|
||||
flash[:comment_notice] = ts('Comment was successfully updated.')
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
redirect_to comment_path(@comment) and return if @comment.unreviewed?
|
||||
redirect_to_comment(@comment)
|
||||
end
|
||||
format.js # updating the comment in place
|
||||
end
|
||||
else
|
||||
render action: "edit"
|
||||
end
|
||||
end
|
||||
|
||||
# DELETE /comments/1
|
||||
# DELETE /comments/1.xml
|
||||
def destroy
|
||||
authorize @comment if logged_in_as_admin?
|
||||
|
||||
parent = @comment.ultimate_parent
|
||||
parent_comment = @comment.reply_comment? ? @comment.commentable : nil
|
||||
unreviewed = @comment.unreviewed?
|
||||
|
||||
if !@comment.destroy_or_mark_deleted
|
||||
# something went wrong?
|
||||
flash[:comment_error] = ts("We couldn't delete that comment.")
|
||||
redirect_to_comment(@comment)
|
||||
elsif unreviewed
|
||||
# go back to the rest of the unreviewed comments
|
||||
flash[:notice] = ts("Comment deleted.")
|
||||
redirect_back(fallback_location: unreviewed_work_comments_path(@comment.commentable))
|
||||
elsif parent_comment
|
||||
flash[:comment_notice] = ts("Comment deleted.")
|
||||
redirect_to_comment(parent_comment)
|
||||
else
|
||||
flash[:comment_notice] = ts("Comment deleted.")
|
||||
redirect_to_all_comments(parent, {show_comments: true})
|
||||
end
|
||||
end
|
||||
|
||||
def review
|
||||
if logged_in_as_admin?
|
||||
authorize @comment
|
||||
else
|
||||
return unless current_user_owns?(@comment.ultimate_parent)
|
||||
end
|
||||
|
||||
return unless @comment&.unreviewed?
|
||||
|
||||
@comment.toggle!(:unreviewed)
|
||||
# mark associated inbox comments as read
|
||||
InboxComment.where(user_id: current_user.id, feedback_comment_id: @comment.id).update_all(read: true) unless logged_in_as_admin?
|
||||
flash[:notice] = ts("Comment approved.")
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
if params[:approved_from] == "inbox"
|
||||
redirect_to user_inbox_path(current_user, page: params[:page], filters: filter_params[:filters])
|
||||
elsif params[:approved_from] == "home"
|
||||
redirect_to root_path
|
||||
elsif @comment.ultimate_parent.is_a?(AdminPost)
|
||||
redirect_to unreviewed_admin_post_comments_path(@comment.ultimate_parent)
|
||||
else
|
||||
redirect_to unreviewed_work_comments_path(@comment.ultimate_parent)
|
||||
end
|
||||
return
|
||||
end
|
||||
format.js
|
||||
end
|
||||
end
|
||||
|
||||
def review_all
|
||||
authorize @commentable, policy_class: CommentPolicy if logged_in_as_admin?
|
||||
unless (@commentable && current_user_owns?(@commentable)) || (@commentable && logged_in_as_admin? && @commentable.is_a?(AdminPost))
|
||||
flash[:error] = ts("What did you want to review comments on?")
|
||||
redirect_back_or_default(root_path)
|
||||
return
|
||||
end
|
||||
|
||||
@comments = @commentable.find_all_comments.unreviewed_only
|
||||
@comments.each { |c| c.toggle!(:unreviewed) }
|
||||
flash[:notice] = ts("All moderated comments approved.")
|
||||
redirect_to @commentable
|
||||
end
|
||||
|
||||
def approve
|
||||
authorize @comment
|
||||
@comment.mark_as_ham!
|
||||
redirect_to_all_comments(@comment.ultimate_parent, show_comments: true)
|
||||
end
|
||||
|
||||
def reject
|
||||
authorize @comment if logged_in_as_admin?
|
||||
@comment.mark_as_spam!
|
||||
redirect_to_all_comments(@comment.ultimate_parent, show_comments: true)
|
||||
end
|
||||
|
||||
# PUT /comments/1/freeze
|
||||
def freeze
|
||||
# TODO: When AO3-5939 is fixed, we can use
|
||||
# comments = @comment.full_set
|
||||
if @comment.iced?
|
||||
flash[:comment_error] = t(".error")
|
||||
else
|
||||
comments = @comment.set_to_freeze_or_unfreeze
|
||||
Comment.mark_all_frozen!(comments)
|
||||
flash[:comment_notice] = t(".success")
|
||||
end
|
||||
|
||||
redirect_to_all_comments(@comment.ultimate_parent, show_comments: true)
|
||||
rescue StandardError
|
||||
flash[:comment_error] = t(".error")
|
||||
redirect_to_all_comments(@comment.ultimate_parent, show_comments: true)
|
||||
end
|
||||
|
||||
# PUT /comments/1/unfreeze
|
||||
def unfreeze
|
||||
# TODO: When AO3-5939 is fixed, we can use
|
||||
# comments = @comment.full_set
|
||||
if @comment.iced?
|
||||
comments = @comment.set_to_freeze_or_unfreeze
|
||||
Comment.mark_all_unfrozen!(comments)
|
||||
flash[:comment_notice] = t(".success")
|
||||
else
|
||||
flash[:comment_error] = t(".error")
|
||||
end
|
||||
|
||||
redirect_to_all_comments(@comment.ultimate_parent, show_comments: true)
|
||||
rescue StandardError
|
||||
flash[:comment_error] = t(".error")
|
||||
redirect_to_all_comments(@comment.ultimate_parent, show_comments: true)
|
||||
end
|
||||
|
||||
# PUT /comments/1/hide
|
||||
def hide
|
||||
if !@comment.hidden_by_admin?
|
||||
@comment.mark_hidden!
|
||||
AdminActivity.log_action(current_admin, @comment, action: "hide comment")
|
||||
flash[:comment_notice] = t(".success")
|
||||
else
|
||||
flash[:comment_error] = t(".error")
|
||||
end
|
||||
redirect_to_all_comments(@comment.ultimate_parent, show_comments: true)
|
||||
end
|
||||
|
||||
# PUT /comments/1/unhide
|
||||
def unhide
|
||||
if @comment.hidden_by_admin?
|
||||
@comment.mark_unhidden!
|
||||
AdminActivity.log_action(current_admin, @comment, action: "unhide comment")
|
||||
flash[:comment_notice] = t(".success")
|
||||
else
|
||||
flash[:comment_error] = t(".error")
|
||||
end
|
||||
redirect_to_all_comments(@comment.ultimate_parent, show_comments: true)
|
||||
end
|
||||
|
||||
def show_comments
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
# if non-ajax it could mean sudden javascript failure OR being redirected from login
|
||||
# so we're being extra-nice and preserving any intention to comment along with the show comments option
|
||||
options = {show_comments: true}
|
||||
options[:add_comment_reply_id] = params[:add_comment_reply_id] if params[:add_comment_reply_id]
|
||||
options[:view_full_work] = params[:view_full_work] if params[:view_full_work]
|
||||
options[:page] = params[:page]
|
||||
redirect_to_all_comments(@commentable, options)
|
||||
end
|
||||
|
||||
format.js do
|
||||
@comments = CommentDecorator.for_commentable(@commentable, page: params[:page])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def hide_comments
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
redirect_to_all_comments(@commentable)
|
||||
end
|
||||
format.js
|
||||
end
|
||||
end
|
||||
|
||||
# If JavaScript is enabled, use add_comment_reply.js to load the reply form
|
||||
# Otherwise, redirect to a comment view with the form already loaded
|
||||
def add_comment_reply
|
||||
@comment = Comment.new
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
options = {show_comments: true}
|
||||
options[:controller] = @commentable.class.to_s.underscore.pluralize
|
||||
options[:anchor] = "comment_#{params[:id]}"
|
||||
options[:page] = params[:page]
|
||||
options[:view_full_work] = params[:view_full_work]
|
||||
if @thread_view
|
||||
options[:id] = @thread_root
|
||||
options[:add_comment_reply_id] = params[:id]
|
||||
redirect_to_comment(@commentable, options)
|
||||
else
|
||||
options[:id] = @commentable.id # work, chapter or other stuff that is not a comment
|
||||
options[:add_comment_reply_id] = params[:id]
|
||||
redirect_to_all_comments(@commentable, options)
|
||||
end
|
||||
end
|
||||
format.js { @commentable = Comment.find(params[:id]) }
|
||||
end
|
||||
end
|
||||
|
||||
def cancel_comment_reply
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
options = {}
|
||||
options[:show_comments] = params[:show_comments] if params[:show_comments]
|
||||
redirect_to_all_comments(@commentable, options)
|
||||
end
|
||||
format.js { @commentable = Comment.find(params[:id]) }
|
||||
end
|
||||
end
|
||||
|
||||
def cancel_comment_edit
|
||||
respond_to do |format|
|
||||
format.html { redirect_to_comment(@comment) }
|
||||
format.js
|
||||
end
|
||||
end
|
||||
|
||||
def delete_comment
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
options = {}
|
||||
options[:show_comments] = params[:show_comments] if params[:show_comments]
|
||||
options[:delete_comment_id] = params[:id] if params[:id]
|
||||
redirect_to_comment(@comment, options) # TO DO: deleting without javascript doesn't work and it never has!
|
||||
end
|
||||
format.js
|
||||
end
|
||||
end
|
||||
|
||||
def cancel_comment_delete
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
options = {}
|
||||
options[:show_comments] = params[:show_comments] if params[:show_comments]
|
||||
redirect_to_comment(@comment, options)
|
||||
end
|
||||
format.js
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# redirect to a particular comment in a thread, going into the thread
|
||||
# if necessary to display it
|
||||
def redirect_to_comment(comment, options = {})
|
||||
if comment.depth > ArchiveConfig.COMMENT_THREAD_MAX_DEPTH
|
||||
if comment.ultimate_parent.is_a?(Tag)
|
||||
default_options = {
|
||||
controller: :comments,
|
||||
action: :show,
|
||||
id: comment.commentable.id,
|
||||
tag_id: comment.ultimate_parent.to_param,
|
||||
anchor: "comment_#{comment.id}"
|
||||
}
|
||||
else
|
||||
default_options = {
|
||||
controller: comment.commentable.class.to_s.underscore.pluralize,
|
||||
action: :show,
|
||||
id: (comment.commentable.is_a?(Tag) ? comment.commentable.to_param : comment.commentable.id),
|
||||
anchor: "comment_#{comment.id}"
|
||||
}
|
||||
end
|
||||
# display the comment's direct parent (and its associated thread)
|
||||
redirect_to(url_for(default_options.merge(options)))
|
||||
else
|
||||
# need to redirect to the specific chapter; redirect_to_all will then retrieve full work view if applicable
|
||||
redirect_to_all_comments(comment.parent, options.merge({show_comments: true, anchor: "comment_#{comment.id}"}))
|
||||
end
|
||||
end
|
||||
|
||||
def redirect_to_all_comments(commentable, options = {})
|
||||
default_options = {anchor: "comments"}
|
||||
options = default_options.merge(options)
|
||||
|
||||
if commentable.is_a?(Tag)
|
||||
redirect_to comments_path(tag_id: commentable.to_param,
|
||||
add_comment_reply_id: options[:add_comment_reply_id],
|
||||
delete_comment_id: options[:delete_comment_id],
|
||||
page: options[:page],
|
||||
anchor: options[:anchor])
|
||||
else
|
||||
if commentable.is_a?(Chapter) && (options[:view_full_work] || current_user.try(:preference).try(:view_full_works))
|
||||
commentable = commentable.work
|
||||
end
|
||||
redirect_to polymorphic_path(commentable,
|
||||
options.slice(:show_comments,
|
||||
:add_comment_reply_id,
|
||||
:delete_comment_id,
|
||||
:view_full_work,
|
||||
:anchor,
|
||||
:page))
|
||||
end
|
||||
end
|
||||
|
||||
def permission_to_modify_frozen_status
|
||||
parent = find_parent
|
||||
return true if policy(@comment).can_freeze_comment?
|
||||
return true if parent.is_a?(Work) && current_user_owns?(parent)
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def comment_params
|
||||
params.require(:comment).permit(
|
||||
:pseud_id, :comment_content, :name, :email, :edited_at
|
||||
)
|
||||
end
|
||||
|
||||
def filter_params
|
||||
params.permit!
|
||||
end
|
||||
end
|
||||
14
app/controllers/concerns/tag_wrangling.rb
Normal file
14
app/controllers/concerns/tag_wrangling.rb
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
module TagWrangling
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
# When used an around_action, record_wrangling_activity will
|
||||
# mark that some wrangling activity has occurred. If a Wrangleable
|
||||
# is saved during the action, a LastWranglingActivity will be
|
||||
# recorded. Otherwise no additional steps are taken.
|
||||
def record_wrangling_activity
|
||||
User.should_update_wrangling_activity = true
|
||||
yield
|
||||
ensure
|
||||
User.should_update_wrangling_activity = false
|
||||
end
|
||||
end
|
||||
72
app/controllers/creatorships_controller.rb
Normal file
72
app/controllers/creatorships_controller.rb
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# A controller for viewing co-creator requests -- that is, creatorships where
|
||||
# the creator hasn't yet approved it.
|
||||
class CreatorshipsController < ApplicationController
|
||||
before_action :load_user
|
||||
before_action :check_ownership
|
||||
|
||||
# Show all of the creatorships associated with the current user. Displays a
|
||||
# form where the user can select multiple creatorships and perform actions
|
||||
# (accept, reject) in bulk.
|
||||
def show
|
||||
@page_subtitle = ts("Co-Creator Requests")
|
||||
@creatorships = @creatorships.unapproved.order(id: :desc).
|
||||
paginate(page: params[:page])
|
||||
end
|
||||
|
||||
# Update the selected creatorships.
|
||||
def update
|
||||
@creatorships = @creatorships.where(id: params[:selected])
|
||||
|
||||
if params[:accept]
|
||||
accept_update
|
||||
elsif params[:reject]
|
||||
reject_update
|
||||
end
|
||||
|
||||
redirect_to user_creatorships_path(@user, page: params[:page])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# When the user presses "Accept" on the co-creator request listing, this is
|
||||
# the code that runs.
|
||||
def accept_update
|
||||
flash[:notice] = []
|
||||
|
||||
@creatorships.each do |creatorship|
|
||||
creatorship.accept!
|
||||
link = view_context.link_to(title_for_creation(creatorship.creation),
|
||||
creatorship.creation)
|
||||
flash[:notice] << ts("You are now listed as a co-creator on %{link}.",
|
||||
link: link).html_safe
|
||||
end
|
||||