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
|
||||||
|
end
|
||||||
|
|
||||||
|
# When the user presses "Reject" on the co-creator request listing, this is
|
||||||
|
# the code that runs. Note that rejection is equivalent to destroying the
|
||||||
|
# request.
|
||||||
|
def reject_update
|
||||||
|
@creatorships.each(&:destroy)
|
||||||
|
flash[:notice] = ts("Requests rejected.")
|
||||||
|
end
|
||||||
|
|
||||||
|
# A helper method used to display a nicely formatted title for a creation.
|
||||||
|
helper_method :title_for_creation
|
||||||
|
def title_for_creation(creation)
|
||||||
|
if creation.is_a?(Chapter)
|
||||||
|
"Chapter #{creation.position} of #{creation.work.title}"
|
||||||
|
else
|
||||||
|
creation.title
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Load the user, and set @creatorships equal to all co-creator requests for
|
||||||
|
# that user.
|
||||||
|
def load_user
|
||||||
|
@user = User.find_by!(login: params[:user_id])
|
||||||
|
@check_ownership_of = @user
|
||||||
|
@creatorships = Creatorship.unapproved.for_user(@user)
|
||||||
|
end
|
||||||
|
end
|
||||||
72
app/controllers/downloads_controller.rb
Normal file
72
app/controllers/downloads_controller.rb
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
class DownloadsController < ApplicationController
|
||||||
|
|
||||||
|
skip_before_action :store_location, only: :show
|
||||||
|
before_action :load_work, only: :show
|
||||||
|
before_action :check_download_posted_status, only: :show
|
||||||
|
before_action :check_download_visibility, only: :show
|
||||||
|
around_action :remove_downloads, only: :show
|
||||||
|
|
||||||
|
def show
|
||||||
|
respond_to :html, :pdf, :mobi, :epub, :azw3
|
||||||
|
@download = Download.new(@work, mime_type: request.format)
|
||||||
|
@download.generate
|
||||||
|
|
||||||
|
# Make sure we were able to generate the download.
|
||||||
|
unless @download.exists?
|
||||||
|
flash[:error] = ts("We were not able to render this work. Please try again in a little while or try another format.")
|
||||||
|
redirect_to work_path(@work)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
# Send file synchronously so we don't delete it before we have finished
|
||||||
|
# sending it
|
||||||
|
File.open(@download.file_path, 'r') do |f|
|
||||||
|
send_data f.read, filename: "#{@download.file_name}.#{@download.file_type}", type: @download.mime_type
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
# Set up the work and check revealed status
|
||||||
|
# Once a format has been created, we want nginx to be able to serve
|
||||||
|
# it directly, without going through Rails again (until the work changes).
|
||||||
|
# This means no processing per user. Consider this the "published" version.
|
||||||
|
# It can't contain unposted chapters, nor unrevealed creators, even
|
||||||
|
# if the creator is the one requesting the download.
|
||||||
|
def load_work
|
||||||
|
unless AdminSetting.current.downloads_enabled?
|
||||||
|
flash[:error] = ts("Sorry, downloads are currently disabled.")
|
||||||
|
redirect_back_or_default works_path
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
@work = Work.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
# We're currently just writing everything to tmp and feeding them through
|
||||||
|
# nginx so we don't want to keep the files around.
|
||||||
|
def remove_downloads
|
||||||
|
yield
|
||||||
|
ensure
|
||||||
|
@download.remove
|
||||||
|
end
|
||||||
|
|
||||||
|
# We can't use check_visibility because this controller doesn't have access to
|
||||||
|
# cookies on production or staging.
|
||||||
|
def check_download_visibility
|
||||||
|
return unless @work.hidden_by_admin || @work.in_unrevealed_collection?
|
||||||
|
message = if @work.hidden_by_admin
|
||||||
|
ts("Sorry, you can't download a work that has been hidden by an admin.")
|
||||||
|
else
|
||||||
|
ts("Sorry, you can't download an unrevealed work.")
|
||||||
|
end
|
||||||
|
flash[:error] = message
|
||||||
|
redirect_to work_path(@work)
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_download_posted_status
|
||||||
|
return if @work.posted
|
||||||
|
flash[:error] = ts("Sorry, you can't download a draft.")
|
||||||
|
redirect_to work_path(@work)
|
||||||
|
end
|
||||||
|
end
|
||||||
16
app/controllers/errors_controller.rb
Normal file
16
app/controllers/errors_controller.rb
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
class ErrorsController < ApplicationController
|
||||||
|
%w[403 404 422 500].each do |error_code|
|
||||||
|
define_method error_code.to_sym do
|
||||||
|
render error_code, status: error_code.to_i, formats: :html
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def auth_error
|
||||||
|
@page_subtitle = t(".browser_title")
|
||||||
|
end
|
||||||
|
|
||||||
|
def timeout_error
|
||||||
|
@page_subtitle = t(".browser_title")
|
||||||
|
render "timeout_error", status: :gateway_timeout
|
||||||
|
end
|
||||||
|
end
|
||||||
104
app/controllers/external_authors_controller.rb
Normal file
104
app/controllers/external_authors_controller.rb
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
class ExternalAuthorsController < ApplicationController
|
||||||
|
before_action :load_user
|
||||||
|
before_action :check_ownership, only: [:edit]
|
||||||
|
before_action :check_user_status, only: [:edit]
|
||||||
|
before_action :get_external_author_from_invitation, only: [:claim, :complete_claim]
|
||||||
|
before_action :users_only, only: [:complete_claim]
|
||||||
|
|
||||||
|
def load_user
|
||||||
|
@user = User.find_by(login: params[:user_id])
|
||||||
|
@check_ownership_of = @user
|
||||||
|
end
|
||||||
|
|
||||||
|
def index
|
||||||
|
if @user && current_user == @user
|
||||||
|
@external_authors = @user.external_authors
|
||||||
|
elsif logged_in? && current_user.archivist
|
||||||
|
@external_authors = ExternalCreatorship.where(archivist_id: current_user).collect(&:external_author).uniq
|
||||||
|
elsif logged_in?
|
||||||
|
redirect_to user_external_authors_path(current_user)
|
||||||
|
else
|
||||||
|
flash[:notice] = "You can't see that information."
|
||||||
|
redirect_to root_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
@external_author = ExternalAuthor.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_external_author_from_invitation
|
||||||
|
token = params[:invitation_token] || (params[:user] && params[:user][:invitation_token])
|
||||||
|
@invitation = Invitation.find_by(token: token)
|
||||||
|
unless @invitation
|
||||||
|
flash[:error] = ts("You need an invitation to do that.")
|
||||||
|
redirect_to root_path
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
@external_author = @invitation.external_author
|
||||||
|
return if @external_author
|
||||||
|
flash[:error] = ts("There are no stories to claim on this invitation. Did you want to sign up instead?")
|
||||||
|
redirect_to signup_path(@invitation.token)
|
||||||
|
end
|
||||||
|
|
||||||
|
def claim
|
||||||
|
end
|
||||||
|
|
||||||
|
def complete_claim
|
||||||
|
# go ahead and give the user the works
|
||||||
|
@external_author.claim!(current_user)
|
||||||
|
@invitation.mark_as_redeemed(current_user) if @invitation
|
||||||
|
flash[:notice] = t('external_author_claimed', default: "We have added the stories imported under %{email} to your account.", email: @external_author.email)
|
||||||
|
redirect_to user_external_authors_path(current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@invitation = Invitation.find_by(token: params[:invitation_token])
|
||||||
|
@external_author = ExternalAuthor.find(params[:id])
|
||||||
|
unless (@invitation && @invitation.external_author == @external_author) || @external_author.user == current_user
|
||||||
|
flash[:error] = "You don't have permission to do that."
|
||||||
|
redirect_to root_path
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
flash[:notice] = ""
|
||||||
|
if params[:imported_stories] == "nothing"
|
||||||
|
flash[:notice] += "Okay, we'll leave things the way they are! You can use the email link any time if you change your mind. "
|
||||||
|
elsif params[:imported_stories] == "orphan"
|
||||||
|
# orphan the works
|
||||||
|
@external_author.orphan(params[:remove_pseud])
|
||||||
|
flash[:notice] += "Your imported stories have been orphaned. Thank you for leaving them in the archive! "
|
||||||
|
elsif params[:imported_stories] == "delete"
|
||||||
|
# delete the works
|
||||||
|
@external_author.delete_works
|
||||||
|
flash[:notice] += "Your imported stories have been deleted. "
|
||||||
|
end
|
||||||
|
|
||||||
|
if @invitation &&
|
||||||
|
params[:imported_stories].present? &&
|
||||||
|
params[:imported_stories] != "nothing"
|
||||||
|
@invitation.mark_as_redeemed
|
||||||
|
end
|
||||||
|
|
||||||
|
if @external_author.update(external_author_params[:external_author] || {})
|
||||||
|
flash[:notice] += "Your preferences have been saved."
|
||||||
|
redirect_to @user ? user_external_authors_path(@user) : root_path
|
||||||
|
else
|
||||||
|
flash[:error] = "There were problems saving your preferences."
|
||||||
|
render action: "edit"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def external_author_params
|
||||||
|
params.permit(
|
||||||
|
:id, :user_id, :utf8, :_method, :authenticity_token, :invitation_token,
|
||||||
|
:imported_stories, :commit, :remove_pseud,
|
||||||
|
external_author: [
|
||||||
|
:email, :do_not_email, :do_not_import
|
||||||
|
]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
69
app/controllers/external_works_controller.rb
Normal file
69
app/controllers/external_works_controller.rb
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
class ExternalWorksController < ApplicationController
|
||||||
|
before_action :admin_only, only: [:edit, :update]
|
||||||
|
before_action :users_only, only: [:new]
|
||||||
|
before_action :check_user_status, only: [:new]
|
||||||
|
|
||||||
|
def new
|
||||||
|
@bookmarkable = ExternalWork.new
|
||||||
|
@bookmark = Bookmark.new
|
||||||
|
end
|
||||||
|
|
||||||
|
# Used with bookmark form to get an existing external work and return it via ajax
|
||||||
|
def fetch
|
||||||
|
if params[:external_work_url]
|
||||||
|
url = Addressable::URI.heuristic_parse(params[:external_work_url]).to_str
|
||||||
|
@external_work = ExternalWork.where(url: url).first
|
||||||
|
end
|
||||||
|
respond_to do |format|
|
||||||
|
format.js
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def index
|
||||||
|
if params[:show] == "duplicates"
|
||||||
|
unless logged_in_as_admin?
|
||||||
|
access_denied
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
@external_works = ExternalWork.duplicate.order("created_at DESC").paginate(page: params[:page])
|
||||||
|
else
|
||||||
|
@external_works = ExternalWork.order("created_at DESC").paginate(page: params[:page])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
@external_work = ExternalWork.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
@external_work = authorize ExternalWork.find(params[:id])
|
||||||
|
@work = @external_work
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@external_work = authorize ExternalWork.find(params[:id])
|
||||||
|
@external_work.attributes = work_params
|
||||||
|
if @external_work.update(external_work_params)
|
||||||
|
flash[:notice] = t(".successfully_updated")
|
||||||
|
redirect_to(@external_work)
|
||||||
|
else
|
||||||
|
render action: "edit"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def external_work_params
|
||||||
|
params.require(:external_work).permit(
|
||||||
|
:url, :author, :title, :summary, :language_id
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def work_params
|
||||||
|
params.require(:work).permit(
|
||||||
|
:rating_string, :fandom_string, :relationship_string, :character_string,
|
||||||
|
:freeform_string, category_strings: [], archive_warning_strings: []
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
56
app/controllers/fandoms_controller.rb
Normal file
56
app/controllers/fandoms_controller.rb
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
class FandomsController < ApplicationController
|
||||||
|
before_action :load_collection
|
||||||
|
|
||||||
|
def index
|
||||||
|
if @collection
|
||||||
|
@media = Media.canonical.by_name - [Media.find_by(name: ArchiveConfig.MEDIA_NO_TAG_NAME)] - [Media.find_by(name: ArchiveConfig.MEDIA_UNCATEGORIZED_NAME)]
|
||||||
|
@page_subtitle = t(".collection_page_title", collection_title: @collection.title)
|
||||||
|
@medium = Media.find_by_name(params[:media_id]) if params[:media_id]
|
||||||
|
@counts = SearchCounts.fandom_ids_for_collection(@collection)
|
||||||
|
@fandoms = (@medium ? @medium.fandoms : Fandom.all).where(id: @counts.keys).by_name
|
||||||
|
elsif params[:media_id]
|
||||||
|
@medium = Media.find_by_name!(params[:media_id])
|
||||||
|
@page_subtitle = @medium.name
|
||||||
|
@fandoms = if @medium == Media.uncategorized
|
||||||
|
@medium.fandoms.in_use.by_name
|
||||||
|
else
|
||||||
|
@medium.fandoms.canonical.by_name.with_count
|
||||||
|
end
|
||||||
|
else
|
||||||
|
flash[:notice] = t(".choose_media")
|
||||||
|
redirect_to media_index_path and return
|
||||||
|
end
|
||||||
|
@fandoms_by_letter = @fandoms.group_by { |f| f.sortable_name[0].upcase }
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
@fandom = Fandom.find_by_name(params[:id])
|
||||||
|
if @fandom.nil?
|
||||||
|
flash[:error] = ts("Could not find fandom named %{fandom_name}", fandom_name: params[:id])
|
||||||
|
redirect_to media_index_path and return
|
||||||
|
end
|
||||||
|
@characters = @fandom.characters.canonical.by_name
|
||||||
|
end
|
||||||
|
|
||||||
|
def unassigned
|
||||||
|
join_string = "LEFT JOIN wrangling_assignments
|
||||||
|
ON (wrangling_assignments.fandom_id = tags.id)
|
||||||
|
LEFT JOIN users
|
||||||
|
ON (users.id = wrangling_assignments.user_id)"
|
||||||
|
conditions = "canonical = 1 AND users.id IS NULL"
|
||||||
|
unless params[:media_id].blank?
|
||||||
|
@media = Media.find_by_name(params[:media_id])
|
||||||
|
if @media
|
||||||
|
join_string << " INNER JOIN common_taggings
|
||||||
|
ON (tags.id = common_taggings.common_tag_id)"
|
||||||
|
conditions << " AND common_taggings.filterable_id = #{@media.id}
|
||||||
|
AND common_taggings.filterable_type = 'Tag'"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@fandoms = Fandom.joins(join_string).
|
||||||
|
where(conditions).
|
||||||
|
order(params[:sort] == 'count' ? "count DESC" : "sortable_name ASC").
|
||||||
|
with_count.
|
||||||
|
paginate(page: params[:page], per_page: 250)
|
||||||
|
end
|
||||||
|
end
|
||||||
52
app/controllers/favorite_tags_controller.rb
Normal file
52
app/controllers/favorite_tags_controller.rb
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
class FavoriteTagsController < ApplicationController
|
||||||
|
skip_before_action :store_location, only: [:create, :destroy]
|
||||||
|
before_action :users_only
|
||||||
|
before_action :load_user
|
||||||
|
before_action :check_ownership
|
||||||
|
|
||||||
|
respond_to :html, :json
|
||||||
|
|
||||||
|
# POST /favorites_tags
|
||||||
|
def create
|
||||||
|
@favorite_tag = current_user.favorite_tags.build(favorite_tag_params)
|
||||||
|
success_message = ts("You have successfully added %{tag_name} to your favorite tags. You can find them on the <a href='#{root_path}'>Archive homepage</a>.", tag_name: @favorite_tag.tag_name)
|
||||||
|
if @favorite_tag.save
|
||||||
|
respond_to do |format|
|
||||||
|
format.html { redirect_to tag_works_path(tag_id: @favorite_tag.tag.to_param), notice: success_message.html_safe }
|
||||||
|
format.json { render json: { item_id: @favorite_tag.id, item_success_message: success_message }, status: :created }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
|
flash.keep
|
||||||
|
redirect_to tag_works_path(tag_id: @favorite_tag.tag.to_param), flash: { error: @favorite_tag.errors.full_messages }
|
||||||
|
end
|
||||||
|
format.json { render json: { errors: @favorite_tag.errors.full_messages }, status: :unprocessable_entity }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# DELETE /favorite_tags/1
|
||||||
|
def destroy
|
||||||
|
@favorite_tag = FavoriteTag.find(params[:id])
|
||||||
|
@favorite_tag.destroy
|
||||||
|
success_message = ts('You have successfully removed %{tag_name} from your favorite tags.', tag_name: @favorite_tag.tag_name)
|
||||||
|
respond_to do |format|
|
||||||
|
format.html { redirect_to tag_works_path(tag_id: @favorite_tag.tag.to_param), notice: success_message }
|
||||||
|
format.json { render json: { item_success_message: success_message }, status: :ok }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_user
|
||||||
|
@user = User.find_by(login: params[:user_id])
|
||||||
|
@check_ownership_of = @user
|
||||||
|
end
|
||||||
|
|
||||||
|
def favorite_tag_params
|
||||||
|
params.require(:favorite_tag).permit(
|
||||||
|
:tag_id
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
48
app/controllers/feedbacks_controller.rb
Normal file
48
app/controllers/feedbacks_controller.rb
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
class FeedbacksController < ApplicationController
|
||||||
|
skip_before_action :store_location
|
||||||
|
before_action :load_support_languages
|
||||||
|
|
||||||
|
def new
|
||||||
|
@admin_setting = AdminSetting.current
|
||||||
|
@feedback = Feedback.new
|
||||||
|
@feedback.referer = request.referer
|
||||||
|
if logged_in_as_admin?
|
||||||
|
@feedback.email = current_admin.email
|
||||||
|
elsif is_registered_user?
|
||||||
|
@feedback.email = current_user.email
|
||||||
|
@feedback.username = current_user.login
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@admin_setting = AdminSetting.current
|
||||||
|
@feedback = Feedback.new(feedback_params)
|
||||||
|
@feedback.rollout = @feedback.rollout_string
|
||||||
|
@feedback.user_agent = request.env["HTTP_USER_AGENT"]&.to(499)
|
||||||
|
@feedback.ip_address = request.remote_ip
|
||||||
|
@feedback.referer = nil unless @feedback.referer && ArchiveConfig.PERMITTED_HOSTS.include?(URI(@feedback.referer).host)
|
||||||
|
@feedback.site_skin = helpers.current_skin
|
||||||
|
if @feedback.save
|
||||||
|
@feedback.email_and_send
|
||||||
|
flash[:notice] = t("successfully_sent",
|
||||||
|
default: "Your message was sent to the Archive team - thank you!")
|
||||||
|
redirect_back_or_default(root_path)
|
||||||
|
else
|
||||||
|
flash[:error] = t("failure_send",
|
||||||
|
default: "Sorry, your message could not be saved - please try again!")
|
||||||
|
render action: "new"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_support_languages
|
||||||
|
@support_languages = Language.where(support_available: true).default_order
|
||||||
|
end
|
||||||
|
|
||||||
|
def feedback_params
|
||||||
|
params.require(:feedback).permit(
|
||||||
|
:comment, :email, :summary, :username, :language, :referer
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
61
app/controllers/gifts_controller.rb
Normal file
61
app/controllers/gifts_controller.rb
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
class GiftsController < ApplicationController
|
||||||
|
|
||||||
|
before_action :load_collection
|
||||||
|
|
||||||
|
def index
|
||||||
|
@user = User.find_by!(login: params[:user_id]) if params[:user_id]
|
||||||
|
@recipient_name = params[:recipient]
|
||||||
|
@page_subtitle = ts("Gifts for %{name}", name: (@user ? @user.login : @recipient_name))
|
||||||
|
unless @user || @recipient_name
|
||||||
|
flash[:error] = ts("Whose gifts did you want to see?")
|
||||||
|
redirect_to(@collection || root_path) and return
|
||||||
|
end
|
||||||
|
if @user
|
||||||
|
if current_user.nil?
|
||||||
|
@works = @user.gift_works.visible_to_all
|
||||||
|
else
|
||||||
|
if @user == current_user && params[:refused]
|
||||||
|
@works = @user.rejected_gift_works.visible_to_registered_user
|
||||||
|
else
|
||||||
|
@works = @user.gift_works.visible_to_registered_user
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
pseud = Pseud.parse_byline(@recipient_name)
|
||||||
|
if pseud
|
||||||
|
if current_user.nil?
|
||||||
|
@works = pseud.gift_works.visible_to_all
|
||||||
|
else
|
||||||
|
@works = pseud.gift_works.visible_to_registered_user
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if current_user.nil?
|
||||||
|
@works = Work.giftworks_for_recipient_name(@recipient_name).visible_to_all
|
||||||
|
else
|
||||||
|
@works = Work.giftworks_for_recipient_name(@recipient_name).visible_to_registered_user
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@works = @works.in_collection(@collection) if @collection
|
||||||
|
@works = @works.order('revised_at DESC').paginate(page: params[:page], per_page: ArchiveConfig.ITEMS_PER_PAGE)
|
||||||
|
end
|
||||||
|
|
||||||
|
def toggle_rejected
|
||||||
|
@gift = Gift.find(params[:id])
|
||||||
|
# have to have the gift, be logged in, and the owner of the gift
|
||||||
|
if @gift && current_user && @gift.user == current_user
|
||||||
|
@gift.rejected = !@gift.rejected?
|
||||||
|
@gift.save!
|
||||||
|
if @gift.rejected?
|
||||||
|
flash[:notice] = ts("This work will no longer be listed among your gifts.")
|
||||||
|
else
|
||||||
|
flash[:notice] = ts("This work will now be listed among your gifts.")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# user doesn't have permission
|
||||||
|
access_denied
|
||||||
|
return
|
||||||
|
end
|
||||||
|
redirect_to user_gifts_path(current_user) and return
|
||||||
|
end
|
||||||
|
end
|
||||||
31
app/controllers/hit_count_controller.rb
Normal file
31
app/controllers/hit_count_controller.rb
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# This controller is exclusively used to track hit counts on works.
|
||||||
|
#
|
||||||
|
# Note that we deliberately only accept JSON requests because JSON requests
|
||||||
|
# bypass a lot of the usual before_action hooks, many of which can trigger
|
||||||
|
# database queries. We want this action to avoid hitting the database if at
|
||||||
|
# all possible.
|
||||||
|
class HitCountController < ApplicationController
|
||||||
|
skip_around_action :set_current_user
|
||||||
|
|
||||||
|
skip_before_action :logout_if_not_user_credentials
|
||||||
|
|
||||||
|
skip_after_action :ensure_user_credentials,
|
||||||
|
:ensure_admin_credentials
|
||||||
|
|
||||||
|
def create
|
||||||
|
respond_to do |format|
|
||||||
|
format.json do
|
||||||
|
if ENV["REQUEST_FROM_BOT"]
|
||||||
|
head :forbidden
|
||||||
|
else
|
||||||
|
RedisHitCounter.add(
|
||||||
|
params[:work_id].to_i,
|
||||||
|
request.remote_ip
|
||||||
|
)
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
102
app/controllers/home_controller.rb
Normal file
102
app/controllers/home_controller.rb
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
class HomeController < ApplicationController
|
||||||
|
|
||||||
|
before_action :users_only, only: [:first_login_help]
|
||||||
|
skip_before_action :store_location, only: [:first_login_help, :token_dispenser]
|
||||||
|
|
||||||
|
# unicorn_test
|
||||||
|
def unicorn_test
|
||||||
|
end
|
||||||
|
|
||||||
|
def content
|
||||||
|
@page_subtitle = t(".page_title")
|
||||||
|
render action: "content", layout: "application"
|
||||||
|
end
|
||||||
|
|
||||||
|
def privacy
|
||||||
|
@page_subtitle = t(".page_title")
|
||||||
|
render action: "privacy", layout: "application"
|
||||||
|
end
|
||||||
|
|
||||||
|
# terms of service
|
||||||
|
def tos
|
||||||
|
@page_subtitle = t(".page_title")
|
||||||
|
render action: "tos", layout: "application"
|
||||||
|
end
|
||||||
|
|
||||||
|
# terms of service faq
|
||||||
|
def tos_faq
|
||||||
|
@page_subtitle = t(".page_title")
|
||||||
|
render action: "tos_faq", layout: "application"
|
||||||
|
end
|
||||||
|
|
||||||
|
# dmca policy
|
||||||
|
def dmca
|
||||||
|
render action: "dmca", layout: "application"
|
||||||
|
end
|
||||||
|
|
||||||
|
# lost cookie
|
||||||
|
def lost_cookie
|
||||||
|
render action: 'lost_cookie', layout: 'application'
|
||||||
|
end
|
||||||
|
|
||||||
|
# for updating form tokens on cached pages
|
||||||
|
def token_dispenser
|
||||||
|
respond_to do |format|
|
||||||
|
format.json { render json: { token: form_authenticity_token } }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# diversity statement
|
||||||
|
def diversity
|
||||||
|
render action: "diversity_statement", layout: "application"
|
||||||
|
end
|
||||||
|
|
||||||
|
# site map
|
||||||
|
def site_map
|
||||||
|
render action: "site_map", layout: "application"
|
||||||
|
end
|
||||||
|
|
||||||
|
# donate
|
||||||
|
def donate
|
||||||
|
@page_subtitle = t(".page_title")
|
||||||
|
render action: "donate", layout: "application"
|
||||||
|
end
|
||||||
|
|
||||||
|
# about
|
||||||
|
def about
|
||||||
|
@page_subtitle = t(".page_title")
|
||||||
|
render action: "about", layout: "application"
|
||||||
|
end
|
||||||
|
|
||||||
|
def first_login_help
|
||||||
|
render action: "first_login_help", layout: false
|
||||||
|
end
|
||||||
|
|
||||||
|
# home page itself
|
||||||
|
#def index
|
||||||
|
#@homepage = Homepage.new(@current_user)
|
||||||
|
#unless @homepage.logged_in?
|
||||||
|
#@user_count, @work_count, @fandom_count = @homepage.rounded_counts
|
||||||
|
#end
|
||||||
|
|
||||||
|
#@hide_dashboard = true
|
||||||
|
#render action: 'index', layout: 'application'
|
||||||
|
#end
|
||||||
|
#end
|
||||||
|
#commenting out old index controller so i can add in random user lol
|
||||||
|
|
||||||
|
# replace index at the bottom with this code
|
||||||
|
|
||||||
|
def index
|
||||||
|
@homepage = Homepage.new(@current_user)
|
||||||
|
@random_user = User.unscoped.order(Arel.sql("RAND()")).first
|
||||||
|
unless @homepage.logged_in?
|
||||||
|
@user_count, @work_count, @fandom_count = @homepage.rounded_counts
|
||||||
|
end
|
||||||
|
|
||||||
|
@hide_dashboard = true
|
||||||
|
render action: 'index', layout: 'application'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
75
app/controllers/inbox_controller.rb
Normal file
75
app/controllers/inbox_controller.rb
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
class InboxController < ApplicationController
|
||||||
|
include BlockHelper
|
||||||
|
|
||||||
|
before_action :load_user
|
||||||
|
before_action :check_ownership_or_admin
|
||||||
|
|
||||||
|
before_action :load_commentable, only: :reply
|
||||||
|
before_action :check_blocked, only: :reply
|
||||||
|
|
||||||
|
def load_user
|
||||||
|
@user = User.find_by(login: params[:user_id])
|
||||||
|
@check_ownership_of = @user
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
authorize InboxComment if logged_in_as_admin?
|
||||||
|
@page_subtitle = t(".page_title", user: @user.login)
|
||||||
|
@inbox_total = @user.inbox_comments.with_bad_comments_removed.count
|
||||||
|
@unread = @user.inbox_comments.with_bad_comments_removed.count_unread
|
||||||
|
@filters = filter_params[:filters] || {}
|
||||||
|
@inbox_comments = @user.inbox_comments.with_bad_comments_removed.find_by_filters(@filters).page(params[:page])
|
||||||
|
end
|
||||||
|
|
||||||
|
def reply
|
||||||
|
@comment = Comment.new
|
||||||
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
|
redirect_to comment_path(@commentable, add_comment_reply_id: @commentable.id, anchor: 'comment_' + @commentable.id.to_s)
|
||||||
|
end
|
||||||
|
format.js
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
authorize InboxComment if logged_in_as_admin?
|
||||||
|
begin
|
||||||
|
@inbox_comments = InboxComment.find(params[:inbox_comments])
|
||||||
|
if params[:read]
|
||||||
|
@inbox_comments.each { |i| i.update_attribute(:read, true) }
|
||||||
|
elsif params[:unread]
|
||||||
|
@inbox_comments.each { |i| i.update_attribute(:read, false) }
|
||||||
|
elsif params[:delete]
|
||||||
|
@inbox_comments.each { |i| i.destroy }
|
||||||
|
end
|
||||||
|
success_message = t(".success")
|
||||||
|
rescue
|
||||||
|
flash[:caution] = t(".must_select_item")
|
||||||
|
end
|
||||||
|
respond_to do |format|
|
||||||
|
format.html { redirect_to request.referer || user_inbox_path(@user, page: params[:page], filters: params[:filters]), notice: success_message }
|
||||||
|
format.json { render json: { item_success_message: success_message }, status: :ok }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Allow flexible params through, since we're not posting any data
|
||||||
|
def filter_params
|
||||||
|
params.permit!
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_commentable
|
||||||
|
@commentable = Comment.find(params[:comment_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_blocked
|
||||||
|
if blocked_by?(@commentable.ultimate_parent)
|
||||||
|
flash[:error] = t("comments.check_blocked.parent")
|
||||||
|
redirect_back(fallback_location: user_inbox_path(@user))
|
||||||
|
elsif blocked_by_comment?(@commentable)
|
||||||
|
flash[:error] = t("comments.check_blocked.reply")
|
||||||
|
redirect_back(fallback_location: user_inbox_path(@user))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
93
app/controllers/invitations_controller.rb
Normal file
93
app/controllers/invitations_controller.rb
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
class InvitationsController < ApplicationController
|
||||||
|
before_action :check_permission
|
||||||
|
before_action :admin_only, only: [:create, :destroy]
|
||||||
|
before_action :check_user_status, only: [:index, :manage, :invite_friend, :update]
|
||||||
|
before_action :load_invitation, only: [:show, :invite_friend, :update, :destroy]
|
||||||
|
before_action :check_ownership_or_admin, only: [:show, :invite_friend, :update]
|
||||||
|
|
||||||
|
def load_invitation
|
||||||
|
@invitation = Invitation.find(params[:id] || invitation_params[:id])
|
||||||
|
@check_ownership_of = @invitation
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_permission
|
||||||
|
@user = User.find_by(login: params[:user_id])
|
||||||
|
access_denied unless policy(User).can_manage_users? || @user.present? && @user == current_user
|
||||||
|
end
|
||||||
|
|
||||||
|
def index
|
||||||
|
@unsent_invitations = @user.invitations.unsent.limit(5)
|
||||||
|
end
|
||||||
|
|
||||||
|
def manage
|
||||||
|
status = params[:status]
|
||||||
|
@invitations = @user.invitations
|
||||||
|
if %w(unsent unredeemed redeemed).include?(status)
|
||||||
|
@invitations = @invitations.send(status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
end
|
||||||
|
|
||||||
|
def invite_friend
|
||||||
|
if !invitation_params[:invitee_email].blank?
|
||||||
|
@invitation.invitee_email = invitation_params[:invitee_email]
|
||||||
|
if @invitation.save
|
||||||
|
flash[:notice] = 'Invitation was successfully sent.'
|
||||||
|
redirect_to([@user, @invitation])
|
||||||
|
else
|
||||||
|
render action: "show"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
flash[:error] = "Please enter an email address."
|
||||||
|
render action: "show"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
if invitation_params[:number_of_invites].to_i > 0
|
||||||
|
invitation_params[:number_of_invites].to_i.times do
|
||||||
|
@user.invitations.create
|
||||||
|
end
|
||||||
|
end
|
||||||
|
flash[:notice] = "Invitations were successfully created."
|
||||||
|
redirect_to user_invitations_path(@user)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@invitation.attributes = invitation_params
|
||||||
|
|
||||||
|
if @invitation.invitee_email_changed? && @invitation.update(invitation_params)
|
||||||
|
flash[:notice] = 'Invitation was successfully sent.'
|
||||||
|
if logged_in_as_admin?
|
||||||
|
redirect_to find_admin_invitations_path("invitation[token]" => @invitation.token)
|
||||||
|
else
|
||||||
|
redirect_to([@user, @invitation])
|
||||||
|
end
|
||||||
|
else
|
||||||
|
flash[:error] = "Please enter an email address." if @invitation.invitee_email.blank?
|
||||||
|
render action: "show"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@user = @invitation.creator
|
||||||
|
if @invitation.destroy
|
||||||
|
flash[:notice] = "Invitation successfully destroyed"
|
||||||
|
else
|
||||||
|
flash[:error] = "Invitation was not destroyed."
|
||||||
|
end
|
||||||
|
if @user.is_a?(User)
|
||||||
|
redirect_to user_invitations_path(@user)
|
||||||
|
else
|
||||||
|
redirect_to admin_invitations_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def invitation_params
|
||||||
|
params.require(:invitation).permit(:id, :invitee_email, :number_of_invites)
|
||||||
|
end
|
||||||
|
end
|
||||||
124
app/controllers/invite_requests_controller.rb
Normal file
124
app/controllers/invite_requests_controller.rb
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
class InviteRequestsController < ApplicationController
|
||||||
|
before_action :admin_only, only: [:manage, :destroy]
|
||||||
|
|
||||||
|
# GET /invite_requests
|
||||||
|
# Set browser page title to Invitation Requests
|
||||||
|
def index
|
||||||
|
@invite_request = InviteRequest.new
|
||||||
|
@page_subtitle = t(".page_title")
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /invite_requests/1
|
||||||
|
def show
|
||||||
|
@invite_request = InviteRequest.find_by(email: params[:email])
|
||||||
|
|
||||||
|
if @invite_request.present?
|
||||||
|
@position_in_queue = @invite_request.position
|
||||||
|
else
|
||||||
|
@invitation = Invitation.unredeemed.from_queue.find_by(invitee_email: params[:email])
|
||||||
|
end
|
||||||
|
|
||||||
|
respond_to do |format|
|
||||||
|
format.html
|
||||||
|
format.js
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def resend
|
||||||
|
@invitation = Invitation.unredeemed.from_queue.find_by(invitee_email: params[:email])
|
||||||
|
|
||||||
|
if @invitation.nil?
|
||||||
|
flash[:error] = t("invite_requests.resend.not_found")
|
||||||
|
elsif !@invitation.can_resend?
|
||||||
|
flash[:error] = t("invite_requests.resend.not_yet",
|
||||||
|
count: ArchiveConfig.HOURS_BEFORE_RESEND_INVITATION)
|
||||||
|
else
|
||||||
|
@invitation.send_and_set_date(resend: true)
|
||||||
|
|
||||||
|
if @invitation.errors.any?
|
||||||
|
flash[:error] = @invitation.errors.full_messages.first
|
||||||
|
else
|
||||||
|
flash[:notice] = t("invite_requests.resend.success", email: @invitation.invitee_email)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect_to status_invite_requests_path
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /invite_requests
|
||||||
|
def create
|
||||||
|
unless AdminSetting.current.invite_from_queue_enabled?
|
||||||
|
flash[:error] = t(".queue_disabled.html",
|
||||||
|
closed_bold: helpers.tag.strong(t("invite_requests.create.queue_disabled.closed")),
|
||||||
|
news_link: helpers.link_to(t("invite_requests.create.queue_disabled.news"), admin_posts_path(tag: 143)))
|
||||||
|
redirect_to invite_requests_path
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
@invite_request = InviteRequest.new(invite_request_params)
|
||||||
|
@invite_request.ip_address = request.remote_ip
|
||||||
|
if @invite_request.save
|
||||||
|
flash[:notice] = t(".success",
|
||||||
|
date: l(@invite_request.proposed_fill_time.to_date, format: :long),
|
||||||
|
return_address: ArchiveConfig.RETURN_ADDRESS)
|
||||||
|
redirect_to invite_requests_path
|
||||||
|
else
|
||||||
|
render action: :index
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def manage
|
||||||
|
authorize(InviteRequest)
|
||||||
|
|
||||||
|
@invite_requests = InviteRequest.all
|
||||||
|
|
||||||
|
if params[:query].present?
|
||||||
|
query = "%#{params[:query]}%"
|
||||||
|
@invite_requests = InviteRequest.where(
|
||||||
|
"simplified_email LIKE ? OR ip_address LIKE ?",
|
||||||
|
query, query
|
||||||
|
)
|
||||||
|
|
||||||
|
# Keep track of the fact that this has been filtered, so the position
|
||||||
|
# will not cleanly correspond to the page that we're on and the index of
|
||||||
|
# the request on the page:
|
||||||
|
@filtered = true
|
||||||
|
end
|
||||||
|
|
||||||
|
@invite_requests = @invite_requests.order(:id).page(params[:page])
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@invite_request = InviteRequest.find(params[:id])
|
||||||
|
authorize @invite_request
|
||||||
|
|
||||||
|
if @invite_request.destroy
|
||||||
|
success_message = ts("Request for %{email} was removed from the queue.", email: @invite_request.email)
|
||||||
|
respond_to do |format|
|
||||||
|
format.html { redirect_to manage_invite_requests_path(page: params[:page], query: params[:query]), notice: success_message }
|
||||||
|
format.json { render json: { item_success_message: success_message }, status: :ok }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
error_message = ts("Request could not be removed. Please try again.")
|
||||||
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
|
flash.keep
|
||||||
|
redirect_to manage_invite_requests_path(page: params[:page], query: params[:query]), flash: { error: error_message }
|
||||||
|
end
|
||||||
|
format.json { render json: { errors: error_message }, status: :unprocessable_entity }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def status
|
||||||
|
@page_subtitle = t(".browser_title")
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def invite_request_params
|
||||||
|
params.require(:invite_request).permit(
|
||||||
|
:email, :query
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
58
app/controllers/known_issues_controller.rb
Normal file
58
app/controllers/known_issues_controller.rb
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
class KnownIssuesController < ApplicationController
|
||||||
|
before_action :admin_only, except: [:index]
|
||||||
|
|
||||||
|
# GET /known_issues
|
||||||
|
def index
|
||||||
|
@known_issues = KnownIssue.all
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /known_issues/1
|
||||||
|
def show
|
||||||
|
@known_issue = authorize KnownIssue.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /known_issues/new
|
||||||
|
def new
|
||||||
|
@known_issue = authorize KnownIssue.new
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /known_issues/1/edit
|
||||||
|
def edit
|
||||||
|
@known_issue = authorize KnownIssue.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /known_issues
|
||||||
|
def create
|
||||||
|
@known_issue = authorize KnownIssue.new(known_issue_params)
|
||||||
|
if @known_issue.save
|
||||||
|
flash[:notice] = "Known issue was successfully created."
|
||||||
|
redirect_to(@known_issue)
|
||||||
|
else
|
||||||
|
render action: "new"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# PUT /known_issues/1
|
||||||
|
def update
|
||||||
|
@known_issue = authorize KnownIssue.find(params[:id])
|
||||||
|
if @known_issue.update(known_issue_params)
|
||||||
|
flash[:notice] = "Known issue was successfully updated."
|
||||||
|
redirect_to(@known_issue)
|
||||||
|
else
|
||||||
|
render action: "edit"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# DELETE /known_issues/1
|
||||||
|
def destroy
|
||||||
|
@known_issue = authorize KnownIssue.find(params[:id])
|
||||||
|
@known_issue.destroy
|
||||||
|
redirect_to(known_issues_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def known_issue_params
|
||||||
|
params.require(:known_issue).permit(:title, :content)
|
||||||
|
end
|
||||||
|
end
|
||||||
94
app/controllers/kudos_controller.rb
Normal file
94
app/controllers/kudos_controller.rb
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
class KudosController < ApplicationController
|
||||||
|
skip_before_action :store_location
|
||||||
|
before_action :load_parent, only: [:index]
|
||||||
|
before_action :check_parent_visible, only: [:index]
|
||||||
|
before_action :admin_logout_required, only: [:create]
|
||||||
|
|
||||||
|
def load_parent
|
||||||
|
@work = Work.find(params[:work_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_parent_visible
|
||||||
|
check_visibility_for(@work)
|
||||||
|
end
|
||||||
|
|
||||||
|
def index
|
||||||
|
@kudos = @work.kudos.includes(:user).with_user
|
||||||
|
@guest_kudos_count = @work.kudos.by_guest.count
|
||||||
|
|
||||||
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
|
@kudos = @kudos.order(id: :desc).paginate(
|
||||||
|
page: params[:page],
|
||||||
|
per_page: ArchiveConfig.MAX_KUDOS_TO_SHOW
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
format.js do
|
||||||
|
@kudos = @kudos.where("id < ?", params[:before].to_i) if params[:before]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@kudo = Kudo.new(kudo_params)
|
||||||
|
if current_user.present?
|
||||||
|
@kudo.user = current_user
|
||||||
|
else
|
||||||
|
@kudo.ip_address = request.remote_ip
|
||||||
|
end
|
||||||
|
|
||||||
|
if @kudo.save
|
||||||
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
|
flash[:kudos_notice] = t(".success")
|
||||||
|
redirect_to request.referer and return
|
||||||
|
end
|
||||||
|
|
||||||
|
format.js do
|
||||||
|
@commentable = @kudo.commentable
|
||||||
|
@kudos = @commentable.kudos.with_user.includes(:user)
|
||||||
|
render :create, status: :created
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
error_message = @kudo.errors.full_messages.first
|
||||||
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
|
# If user is suspended or banned, JavaScript disabled, redirect user to dashboard with message instead.
|
||||||
|
return if check_user_status
|
||||||
|
|
||||||
|
flash[:kudos_error] = error_message
|
||||||
|
redirect_to request.referer and return
|
||||||
|
end
|
||||||
|
|
||||||
|
format.js do
|
||||||
|
render json: { error_message: error_message }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue ActiveRecord::RecordNotUnique
|
||||||
|
# Uniqueness checks at application level (Rails validations) are inherently
|
||||||
|
# prone to race conditions. If we pass Rails validations but get rejected
|
||||||
|
# by database unique indices, use the usual duplicate error message.
|
||||||
|
#
|
||||||
|
# https://api.rubyonrails.org/v5.1/classes/ActiveRecord/Validations/ClassMethods.html#method-i-validates_uniqueness_of-label-Concurrency+and+integrity
|
||||||
|
error_message = t("activerecord.errors.models.kudo.taken")
|
||||||
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
|
flash[:kudos_error] = error_message
|
||||||
|
redirect_to request.referer
|
||||||
|
end
|
||||||
|
|
||||||
|
format.js do
|
||||||
|
render json: { error_message: error_message }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def kudo_params
|
||||||
|
params.require(:kudo).permit(:commentable_id, :commentable_type)
|
||||||
|
end
|
||||||
|
end
|
||||||
53
app/controllers/languages_controller.rb
Normal file
53
app/controllers/languages_controller.rb
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
class LanguagesController < ApplicationController
|
||||||
|
def index
|
||||||
|
@languages = Language.default_order
|
||||||
|
@works_counts = Rails.cache.fetch("/v1/languages/work_counts/#{current_user.present?}", expires_in: 1.day) do
|
||||||
|
WorkQuery.new.works_per_language(@languages.count)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
@language = Language.new
|
||||||
|
authorize @language
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@language = Language.new(language_params)
|
||||||
|
authorize @language
|
||||||
|
if @language.save
|
||||||
|
flash[:notice] = t("languages.successfully_added")
|
||||||
|
redirect_to languages_path
|
||||||
|
else
|
||||||
|
render action: "new"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
@language = Language.find_by(short: params[:id])
|
||||||
|
authorize @language
|
||||||
|
return unless @language == Language.default
|
||||||
|
|
||||||
|
flash[:error] = t("languages.cannot_edit_default")
|
||||||
|
redirect_to languages_path
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@language = Language.find_by(short: params[:id])
|
||||||
|
authorize @language
|
||||||
|
|
||||||
|
if @language.update(permitted_attributes(@language))
|
||||||
|
flash[:notice] = t("languages.successfully_updated")
|
||||||
|
redirect_to languages_path
|
||||||
|
else
|
||||||
|
render action: "new"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def language_params
|
||||||
|
params.require(:language).permit(
|
||||||
|
:name, :short, :support_available, :abuse_support_available, :sortable_name
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
55
app/controllers/locales_controller.rb
Normal file
55
app/controllers/locales_controller.rb
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
class LocalesController < ApplicationController
|
||||||
|
|
||||||
|
def index
|
||||||
|
authorize Locale
|
||||||
|
|
||||||
|
@locales = Locale.default_order
|
||||||
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
@locale = Locale.new
|
||||||
|
authorize @locale
|
||||||
|
@languages = Language.default_order
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /locales/en/edit
|
||||||
|
def edit
|
||||||
|
@locale = Locale.find_by(iso: params[:id])
|
||||||
|
authorize @locale
|
||||||
|
@languages = Language.default_order
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@locale = Locale.find_by(iso: params[:id])
|
||||||
|
@locale.attributes = locale_params
|
||||||
|
authorize @locale
|
||||||
|
if @locale.save
|
||||||
|
flash[:notice] = ts('Your locale was successfully updated.')
|
||||||
|
redirect_to action: 'index', status: 303
|
||||||
|
else
|
||||||
|
@languages = Language.default_order
|
||||||
|
render action: "edit"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def create
|
||||||
|
@locale = Locale.new(locale_params)
|
||||||
|
authorize @locale
|
||||||
|
if @locale.save
|
||||||
|
flash[:notice] = t('successfully_added', default: 'Locale was successfully added.')
|
||||||
|
redirect_to locales_path
|
||||||
|
else
|
||||||
|
@languages = Language.default_order
|
||||||
|
render action: "new"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def locale_params
|
||||||
|
params.require(:locale).permit(
|
||||||
|
:name, :iso, :language_id, :email_enabled, :interface_enabled
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
25
app/controllers/media_controller.rb
Normal file
25
app/controllers/media_controller.rb
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
class MediaController < ApplicationController
|
||||||
|
before_action :load_collection
|
||||||
|
skip_before_action :store_location, only: [:show]
|
||||||
|
|
||||||
|
def index
|
||||||
|
uncategorized = Media.uncategorized
|
||||||
|
@media = Media.canonical.by_name.where.not(name: [ArchiveConfig.MEDIA_UNCATEGORIZED_NAME, ArchiveConfig.MEDIA_NO_TAG_NAME]) + [uncategorized]
|
||||||
|
@fandom_listing = {}
|
||||||
|
@media.each do |medium|
|
||||||
|
if medium == uncategorized
|
||||||
|
@fandom_listing[medium] = medium.children.in_use.by_type('Fandom').order('created_at DESC').limit(5)
|
||||||
|
else
|
||||||
|
@fandom_listing[medium] = (logged_in? || logged_in_as_admin?) ?
|
||||||
|
# was losing the select trying to do this through the parents association
|
||||||
|
Fandom.unhidden_top(5).joins(:common_taggings).where(canonical: true, common_taggings: {filterable_id: medium.id, filterable_type: 'Tag'}) :
|
||||||
|
Fandom.public_top(5).joins(:common_taggings).where(canonical: true, common_taggings: {filterable_id: medium.id, filterable_type: 'Tag'})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@page_subtitle = t(".browser_title")
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
redirect_to media_fandoms_path(media_id: params[:id])
|
||||||
|
end
|
||||||
|
end
|
||||||
23
app/controllers/menu_controller.rb
Normal file
23
app/controllers/menu_controller.rb
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
class MenuController < ApplicationController
|
||||||
|
|
||||||
|
# about menu
|
||||||
|
def about
|
||||||
|
render action: "about", layout: "application"
|
||||||
|
end
|
||||||
|
|
||||||
|
# browse menu
|
||||||
|
def browse
|
||||||
|
render action: "browse", layout: "application"
|
||||||
|
end
|
||||||
|
|
||||||
|
# fandoms menu
|
||||||
|
def fandoms
|
||||||
|
render action: "fandoms", layout: "application"
|
||||||
|
end
|
||||||
|
|
||||||
|
# search menu
|
||||||
|
def search
|
||||||
|
render action: "search", layout: "application"
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
89
app/controllers/muted/users_controller.rb
Normal file
89
app/controllers/muted/users_controller.rb
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
module Muted
|
||||||
|
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_mute, only: [:confirm_mute, :create]
|
||||||
|
before_action :set_mute, only: [:confirm_unmute, :destroy]
|
||||||
|
|
||||||
|
# GET /users/:user_id/muted/users
|
||||||
|
def index
|
||||||
|
@mutes = @user.mutes_as_muter
|
||||||
|
.joins(:muted)
|
||||||
|
.includes(muted: [: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 = @mutes.map { |b| b.muted.default_pseud }
|
||||||
|
@rec_counts = Pseud.rec_counts_for_pseuds(@pseuds)
|
||||||
|
@work_counts = Pseud.work_counts_for_pseuds(@pseuds)
|
||||||
|
|
||||||
|
@page_subtitle = t(".title")
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /users/:user_id/muted/users/confirm_mute
|
||||||
|
def confirm_mute
|
||||||
|
@hide_dashboard = true
|
||||||
|
|
||||||
|
return if @mute.valid?
|
||||||
|
|
||||||
|
# We can't mute this user for whatever reason
|
||||||
|
flash[:error] = @mute.errors.full_messages.first
|
||||||
|
redirect_to user_muted_users_path(@user)
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /users/:user_id/muted/users/confirm_unmute
|
||||||
|
def confirm_unmute
|
||||||
|
@hide_dashboard = true
|
||||||
|
|
||||||
|
@muted = @mute.muted
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /users/:user_id/muted/users
|
||||||
|
def create
|
||||||
|
if @mute.save
|
||||||
|
flash[:notice] = t(".muted", name: @mute.muted.login)
|
||||||
|
else
|
||||||
|
# We can't mute this user for whatever reason
|
||||||
|
flash[:error] = @mute.errors.full_messages.first
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect_to user_muted_users_path(@user)
|
||||||
|
end
|
||||||
|
|
||||||
|
# DELETE /users/:user_id/muted/users/:id
|
||||||
|
def destroy
|
||||||
|
@mute.destroy
|
||||||
|
flash[:notice] = t(".unmuted", name: @mute.muted.login)
|
||||||
|
redirect_to user_muted_users_path(@user)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Sets the user whose mutes 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 mute matching the desired params:
|
||||||
|
def build_mute
|
||||||
|
muted_byline = params.fetch(:muted_id, "")
|
||||||
|
@mute = @user.mutes_as_muter.build(muted_byline: muted_byline)
|
||||||
|
@muted = @mute.muted
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_mute
|
||||||
|
@mute = @user.mutes_as_muter.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_admin_permissions
|
||||||
|
authorize Mute if logged_in_as_admin?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
78
app/controllers/opendoors/external_authors_controller.rb
Normal file
78
app/controllers/opendoors/external_authors_controller.rb
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
class Opendoors::ExternalAuthorsController < ApplicationController
|
||||||
|
|
||||||
|
before_action :users_only
|
||||||
|
before_action :opendoors_only
|
||||||
|
before_action :load_external_author, only: [:show, :forward]
|
||||||
|
|
||||||
|
def load_external_author
|
||||||
|
@external_author = ExternalAuthor.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def index
|
||||||
|
if params[:query]
|
||||||
|
@query = params[:query]
|
||||||
|
sql_query = '%' + @query +'%'
|
||||||
|
@external_authors = ExternalAuthor.where("external_authors.email LIKE ?", sql_query)
|
||||||
|
else
|
||||||
|
@external_authors = ExternalAuthor.unclaimed
|
||||||
|
end
|
||||||
|
# list in reverse order
|
||||||
|
@external_authors = @external_authors.order("created_at DESC").paginate(page: params[:page])
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
@external_author = ExternalAuthor.new
|
||||||
|
end
|
||||||
|
|
||||||
|
# create an external author identity (and pre-emptively block it)
|
||||||
|
def create
|
||||||
|
@external_author = ExternalAuthor.new(external_author_params)
|
||||||
|
unless @external_author.save
|
||||||
|
flash[:error] = ts("We couldn't save that address.")
|
||||||
|
else
|
||||||
|
flash[:notice] = ts("We have saved and blocked the email address %{email}", email: @external_author.email)
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect_to opendoors_tools_path
|
||||||
|
end
|
||||||
|
|
||||||
|
def forward
|
||||||
|
if @external_author.is_claimed
|
||||||
|
flash[:error] = ts("This external author has already been claimed!")
|
||||||
|
redirect_to opendoors_external_author_path(@external_author) and return
|
||||||
|
end
|
||||||
|
|
||||||
|
# get the invitation
|
||||||
|
@invitation = Invitation.where(external_author_id: @external_author.id).first
|
||||||
|
|
||||||
|
unless @invitation
|
||||||
|
# if there is no invite we create one
|
||||||
|
@invitation = Invitation.new(external_author: @external_author)
|
||||||
|
end
|
||||||
|
|
||||||
|
# send the invitation to specified address
|
||||||
|
@email = params[:email]
|
||||||
|
@invitation.invitee_email = @email
|
||||||
|
@invitation.creator = User.find_by(login: "open_doors") || current_user
|
||||||
|
if @invitation.save
|
||||||
|
flash[:notice] = ts("Claim invitation for %{author_email} has been forwarded to %{invitee_email}!", author_email: @external_author.email, invitee_email: @invitation.invitee_email)
|
||||||
|
else
|
||||||
|
flash[:error] = ts("We couldn't forward the claim for %{author_email} to that email address.", author_email: @external_author.email) + @invitation.errors.full_messages.join(", ")
|
||||||
|
end
|
||||||
|
|
||||||
|
# redirect to external author listing for that user
|
||||||
|
redirect_to opendoors_external_authors_path(query: @external_author.email)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def external_author_params
|
||||||
|
params.require(:external_author).permit(
|
||||||
|
:email, :do_not_email, :do_not_import
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
61
app/controllers/opendoors/tools_controller.rb
Normal file
61
app/controllers/opendoors/tools_controller.rb
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
class Opendoors::ToolsController < ApplicationController
|
||||||
|
|
||||||
|
before_action :users_only
|
||||||
|
before_action :opendoors_only
|
||||||
|
|
||||||
|
def index
|
||||||
|
@imported_from_url = params[:imported_from_url]
|
||||||
|
@external_author = ExternalAuthor.new
|
||||||
|
end
|
||||||
|
|
||||||
|
# Update the imported_from_url value on an existing AO3 work
|
||||||
|
# This is not RESTful but is IMO a better idea than setting up a works controller under the opendoors namespace,
|
||||||
|
# since the functionality we want to provide is so limited.
|
||||||
|
def url_update
|
||||||
|
|
||||||
|
# extract the work id and find the work
|
||||||
|
if params[:work_url] && params[:work_url].match(/works\/([0-9]+)\/?$/)
|
||||||
|
work_id = $1
|
||||||
|
@work = Work.find_by_id(work_id)
|
||||||
|
end
|
||||||
|
unless @work
|
||||||
|
flash[:error] = ts("We couldn't find that work on the Archive. Have you put in the full URL?")
|
||||||
|
redirect_to action: :index and return
|
||||||
|
end
|
||||||
|
|
||||||
|
# check validity of the new redirecting url
|
||||||
|
unless params[:imported_from_url].blank?
|
||||||
|
# try to parse the original entered url
|
||||||
|
begin
|
||||||
|
URI.parse(params[:imported_from_url])
|
||||||
|
@imported_from_url = params[:imported_from_url]
|
||||||
|
rescue
|
||||||
|
end
|
||||||
|
|
||||||
|
# if that didn't work, try to encode the URL and then parse it
|
||||||
|
if @imported_from_url.blank?
|
||||||
|
begin
|
||||||
|
URI.parse(URI::Parser.new.escape(params[:imported_from_url]))
|
||||||
|
@imported_from_url = URI::Parser.new.escape(params[:imported_from_url])
|
||||||
|
rescue
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if @imported_from_url.blank?
|
||||||
|
flash[:error] = ts("The imported-from url you are trying to set doesn't seem valid.")
|
||||||
|
else
|
||||||
|
# check for any other works
|
||||||
|
works = Work.where(imported_from_url: @imported_from_url)
|
||||||
|
if works.count > 0
|
||||||
|
flash[:error] = ts("There is already a work imported from the url %{url}.", url: @imported_from_url)
|
||||||
|
else
|
||||||
|
# ok let's try to update
|
||||||
|
@work.update_attribute(:imported_from_url, @imported_from_url)
|
||||||
|
flash[:notice] = "Updated imported-from url for #{@work.title} to #{@imported_from_url}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
redirect_to action: :index, imported_from_url: @imported_from_url and return
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
90
app/controllers/orphans_controller.rb
Normal file
90
app/controllers/orphans_controller.rb
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
class OrphansController < ApplicationController
|
||||||
|
# You must be logged in to orphan works - relies on current_user data
|
||||||
|
before_action :users_only, except: [:index]
|
||||||
|
|
||||||
|
before_action :check_user_not_suspended, except: [:index]
|
||||||
|
before_action :load_pseuds, only: [:create]
|
||||||
|
before_action :load_orphans, only: [:create]
|
||||||
|
|
||||||
|
def index
|
||||||
|
@user = User.orphan_account
|
||||||
|
@works = @user.works
|
||||||
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
if params[:work_id]
|
||||||
|
@to_be_orphaned = Work.find(params[:work_id])
|
||||||
|
check_one_owned(@to_be_orphaned, current_user.works)
|
||||||
|
elsif params[:work_ids]
|
||||||
|
@to_be_orphaned = Work.where(id: params[:work_ids]).to_a
|
||||||
|
check_all_owned(@to_be_orphaned, current_user.works)
|
||||||
|
elsif params[:series_id]
|
||||||
|
@to_be_orphaned = Series.find(params[:series_id])
|
||||||
|
check_one_owned(@to_be_orphaned, current_user.series)
|
||||||
|
elsif params[:pseud_id]
|
||||||
|
@to_be_orphaned = Pseud.find(params[:pseud_id])
|
||||||
|
check_one_owned(@to_be_orphaned, current_user.pseuds)
|
||||||
|
else
|
||||||
|
@to_be_orphaned = current_user
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
use_default = params[:use_default] == "true"
|
||||||
|
Creatorship.orphan(@pseuds, @orphans, use_default)
|
||||||
|
flash[:notice] = ts("Orphaning was successful.")
|
||||||
|
redirect_to user_path(current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def show_orphan_permission_error
|
||||||
|
flash[:error] = ts("You don't have permission to orphan that!")
|
||||||
|
redirect_to root_path
|
||||||
|
end
|
||||||
|
|
||||||
|
# Given an ActiveRecord item and an ActiveRecord relation, check whether the
|
||||||
|
# item is in the relation. If not, show a flash error.
|
||||||
|
def check_one_owned(chosen_item, all_owned_items)
|
||||||
|
show_orphan_permission_error unless all_owned_items.exists?(chosen_item.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Given a collection of ActiveRecords and an ActiveRecord relation, check
|
||||||
|
# whether all items in the collection are contained in the relation. If not,
|
||||||
|
# show a flash error.
|
||||||
|
def check_all_owned(chosen_items, all_owned_items)
|
||||||
|
chosen_ids = chosen_items.map(&:id)
|
||||||
|
owned_ids = all_owned_items.where(id: chosen_ids).pluck(:id)
|
||||||
|
unowned_ids = chosen_ids - owned_ids
|
||||||
|
show_orphan_permission_error if unowned_ids.any?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Load the list of works or series into the @orphans variable, and verify
|
||||||
|
# that the current user owns the works/series in question.
|
||||||
|
def load_orphans
|
||||||
|
if params[:work_ids]
|
||||||
|
@orphans = Work.where(id: params[:work_ids]).to_a
|
||||||
|
check_all_owned(@orphans, current_user.works)
|
||||||
|
elsif params[:series_id]
|
||||||
|
@orphans = Series.where(id: params[:series_id]).to_a
|
||||||
|
check_all_owned(@orphans, current_user.series)
|
||||||
|
else
|
||||||
|
flash[:error] = ts("What did you want to orphan?")
|
||||||
|
redirect_to current_user
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# If a pseud_id is specified, load it and check that it belongs to the
|
||||||
|
# current user. Otherwise, assume that the user wants to orphan with all of
|
||||||
|
# their pseuds.
|
||||||
|
def load_pseuds
|
||||||
|
if params[:pseud_id]
|
||||||
|
@pseuds = Pseud.where(id: params[:pseud_id]).to_a
|
||||||
|
check_all_owned(@pseuds, current_user.pseuds)
|
||||||
|
else
|
||||||
|
@pseuds = current_user.pseuds
|
||||||
|
# We don't need to check ownership here because these pseuds are
|
||||||
|
# guaranteed to be owned by the current user.
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
239
app/controllers/owned_tag_sets_controller.rb
Normal file
239
app/controllers/owned_tag_sets_controller.rb
Normal file
|
|
@ -0,0 +1,239 @@
|
||||||
|
class OwnedTagSetsController < ApplicationController
|
||||||
|
cache_sweeper :tag_set_sweeper
|
||||||
|
|
||||||
|
before_action :load_tag_set, except: [:index, :new, :create, :show_options]
|
||||||
|
before_action :users_only, only: [:new, :create]
|
||||||
|
before_action :moderators_only, except: [:index, :new, :create, :show, :show_options]
|
||||||
|
before_action :owners_only, only: [:destroy]
|
||||||
|
|
||||||
|
def load_tag_set
|
||||||
|
@tag_set = OwnedTagSet.find_by(id: params[:id])
|
||||||
|
unless @tag_set
|
||||||
|
flash[:error] = ts("What Tag Set did you want to look at?")
|
||||||
|
redirect_to tag_sets_path and return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def moderators_only
|
||||||
|
@tag_set.user_is_moderator?(current_user) || access_denied
|
||||||
|
end
|
||||||
|
|
||||||
|
def owners_only
|
||||||
|
@tag_set.user_is_owner?(current_user) || access_denied
|
||||||
|
end
|
||||||
|
|
||||||
|
def nominated_only
|
||||||
|
@tag_set.nominated || access_denied
|
||||||
|
end
|
||||||
|
|
||||||
|
### ACTIONS
|
||||||
|
|
||||||
|
def index
|
||||||
|
if params[:user_id]
|
||||||
|
@user = User.find_by login: params[:user_id]
|
||||||
|
@tag_sets = OwnedTagSet.owned_by(@user)
|
||||||
|
elsif params[:restriction]
|
||||||
|
@restriction = PromptRestriction.find(params[:restriction])
|
||||||
|
@tag_sets = OwnedTagSet.in_prompt_restriction(@restriction)
|
||||||
|
if @tag_sets.count == 1
|
||||||
|
redirect_to tag_set_path(@tag_sets.first, tag_type: (params[:tag_type] || "fandom")) and return
|
||||||
|
end
|
||||||
|
else
|
||||||
|
@tag_sets = OwnedTagSet
|
||||||
|
if params[:query]
|
||||||
|
@query = params[:query]
|
||||||
|
@tag_sets = @tag_sets.where("title LIKE ?", '%' + params[:query] + '%')
|
||||||
|
else
|
||||||
|
# show a random selection
|
||||||
|
@tag_sets = @tag_sets.order("created_at DESC")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@tag_sets = @tag_sets.paginate(per_page: (params[:per_page] || ArchiveConfig.ITEMS_PER_PAGE), page: (params[:page] || 1))
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_options
|
||||||
|
@restriction = PromptRestriction.find_by(id: params[:restriction])
|
||||||
|
unless @restriction
|
||||||
|
flash[:error] = ts("Which Tag Set did you want to look at?")
|
||||||
|
redirect_to tag_sets_path and return
|
||||||
|
end
|
||||||
|
@tag_sets = OwnedTagSet.in_prompt_restriction(@restriction)
|
||||||
|
@tag_set_ids = @tag_sets.pluck(:tag_set_id)
|
||||||
|
@tag_type = params[:tag_type] && TagSet::TAG_TYPES.include?(params[:tag_type]) ? params[:tag_type] : "fandom"
|
||||||
|
# @tag_type is restricted by in_prompt_restriction and therefore safe to pass to constantize
|
||||||
|
@tags = @tag_type.classify.constantize.joins(:set_taggings).where("set_taggings.tag_set_id IN (?)", @tag_set_ids).by_name_without_articles
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
# don't bother collecting tags unless the user gets to see them
|
||||||
|
if @tag_set.visible || @tag_set.user_is_moderator?(current_user)
|
||||||
|
|
||||||
|
# we use this to collect up fandom parents for characters and relationships
|
||||||
|
@fandom_keys_from_other_tags = []
|
||||||
|
|
||||||
|
# we use this to store the tag name results
|
||||||
|
@tag_hash = HashWithIndifferentAccess.new
|
||||||
|
|
||||||
|
%w[character relationship].each do |tag_type|
|
||||||
|
next unless @tag_set.has_type?(tag_type)
|
||||||
|
|
||||||
|
## names_by_parent returns a hash of arrays like so:
|
||||||
|
## hash[parent_name] => [child name, child name, child name]
|
||||||
|
|
||||||
|
# get the manually associated fandoms
|
||||||
|
assoc_hash = TagSetAssociation.names_by_parent(TagSetAssociation.for_tag_set(@tag_set), tag_type)
|
||||||
|
|
||||||
|
# get canonically associated fandoms
|
||||||
|
# Safe for constantize as tag_type restricted to character relationship
|
||||||
|
canonical_hash = Tag.names_by_parent(tag_type.classify.constantize.in_tag_set(@tag_set).canonical, "fandom")
|
||||||
|
|
||||||
|
# merge the values of the two hashes (each value is an array) as a set (ie remove duplicates)
|
||||||
|
@tag_hash[tag_type] = assoc_hash.merge(canonical_hash) { |_key, oldval, newval| (oldval | newval) }
|
||||||
|
|
||||||
|
# get any tags without a fandom
|
||||||
|
remaining = @tag_set.with_type(tag_type).where.not(name: @tag_hash[tag_type].values.flatten)
|
||||||
|
if remaining.any?
|
||||||
|
@tag_hash[tag_type]["(No linked fandom - might need association)"] ||= []
|
||||||
|
@tag_hash[tag_type]["(No linked fandom - might need association)"] += remaining.pluck(:name)
|
||||||
|
end
|
||||||
|
|
||||||
|
# store the parents
|
||||||
|
@fandom_keys_from_other_tags += @tag_hash[tag_type].keys
|
||||||
|
end
|
||||||
|
|
||||||
|
# get rid of duplicates and sort
|
||||||
|
@fandom_keys_from_other_tags = @fandom_keys_from_other_tags.compact.uniq.sort {|a,b| a.gsub(/^(the |an |a )/, '') <=> b.gsub(/^(the |an |a )/, '')}
|
||||||
|
|
||||||
|
# now handle fandoms
|
||||||
|
if @tag_set.has_type?("fandom")
|
||||||
|
# Get fandoms hashed by media -- just the canonical associations
|
||||||
|
@tag_hash[:fandom] = Tag.names_by_parent(Fandom.in_tag_set(@tag_set), "media")
|
||||||
|
|
||||||
|
# get any fandoms without a media
|
||||||
|
if @tag_set.with_type("fandom").with_no_parents.count > 0
|
||||||
|
@tag_hash[:fandom]["(No Media)"] ||= []
|
||||||
|
@tag_hash[:fandom]["(No Media)"] += @tag_set.with_type("fandom").with_no_parents.pluck(:name)
|
||||||
|
end
|
||||||
|
|
||||||
|
# we want to collect and warn about any chars or relationships not in the set's fandoms
|
||||||
|
@character_seen = {}
|
||||||
|
@relationship_seen = {}
|
||||||
|
|
||||||
|
# clear out the fandoms we're showing from the list of other tags
|
||||||
|
if @fandom_keys_from_other_tags && !@fandom_keys_from_other_tags.empty?
|
||||||
|
@fandom_keys_from_other_tags -= @tag_hash[:fandom].values.flatten
|
||||||
|
end
|
||||||
|
|
||||||
|
@unassociated_chars = []
|
||||||
|
@unassociated_rels = []
|
||||||
|
unless @fandom_keys_from_other_tags.empty?
|
||||||
|
if @tag_hash[:character]
|
||||||
|
@unassociated_chars = @tag_hash[:character].values_at(*@fandom_keys_from_other_tags).flatten.compact.uniq
|
||||||
|
end
|
||||||
|
if @tag_hash[:relationship]
|
||||||
|
@unassociated_rels = @tag_hash[:relationship].values_at(*@fandom_keys_from_other_tags).flatten.compact.uniq
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
@tag_set = OwnedTagSet.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@tag_set = OwnedTagSet.new(owned_tag_set_params)
|
||||||
|
@tag_set.add_owner(current_user.default_pseud)
|
||||||
|
if @tag_set.save
|
||||||
|
flash[:notice] = ts('Tag Set was successfully created.')
|
||||||
|
redirect_to tag_set_path(@tag_set)
|
||||||
|
else
|
||||||
|
render action: "new"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
get_parent_child_tags
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
if @tag_set.update(owned_tag_set_params) && @tag_set.tag_set.save!
|
||||||
|
flash[:notice] = ts("Tag Set was successfully updated.")
|
||||||
|
redirect_to tag_set_path(@tag_set)
|
||||||
|
else
|
||||||
|
get_parent_child_tags
|
||||||
|
render action: :edit
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def confirm_delete
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@tag_set = OwnedTagSet.find(params[:id])
|
||||||
|
begin
|
||||||
|
name = @tag_set.title
|
||||||
|
@tag_set.destroy
|
||||||
|
flash[:notice] = ts("Your Tag Set %{name} was deleted.", name: name)
|
||||||
|
rescue
|
||||||
|
flash[:error] = ts("We couldn't delete that right now, sorry! Please try again later.")
|
||||||
|
end
|
||||||
|
redirect_to tag_sets_path
|
||||||
|
end
|
||||||
|
|
||||||
|
def batch_load
|
||||||
|
end
|
||||||
|
|
||||||
|
def do_batch_load
|
||||||
|
if params[:batch_associations]
|
||||||
|
failed = @tag_set.load_batch_associations!(params[:batch_associations], do_relationships: (params[:batch_do_relationships] ? true : false))
|
||||||
|
if failed.empty?
|
||||||
|
flash[:notice] = ts("Tags and associations loaded!")
|
||||||
|
redirect_to tag_set_path(@tag_set) and return
|
||||||
|
else
|
||||||
|
flash.now[:notice] = ts("We couldn't add all the tags and associations you wanted -- the ones left below didn't work. See the help for suggestions!")
|
||||||
|
@failed_batch_associations = failed.join("\n")
|
||||||
|
render action: :batch_load and return
|
||||||
|
end
|
||||||
|
else
|
||||||
|
flash[:error] = ts("What did you want to load?")
|
||||||
|
redirect_to action: :batch_load and return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
# for manual associations
|
||||||
|
def get_parent_child_tags
|
||||||
|
@tags_in_set = Tag.joins(:set_taggings).where("set_taggings.tag_set_id = ?", @tag_set.tag_set_id).order("tags.name ASC")
|
||||||
|
@parent_tags_in_set = @tags_in_set.where(type: 'Fandom').pluck :name, :id
|
||||||
|
@child_tags_in_set = @tags_in_set.where("type IN ('Relationship', 'Character')").pluck :name, :id
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def owned_tag_set_params
|
||||||
|
params.require(:owned_tag_set).permit(
|
||||||
|
:owner_changes, :moderator_changes, :title, :description, :visible,
|
||||||
|
:usable, :nominated, :fandom_nomination_limit, :character_nomination_limit,
|
||||||
|
:relationship_nomination_limit, :freeform_nomination_limit,
|
||||||
|
associations_to_remove: [],
|
||||||
|
tag_set_associations_attributes: [
|
||||||
|
:id, :create_association, :tag_id, :parent_tag_id, :_destroy
|
||||||
|
],
|
||||||
|
tag_set_attributes: [
|
||||||
|
:id,
|
||||||
|
:from_owned_tag_set, :fandom_tagnames_to_add, :character_tagnames_to_add,
|
||||||
|
:relationship_tagnames_to_add, :freeform_tagnames_to_add,
|
||||||
|
character_tagnames: [], rating_tagnames: [], archive_warning_tagnames: [],
|
||||||
|
category_tagnames: [], fandom_tags_to_remove: [], character_tags_to_remove: [],
|
||||||
|
relationship_tags_to_remove: [], freeform_tags_to_remove: []
|
||||||
|
]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
33
app/controllers/people_controller.rb
Normal file
33
app/controllers/people_controller.rb
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
class PeopleController < ApplicationController
|
||||||
|
|
||||||
|
before_action :load_collection
|
||||||
|
|
||||||
|
def search
|
||||||
|
if people_search_params.blank?
|
||||||
|
@search = PseudSearchForm.new({})
|
||||||
|
else
|
||||||
|
options = people_search_params.merge(page: params[:page])
|
||||||
|
@search = PseudSearchForm.new(options)
|
||||||
|
@people = @search.search_results.scope(:for_search)
|
||||||
|
flash_search_warnings(@people)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def index
|
||||||
|
if @collection.present?
|
||||||
|
@people = @collection.participants.with_attached_icon.includes(:user).order(:name).page(params[:page])
|
||||||
|
@rec_counts = Pseud.rec_counts_for_pseuds(@people)
|
||||||
|
@work_counts = Pseud.work_counts_for_pseuds(@people)
|
||||||
|
@page_subtitle = t(".collection_page_title", collection_title: @collection.title)
|
||||||
|
else
|
||||||
|
redirect_to search_people_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def people_search_params
|
||||||
|
return {} unless params[:people_search].present?
|
||||||
|
params[:people_search].permit!
|
||||||
|
end
|
||||||
|
end
|
||||||
146
app/controllers/potential_matches_controller.rb
Normal file
146
app/controllers/potential_matches_controller.rb
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
class PotentialMatchesController < ApplicationController
|
||||||
|
|
||||||
|
before_action :users_only
|
||||||
|
before_action :load_collection
|
||||||
|
before_action :collection_maintainers_only
|
||||||
|
before_action :load_challenge
|
||||||
|
before_action :check_assignments_not_sent
|
||||||
|
before_action :check_signup_closed, only: [:generate]
|
||||||
|
before_action :load_potential_match_from_id, only: [:show]
|
||||||
|
|
||||||
|
|
||||||
|
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 load_potential_match_from_id
|
||||||
|
@potential_match = PotentialMatch.find(params[:id])
|
||||||
|
no_potential_match and return unless @potential_match
|
||||||
|
end
|
||||||
|
|
||||||
|
def no_assignment
|
||||||
|
flash[:error] = ts("What potential match did you want to work on?")
|
||||||
|
redirect_to collection_path(@collection) rescue redirect_to '/'
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_signup_closed
|
||||||
|
signup_open and return unless !@challenge.signup_open
|
||||||
|
end
|
||||||
|
|
||||||
|
def signup_open
|
||||||
|
flash[:error] = ts("Sign-up is still open, you cannot determine potential matches 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] = ts("Assignments have already been sent! If necessary, you can purge them.")
|
||||||
|
redirect_to collection_assignments_path(@collection) rescue redirect_to '/'
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def index
|
||||||
|
@settings = @collection.challenge.potential_match_settings
|
||||||
|
|
||||||
|
if (invalid_ids = PotentialMatch.invalid_signups_for(@collection)).present?
|
||||||
|
# there are invalid signups
|
||||||
|
@invalid_signups = ChallengeSignup.where(id: invalid_ids)
|
||||||
|
elsif PotentialMatch.in_progress?(@collection)
|
||||||
|
# we're generating
|
||||||
|
@in_progress = true
|
||||||
|
@progress = PotentialMatch.progress(@collection)
|
||||||
|
elsif ChallengeAssignment.in_progress?(@collection)
|
||||||
|
@assignment_in_progress = true
|
||||||
|
elsif @collection.potential_matches.count > 0 && @collection.assignments.count == 0
|
||||||
|
flash[:error] = ts("There has been an error in the potential matching. Please first try regenerating assignments, and if that doesn't work, all potential matches. If the problem persists, please contact Support.")
|
||||||
|
elsif @collection.potential_matches.count > 0
|
||||||
|
# we have potential_matches and assignments
|
||||||
|
|
||||||
|
### find assignments with no potential recipients
|
||||||
|
# first get signups with no offer potential matches
|
||||||
|
no_opms = ChallengeSignup.in_collection(@collection).no_potential_offers.pluck(:id)
|
||||||
|
@assignments_with_no_potential_recipients = @collection.assignments.where(offer_signup_id: no_opms)
|
||||||
|
|
||||||
|
### find assignments with no potential giver
|
||||||
|
# first get signups with no request potential matches
|
||||||
|
no_rpms = ChallengeSignup.in_collection(@collection).no_potential_requests.pluck(:id)
|
||||||
|
@assignments_with_no_potential_givers = @collection.assignments.where(request_signup_id: no_rpms)
|
||||||
|
|
||||||
|
# list the assignments by requester
|
||||||
|
if params[:no_giver]
|
||||||
|
@assignments = @collection.assignments.with_request.with_no_offer.order_by_requesting_pseud
|
||||||
|
elsif params[:no_recipient]
|
||||||
|
# ordering causes this to hang on large challenge due to
|
||||||
|
# left join required to get offering pseuds
|
||||||
|
@assignments = @collection.assignments.with_offer.with_no_request # .order_by_offering_pseud
|
||||||
|
elsif params[:dup_giver]
|
||||||
|
@assignments = ChallengeAssignment.duplicate_givers(@collection).order_by_offering_pseud
|
||||||
|
elsif params[:dup_recipient]
|
||||||
|
@assignments = ChallengeAssignment.duplicate_recipients(@collection).order_by_requesting_pseud
|
||||||
|
else
|
||||||
|
@assignments = @collection.assignments.with_request.with_offer.order_by_requesting_pseud
|
||||||
|
end
|
||||||
|
@assignments = @assignments.paginate page: params[:page], per_page: ArchiveConfig.ITEMS_PER_PAGE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generate potential matches
|
||||||
|
def generate
|
||||||
|
if PotentialMatch.in_progress?(@collection)
|
||||||
|
flash[:error] = ts("Potential matches are already being generated for this collection!")
|
||||||
|
else
|
||||||
|
# delete all existing assignments and potential matches for this collection
|
||||||
|
ChallengeAssignment.clear!(@collection)
|
||||||
|
PotentialMatch.clear!(@collection)
|
||||||
|
|
||||||
|
flash[:notice] = ts("Beginning generation of potential matches. This may take some time, especially if your challenge is large.")
|
||||||
|
PotentialMatch.set_up_generating(@collection)
|
||||||
|
PotentialMatch.generate(@collection)
|
||||||
|
end
|
||||||
|
|
||||||
|
# redirect to index
|
||||||
|
redirect_to collection_potential_matches_path(@collection)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Regenerate matches for one signup
|
||||||
|
def regenerate_for_signup
|
||||||
|
if params[:signup_id].blank? || (@signup = ChallengeSignup.where(id: params[:signup_id]).first).nil?
|
||||||
|
flash[:error] = ts("What sign-up did you want to regenerate matches for?")
|
||||||
|
else
|
||||||
|
PotentialMatch.regenerate_for_signup(@signup)
|
||||||
|
flash[:notice] = ts("Matches are being regenerated for ") + @signup.pseud.byline +
|
||||||
|
ts(". Please allow at least 5 minutes for this process to complete before refreshing the page.")
|
||||||
|
end
|
||||||
|
# redirect to index
|
||||||
|
redirect_to collection_potential_matches_path(@collection)
|
||||||
|
end
|
||||||
|
|
||||||
|
def cancel_generate
|
||||||
|
if !PotentialMatch.in_progress?(@collection)
|
||||||
|
flash[:error] = ts("Potential matches are not currently being generated for this challenge.")
|
||||||
|
elsif PotentialMatch.canceled?(@collection)
|
||||||
|
flash[:error] = ts("Potential match generation has already been canceled, please refresh again shortly.")
|
||||||
|
else
|
||||||
|
PotentialMatch.cancel_generation(@collection)
|
||||||
|
flash[:notice] = ts("Potential match generation cancellation requested. This may take a while, please refresh shortly.")
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect_to collection_potential_matches_path(@collection)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
72
app/controllers/preferences_controller.rb
Normal file
72
app/controllers/preferences_controller.rb
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
class PreferencesController < ApplicationController
|
||||||
|
before_action :load_user
|
||||||
|
before_action :check_ownership
|
||||||
|
skip_before_action :store_location
|
||||||
|
|
||||||
|
# Ensure that the current user is authorized to view and change this information
|
||||||
|
def load_user
|
||||||
|
@user = User.find_by(login: params[:user_id])
|
||||||
|
@check_ownership_of = @user
|
||||||
|
end
|
||||||
|
|
||||||
|
def index
|
||||||
|
@user = User.find_by(login: params[:user_id])
|
||||||
|
@preference = @user.preference
|
||||||
|
@available_skins = (current_user.skins.site_skins + Skin.approved_skins.site_skins).uniq
|
||||||
|
@available_locales = Locale.where(email_enabled: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@user = User.find_by(login: params[:user_id])
|
||||||
|
@preference = @user.preference
|
||||||
|
@user.preference.attributes = preference_params
|
||||||
|
@available_skins = (current_user.skins.site_skins + Skin.approved_skins.site_skins).uniq
|
||||||
|
@available_locales = Locale.where(email_enabled: true)
|
||||||
|
|
||||||
|
if params[:preference][:skin_id].present?
|
||||||
|
# unset session skin if user changed their skin
|
||||||
|
session[:site_skin] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if @user.preference.save
|
||||||
|
flash[:notice] = ts('Your preferences were successfully updated.')
|
||||||
|
redirect_to user_path(@user)
|
||||||
|
else
|
||||||
|
flash[:error] = ts('Sorry, something went wrong. Please try that again.')
|
||||||
|
render action: :index
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def preference_params
|
||||||
|
params.require(:preference).permit(
|
||||||
|
:minimize_search_engines,
|
||||||
|
:disable_share_links,
|
||||||
|
:adult,
|
||||||
|
:view_full_works,
|
||||||
|
:hide_warnings,
|
||||||
|
:hide_freeform,
|
||||||
|
:disable_work_skins,
|
||||||
|
:skin_id,
|
||||||
|
:time_zone,
|
||||||
|
:preferred_locale,
|
||||||
|
:work_title_format,
|
||||||
|
:comment_emails_off,
|
||||||
|
:comment_inbox_off,
|
||||||
|
:comment_copy_to_self_off,
|
||||||
|
:kudos_emails_off,
|
||||||
|
:admin_emails_off,
|
||||||
|
:allow_collection_invitation,
|
||||||
|
:collection_emails_off,
|
||||||
|
:collection_inbox_off,
|
||||||
|
:recipient_emails_off,
|
||||||
|
:history_enabled,
|
||||||
|
:first_login,
|
||||||
|
:banner_seen,
|
||||||
|
:allow_cocreator,
|
||||||
|
:allow_gifts,
|
||||||
|
:guest_replies_off
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
45
app/controllers/profile_controller.rb
Normal file
45
app/controllers/profile_controller.rb
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
class ProfileController < ApplicationController
|
||||||
|
before_action :load_user_and_pseuds
|
||||||
|
|
||||||
|
def show
|
||||||
|
@user = User.find_by(login: params[:user_id])
|
||||||
|
if @user.profile.nil?
|
||||||
|
Profile.create(user_id: @user.id)
|
||||||
|
@user.reload
|
||||||
|
end
|
||||||
|
|
||||||
|
@profile = @user.profile
|
||||||
|
|
||||||
|
# code the same as the stuff in users_controller
|
||||||
|
if current_user.respond_to?(:subscriptions)
|
||||||
|
@subscription = current_user.subscriptions.where(subscribable_id: @user.id,
|
||||||
|
subscribable_type: "User").first ||
|
||||||
|
current_user.subscriptions.build(subscribable: @user)
|
||||||
|
end
|
||||||
|
@page_subtitle = t(".page_title", username: @user.login)
|
||||||
|
end
|
||||||
|
|
||||||
|
def pseuds
|
||||||
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
|
redirect_to user_pseuds_path(@user)
|
||||||
|
end
|
||||||
|
|
||||||
|
format.js
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_user_and_pseuds
|
||||||
|
@user = User.find_by(login: params[:user_id])
|
||||||
|
|
||||||
|
if @user.nil?
|
||||||
|
flash[:error] = ts("Sorry, there's no user by that name.")
|
||||||
|
redirect_to root_path
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
@pseuds = @user.pseuds.default_alphabetical.paginate(page: params[:page])
|
||||||
|
end
|
||||||
|
end
|
||||||
211
app/controllers/prompts_controller.rb
Normal file
211
app/controllers/prompts_controller.rb
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
class PromptsController < ApplicationController
|
||||||
|
|
||||||
|
before_action :users_only, except: [:show]
|
||||||
|
before_action :load_collection, except: [:index]
|
||||||
|
before_action :load_challenge, except: [:index]
|
||||||
|
before_action :load_prompt_from_id, only: [:show, :edit, :update, :destroy]
|
||||||
|
before_action :load_signup, except: [:index, :destroy, :show]
|
||||||
|
# before_action :promptmeme_only, except: [:index, :new]
|
||||||
|
before_action :allowed_to_destroy, only: [:destroy]
|
||||||
|
before_action :allowed_to_view, only: [:show]
|
||||||
|
before_action :signup_owner_only, only: [:edit, :update]
|
||||||
|
before_action :check_signup_open, only: [:new, :create, :edit, :update]
|
||||||
|
before_action :check_prompt_in_collection, only: [:show, :edit, :update, :destroy]
|
||||||
|
|
||||||
|
# def promptmeme_only
|
||||||
|
# unless @collection.challenge_type == "PromptMeme"
|
||||||
|
# flash[:error] = ts("Only available for prompt meme challenges, not gift exchanges")
|
||||||
|
# redirect_to collection_path(@collection) rescue redirect_to '/'
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
|
||||||
|
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 load_signup
|
||||||
|
unless @challenge_signup
|
||||||
|
@challenge_signup = ChallengeSignup.in_collection(@collection).by_user(current_user).first
|
||||||
|
end
|
||||||
|
no_signup and return unless @challenge_signup
|
||||||
|
end
|
||||||
|
|
||||||
|
def no_signup
|
||||||
|
flash[:error] = ts("Please submit a basic sign-up with the required fields first.")
|
||||||
|
redirect_to new_collection_signup_path(@collection) rescue redirect_to '/'
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_signup_open
|
||||||
|
signup_closed and return unless (@challenge.signup_open || @collection.user_is_owner?(current_user) || @collection.user_is_moderator?(current_user))
|
||||||
|
end
|
||||||
|
|
||||||
|
def signup_closed
|
||||||
|
flash[:error] = ts("Signup is currently closed: please contact a moderator for help.")
|
||||||
|
redirect_to @collection rescue redirect_to '/'
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def signup_owner_only
|
||||||
|
not_signup_owner and return unless (@challenge_signup.pseud.user == current_user || (@collection.challenge_type == "GiftExchange" && !@challenge.signup_open && @collection.user_is_owner?(current_user)))
|
||||||
|
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_prompt_from_id
|
||||||
|
@prompt = Prompt.find_by(id: params[:id])
|
||||||
|
if @prompt.nil?
|
||||||
|
no_prompt
|
||||||
|
return
|
||||||
|
end
|
||||||
|
@challenge_signup = @prompt.challenge_signup
|
||||||
|
end
|
||||||
|
|
||||||
|
def no_prompt
|
||||||
|
flash[:error] = ts("What prompt did you want to work on?")
|
||||||
|
redirect_to collection_path(@collection) rescue redirect_to '/'
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_prompt_in_collection
|
||||||
|
unless @prompt.collection_id == @collection.id
|
||||||
|
flash[:error] = ts("Sorry, that prompt isn't associated with that collection.")
|
||||||
|
redirect_to @collection
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def allowed_to_view
|
||||||
|
unless @challenge.user_allowed_to_see_prompt?(current_user, @prompt)
|
||||||
|
access_denied(redirect: @collection)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
#### ACTIONS
|
||||||
|
|
||||||
|
def index
|
||||||
|
# this currently doesn't get called anywhere
|
||||||
|
# should probably list all the prompts in a given collection (instead of using challenge signup for that)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
if params[:prompt_type] == "offer"
|
||||||
|
@index = @challenge_signup.offers.count
|
||||||
|
@prompt = @challenge_signup.offers.build
|
||||||
|
else
|
||||||
|
@index = @challenge_signup.requests.count
|
||||||
|
@prompt = @challenge_signup.requests.build
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
@index = @challenge_signup.send(@prompt.class.name.downcase.pluralize).index(@prompt)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
if params[:prompt_type] == "offer"
|
||||||
|
@prompt = @challenge_signup.offers.build(prompt_params)
|
||||||
|
else
|
||||||
|
@prompt = @challenge_signup.requests.build(prompt_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
if @challenge_signup.save
|
||||||
|
flash[:notice] = ts("Prompt was successfully added.")
|
||||||
|
redirect_to collection_signup_path(@collection, @challenge_signup)
|
||||||
|
else
|
||||||
|
flash[:error] = ts("That prompt would make your overall sign-up invalid, sorry.")
|
||||||
|
redirect_to edit_collection_signup_path(@collection, @challenge_signup)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
if @prompt.update(prompt_params)
|
||||||
|
flash[:notice] = ts("Prompt was successfully updated.")
|
||||||
|
redirect_to collection_signup_path(@collection, @challenge_signup)
|
||||||
|
else
|
||||||
|
render action: :edit
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
if !(@challenge.signup_open || @collection.user_is_maintainer?(current_user))
|
||||||
|
flash[:error] = ts("You cannot delete a prompt after sign-ups are closed. Please contact a moderator for help.")
|
||||||
|
else
|
||||||
|
if !@prompt.can_delete?
|
||||||
|
flash[:error] = ts("That would make your sign-up invalid, sorry! Please edit instead.")
|
||||||
|
else
|
||||||
|
@prompt.destroy
|
||||||
|
flash[:notice] = ts("Prompt was deleted.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if @collection.user_is_maintainer?(current_user) && @collection.challenge_type == "PromptMeme"
|
||||||
|
redirect_to collection_requests_path(@collection)
|
||||||
|
elsif @prompt.challenge_signup
|
||||||
|
redirect_to collection_signup_path(@collection, @prompt.challenge_signup)
|
||||||
|
elsif @collection.user_is_maintainer?(current_user)
|
||||||
|
redirect_to collection_signups_path(@collection)
|
||||||
|
else
|
||||||
|
redirect_to @collection
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def prompt_params
|
||||||
|
params.require(:prompt).permit(
|
||||||
|
:title,
|
||||||
|
:url,
|
||||||
|
:anonymous,
|
||||||
|
:description,
|
||||||
|
:any_fandom,
|
||||||
|
:any_character,
|
||||||
|
:any_relationship,
|
||||||
|
:any_freeform,
|
||||||
|
:any_category,
|
||||||
|
:any_rating,
|
||||||
|
:any_archive_warning,
|
||||||
|
tag_set_attributes: [
|
||||||
|
:fandom_tagnames,
|
||||||
|
: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: []
|
||||||
|
],
|
||||||
|
optional_tag_set_attributes: [
|
||||||
|
:tagnames
|
||||||
|
]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
143
app/controllers/pseuds_controller.rb
Normal file
143
app/controllers/pseuds_controller.rb
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
class PseudsController < ApplicationController
|
||||||
|
cache_sweeper :pseud_sweeper
|
||||||
|
|
||||||
|
before_action :load_user
|
||||||
|
before_action :check_ownership, only: [:create, :destroy, :new]
|
||||||
|
before_action :check_ownership_or_admin, only: [:edit, :update]
|
||||||
|
before_action :check_user_status, only: [:new, :create, :edit, :update]
|
||||||
|
|
||||||
|
def load_user
|
||||||
|
@user = User.find_by!(login: params[:user_id])
|
||||||
|
@check_ownership_of = @user
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /pseuds
|
||||||
|
# GET /pseuds.xml
|
||||||
|
def index
|
||||||
|
@pseuds = @user.pseuds.with_attached_icon.alphabetical.paginate(page: params[:page])
|
||||||
|
@rec_counts = Pseud.rec_counts_for_pseuds(@pseuds)
|
||||||
|
@work_counts = Pseud.work_counts_for_pseuds(@pseuds)
|
||||||
|
@page_subtitle = @user.login
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /users/:user_id/pseuds/:id
|
||||||
|
def show
|
||||||
|
@pseud = @user.pseuds.find_by!(name: params[:id])
|
||||||
|
@page_subtitle = @pseud.name
|
||||||
|
|
||||||
|
# very similar to show under users - if you change something here, change it there too
|
||||||
|
if logged_in? || logged_in_as_admin?
|
||||||
|
visible_works = @pseud.works.visible_to_registered_user
|
||||||
|
visible_series = @pseud.series.visible_to_registered_user
|
||||||
|
visible_bookmarks = @pseud.bookmarks.visible_to_registered_user
|
||||||
|
else
|
||||||
|
visible_works = @pseud.works.visible_to_all
|
||||||
|
visible_series = @pseud.series.visible_to_all
|
||||||
|
visible_bookmarks = @pseud.bookmarks.visible_to_all
|
||||||
|
end
|
||||||
|
|
||||||
|
visible_works = visible_works.revealed.non_anon
|
||||||
|
visible_series = visible_series.exclude_anonymous
|
||||||
|
|
||||||
|
@fandoms = \
|
||||||
|
Fandom.select("tags.*, count(DISTINCT works.id) as work_count")
|
||||||
|
.joins(:filtered_works).group("tags.id").merge(visible_works)
|
||||||
|
.where(filter_taggings: { inherited: false })
|
||||||
|
.order("work_count DESC").load
|
||||||
|
|
||||||
|
@works = visible_works.order("revised_at DESC").limit(ArchiveConfig.NUMBER_OF_ITEMS_VISIBLE_IN_DASHBOARD)
|
||||||
|
@series = visible_series.order("updated_at DESC").limit(ArchiveConfig.NUMBER_OF_ITEMS_VISIBLE_IN_DASHBOARD)
|
||||||
|
@bookmarks = visible_bookmarks.order("updated_at DESC").limit(ArchiveConfig.NUMBER_OF_ITEMS_VISIBLE_IN_DASHBOARD)
|
||||||
|
|
||||||
|
return unless current_user.respond_to?(:subscriptions)
|
||||||
|
|
||||||
|
@subscription = current_user.subscriptions.where(subscribable_id: @user.id,
|
||||||
|
subscribable_type: "User").first ||
|
||||||
|
current_user.subscriptions.build(subscribable: @user)
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /pseuds/new
|
||||||
|
# GET /pseuds/new.xml
|
||||||
|
def new
|
||||||
|
@pseud = @user.pseuds.build
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /pseuds/1/edit
|
||||||
|
def edit
|
||||||
|
@pseud = @user.pseuds.find_by!(name: params[:id])
|
||||||
|
authorize @pseud if logged_in_as_admin?
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /pseuds
|
||||||
|
# POST /pseuds.xml
|
||||||
|
def create
|
||||||
|
@pseud = Pseud.new(permitted_attributes(Pseud))
|
||||||
|
if @user.pseuds.where(name: @pseud.name).blank?
|
||||||
|
@pseud.user_id = @user.id
|
||||||
|
old_default = @user.default_pseud
|
||||||
|
if @pseud.save
|
||||||
|
flash[:notice] = t(".successfully_created")
|
||||||
|
if @pseud.is_default
|
||||||
|
# if setting this one as default, unset the attribute of the current default pseud
|
||||||
|
old_default.update_attribute(:is_default, false)
|
||||||
|
end
|
||||||
|
redirect_to polymorphic_path([@user, @pseud])
|
||||||
|
else
|
||||||
|
render action: "new"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# user tried to add pseud they already have
|
||||||
|
flash[:error] = t(".already_have_pseud_with_name")
|
||||||
|
render action: "new"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# PUT /pseuds/1
|
||||||
|
# PUT /pseuds/1.xml
|
||||||
|
def update
|
||||||
|
@pseud = @user.pseuds.find_by(name: params[:id])
|
||||||
|
authorize @pseud if logged_in_as_admin?
|
||||||
|
default = @user.default_pseud
|
||||||
|
if @pseud.update(permitted_attributes(@pseud))
|
||||||
|
if logged_in_as_admin? && @pseud.ticket_url.present?
|
||||||
|
link = view_context.link_to("Ticket ##{@pseud.ticket_number}", @pseud.ticket_url)
|
||||||
|
summary = "#{link} for User ##{@pseud.user_id}"
|
||||||
|
AdminActivity.log_action(current_admin, @pseud, action: "edit pseud", summary: summary)
|
||||||
|
end
|
||||||
|
# if setting this one as default, unset the attribute of the current default pseud
|
||||||
|
default.update_attribute(:is_default, false) if @pseud.is_default && default != @pseud
|
||||||
|
flash[:notice] = t(".successfully_updated")
|
||||||
|
redirect_to([@user, @pseud])
|
||||||
|
else
|
||||||
|
render action: "edit"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# DELETE /pseuds/1
|
||||||
|
# DELETE /pseuds/1.xml
|
||||||
|
def destroy
|
||||||
|
@hide_dashboard = true
|
||||||
|
if params[:cancel_button]
|
||||||
|
flash[:notice] = t(".not_deleted")
|
||||||
|
redirect_to(user_pseuds_path(@user)) && return
|
||||||
|
end
|
||||||
|
|
||||||
|
@pseud = @user.pseuds.find_by(name: params[:id])
|
||||||
|
if @pseud.is_default
|
||||||
|
flash[:error] = t(".cannot_delete_default")
|
||||||
|
elsif @pseud.name == @user.login
|
||||||
|
flash[:error] = t(".cannot_delete_matching_username")
|
||||||
|
elsif params[:bookmarks_action] == "transfer_bookmarks"
|
||||||
|
@pseud.change_bookmarks_ownership
|
||||||
|
@pseud.replace_me_with_default
|
||||||
|
flash[:notice] = t(".successfully_deleted")
|
||||||
|
elsif params[:bookmarks_action] == "delete_bookmarks" || @pseud.bookmarks.empty?
|
||||||
|
@pseud.replace_me_with_default
|
||||||
|
flash[:notice] = t(".successfully_deleted")
|
||||||
|
else
|
||||||
|
render "delete_preview" and return
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect_to(user_pseuds_path(@user))
|
||||||
|
end
|
||||||
|
end
|
||||||
38
app/controllers/questions_controller.rb
Normal file
38
app/controllers/questions_controller.rb
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
class QuestionsController < ApplicationController
|
||||||
|
before_action :load_archive_faq, except: :update_positions
|
||||||
|
|
||||||
|
# GET /archive_faq/:archive_faq_id/questions/manage
|
||||||
|
def manage
|
||||||
|
authorize :archive_faq, :full_access?
|
||||||
|
@questions = @archive_faq.questions.order("position")
|
||||||
|
end
|
||||||
|
|
||||||
|
# fetch archive_faq these questions belong to from db
|
||||||
|
def load_archive_faq
|
||||||
|
@archive_faq = ArchiveFaq.find_by(slug: params[:archive_faq_id])
|
||||||
|
unless @archive_faq.present?
|
||||||
|
flash[:error] = t("questions.not_found")
|
||||||
|
redirect_to root_path and return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Update the position number of questions within a archive_faq
|
||||||
|
def update_positions
|
||||||
|
authorize :archive_faq, :full_access?
|
||||||
|
if params[:questions]
|
||||||
|
@archive_faq = ArchiveFaq.find_by(slug: params[:archive_faq_id])
|
||||||
|
@archive_faq.reorder_list(params[:questions])
|
||||||
|
flash[:notice] = t(".success")
|
||||||
|
elsif params[:question]
|
||||||
|
params[:question].each_with_index do |id, position|
|
||||||
|
Question.update(id, position: position + 1)
|
||||||
|
(@questions ||= []) << Question.find(id)
|
||||||
|
end
|
||||||
|
flash[:notice] = t(".success")
|
||||||
|
end
|
||||||
|
respond_to do |format|
|
||||||
|
format.html { redirect_to(@archive_faq) and return }
|
||||||
|
format.js { render nothing: true }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
70
app/controllers/readings_controller.rb
Normal file
70
app/controllers/readings_controller.rb
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
class ReadingsController < ApplicationController
|
||||||
|
before_action :users_only
|
||||||
|
before_action :load_user
|
||||||
|
before_action :check_ownership
|
||||||
|
before_action :check_history_enabled
|
||||||
|
|
||||||
|
def load_user
|
||||||
|
@user = User.find_by(login: params[:user_id])
|
||||||
|
@check_ownership_of = @user
|
||||||
|
end
|
||||||
|
|
||||||
|
def index
|
||||||
|
@readings = @user.readings.visible
|
||||||
|
@page_subtitle = ts("History")
|
||||||
|
if params[:show] == 'to-read'
|
||||||
|
@readings = @readings.where(toread: true)
|
||||||
|
@page_subtitle = ts("Marked For Later")
|
||||||
|
end
|
||||||
|
@readings = @readings.order("last_viewed DESC")
|
||||||
|
@pagy, @readings = pagy(@readings)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@reading = @user.readings.find(params[:id])
|
||||||
|
if @reading.destroy
|
||||||
|
success_message = ts('Work successfully deleted from your history.')
|
||||||
|
respond_to do |format|
|
||||||
|
format.html { redirect_to request.referer || user_readings_path(current_user, page: params[:page]), notice: success_message }
|
||||||
|
format.json { render json: { item_success_message: success_message }, status: :ok }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
|
flash.keep
|
||||||
|
redirect_to request.referer || user_readings_path(current_user, page: params[:page]), flash: { error: @reading.errors.full_messages }
|
||||||
|
end
|
||||||
|
format.json { render json: { errors: @reading.errors.full_messages }, status: :unprocessable_entity }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear
|
||||||
|
success = true
|
||||||
|
|
||||||
|
@user.readings.each do |reading|
|
||||||
|
reading.destroy!
|
||||||
|
rescue ActiveRecord::RecordNotDestroyed
|
||||||
|
success = false
|
||||||
|
end
|
||||||
|
|
||||||
|
if success
|
||||||
|
flash[:notice] = t(".success")
|
||||||
|
else
|
||||||
|
flash[:error] = t(".error")
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect_to user_readings_path(current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
# checks if user has history enabled and redirects to preferences if not, so they can potentially change it
|
||||||
|
def check_history_enabled
|
||||||
|
unless current_user.preference.history_enabled?
|
||||||
|
flash[:notice] = ts("You have reading history disabled in your preferences. Change it below if you'd like us to keep track of it.")
|
||||||
|
redirect_to user_preferences_path(current_user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
29
app/controllers/redirect_controller.rb
Normal file
29
app/controllers/redirect_controller.rb
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
class RedirectController < ApplicationController
|
||||||
|
|
||||||
|
def index
|
||||||
|
do_redirect
|
||||||
|
end
|
||||||
|
|
||||||
|
def do_redirect
|
||||||
|
url = params[:original_url]
|
||||||
|
if url.blank?
|
||||||
|
flash[:error] = ts("What url did you want to look up?")
|
||||||
|
else
|
||||||
|
@work = Work.find_by_url(url)
|
||||||
|
if @work
|
||||||
|
flash[:notice] = ts("You have been redirected here from %{url}. Please update the original link if possible!", url: url)
|
||||||
|
redirect_to work_path(@work) and return
|
||||||
|
else
|
||||||
|
flash[:error] = ts("We could not find a work imported from that url in the Archive of Our Own, sorry! Try another url?")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
redirect_to redirect_path
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
if params[:original_url].present?
|
||||||
|
redirect_to action: :do_redirect, original_url: params[:original_url] and return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
96
app/controllers/related_works_controller.rb
Normal file
96
app/controllers/related_works_controller.rb
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
class RelatedWorksController < ApplicationController
|
||||||
|
|
||||||
|
before_action :load_user, only: [:index]
|
||||||
|
before_action :users_only, except: [:index]
|
||||||
|
before_action :get_instance_variables, except: [:index]
|
||||||
|
|
||||||
|
def index
|
||||||
|
@page_subtitle = t(".page_title", login: @user.login)
|
||||||
|
@translations_of_user = @user.related_works.posted.where(translation: true)
|
||||||
|
@remixes_of_user = @user.related_works.posted.where(translation: false)
|
||||||
|
@translations_by_user = @user.parent_work_relationships.posted.where(translation: true)
|
||||||
|
@remixes_by_user = @user.parent_work_relationships.posted.where(translation: false)
|
||||||
|
|
||||||
|
return if @user == current_user
|
||||||
|
|
||||||
|
# Extra constraints on what we display if someone else is viewing @user's
|
||||||
|
# related works page:
|
||||||
|
@translations_of_user = @translations_of_user.merge(Work.revealed.non_anon)
|
||||||
|
@remixes_of_user = @remixes_of_user.merge(Work.revealed.non_anon)
|
||||||
|
@translations_by_user = @translations_by_user.merge(Work.revealed.non_anon)
|
||||||
|
@remixes_by_user = @remixes_by_user.merge(Work.revealed.non_anon)
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /related_works/1
|
||||||
|
# GET /related_works/1.xml
|
||||||
|
def show
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
# updates are done by the owner of the parent, to aprove or remove links on the parent work.
|
||||||
|
unless @user
|
||||||
|
if current_user_owns?(@child)
|
||||||
|
flash[:error] = ts("Sorry, but you don't have permission to do that. Try removing the link from your own work.")
|
||||||
|
redirect_back_or_default(user_related_works_path(current_user))
|
||||||
|
return
|
||||||
|
else
|
||||||
|
flash[:error] = ts("Sorry, but you don't have permission to do that.")
|
||||||
|
redirect_back_or_default(root_path)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# the assumption here is that any update is a toggle from what was before
|
||||||
|
@related_work.reciprocal = !@related_work.reciprocal?
|
||||||
|
if @related_work.update_attribute(:reciprocal, @related_work.reciprocal)
|
||||||
|
notice = @related_work.reciprocal? ? ts("Link was successfully approved") :
|
||||||
|
ts("Link was successfully removed")
|
||||||
|
flash[:notice] = notice
|
||||||
|
redirect_to(@related_work.parent)
|
||||||
|
else
|
||||||
|
flash[:error] = ts('Sorry, something went wrong.')
|
||||||
|
redirect_to(@related_work)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
# destroys are done by the owner of the child, to remove links to the parent work which also removes the link back if it exists.
|
||||||
|
unless current_user_owns?(@child)
|
||||||
|
if @user
|
||||||
|
flash[:error] = ts("Sorry, but you don't have permission to do that. You can only approve or remove the link from your own work.")
|
||||||
|
redirect_back_or_default(user_related_works_path(current_user))
|
||||||
|
return
|
||||||
|
else
|
||||||
|
flash[:error] = ts("Sorry, but you don't have permission to do that.")
|
||||||
|
redirect_back_or_default(root_path)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@related_work.destroy
|
||||||
|
redirect_back(fallback_location: user_related_works_path(current_user))
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_user
|
||||||
|
if params[:user_id].blank?
|
||||||
|
flash[:error] = ts("Whose related works were you looking for?")
|
||||||
|
redirect_back_or_default(search_people_path)
|
||||||
|
else
|
||||||
|
@user = User.find_by(login: params[:user_id])
|
||||||
|
if @user.blank?
|
||||||
|
flash[:error] = ts("Sorry, we couldn't find that user")
|
||||||
|
redirect_back_or_default(root_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_instance_variables
|
||||||
|
@related_work = RelatedWork.find(params[:id])
|
||||||
|
@child = @related_work.work
|
||||||
|
if @related_work.parent.is_a? (Work)
|
||||||
|
@owners = @related_work.parent.pseuds.map(&:user)
|
||||||
|
@user = current_user if @owners.include?(current_user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
25
app/controllers/serial_works_controller.rb
Normal file
25
app/controllers/serial_works_controller.rb
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Controller for Serial Works
|
||||||
|
class SerialWorksController < ApplicationController
|
||||||
|
|
||||||
|
before_action :load_serial_work
|
||||||
|
before_action :check_ownership
|
||||||
|
|
||||||
|
def load_serial_work
|
||||||
|
@serial_work = SerialWork.find(params[:id])
|
||||||
|
@check_ownership_of = @serial_work.series
|
||||||
|
end
|
||||||
|
|
||||||
|
# DELETE /related_works/1
|
||||||
|
# Updated so if last work in series is deleted redirects to current user works listing instead of throwing 404
|
||||||
|
def destroy
|
||||||
|
last_work = (@serial_work.series.works.count <= 1)
|
||||||
|
|
||||||
|
@serial_work.destroy
|
||||||
|
|
||||||
|
if last_work
|
||||||
|
redirect_to current_user
|
||||||
|
else
|
||||||
|
redirect_to series_path(@serial_work.series)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
156
app/controllers/series_controller.rb
Normal file
156
app/controllers/series_controller.rb
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
class SeriesController < ApplicationController
|
||||||
|
before_action :check_user_status, only: [:new, :create, :edit, :update]
|
||||||
|
before_action :load_series, only: [ :show, :edit, :update, :manage, :destroy, :confirm_delete ]
|
||||||
|
before_action :check_ownership, only: [ :edit, :update, :manage, :destroy, :confirm_delete ]
|
||||||
|
before_action :check_visibility, only: [:show]
|
||||||
|
|
||||||
|
def load_series
|
||||||
|
@series = Series.find_by(id: params[:id])
|
||||||
|
unless @series
|
||||||
|
raise ActiveRecord::RecordNotFound, "Couldn't find series '#{params[:id]}'"
|
||||||
|
end
|
||||||
|
@check_ownership_of = @series
|
||||||
|
@check_visibility_of = @series
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /series
|
||||||
|
# GET /series.xml
|
||||||
|
def index
|
||||||
|
unless params[:user_id]
|
||||||
|
flash[:error] = ts("Whose series did you want to see?")
|
||||||
|
redirect_to(root_path) and return
|
||||||
|
end
|
||||||
|
@user = User.find_by!(login: params[:user_id])
|
||||||
|
@page_subtitle = t(".page_title", username: @user.login)
|
||||||
|
|
||||||
|
@series = if current_user.nil?
|
||||||
|
Series.visible_to_all
|
||||||
|
else
|
||||||
|
Series.visible_to_registered_user
|
||||||
|
end
|
||||||
|
|
||||||
|
if params[:pseud_id]
|
||||||
|
@pseud = @user.pseuds.find_by!(name: params[:pseud_id])
|
||||||
|
@page_subtitle = t(".page_title", username: @pseud.name)
|
||||||
|
@series = @series.exclude_anonymous.for_pseud(@pseud)
|
||||||
|
else
|
||||||
|
@series = @series.exclude_anonymous.for_user(@user)
|
||||||
|
end
|
||||||
|
@series = @series.paginate(page: params[:page])
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /series/1
|
||||||
|
# GET /series/1.xml
|
||||||
|
def show
|
||||||
|
@works = @series.works_in_order.posted.select(&:visible?).paginate(page: params[:page])
|
||||||
|
|
||||||
|
# sets the page title with the data for the series
|
||||||
|
if @series.unrevealed?
|
||||||
|
@page_subtitle = t(".unrevealed_series")
|
||||||
|
else
|
||||||
|
@page_title = get_page_title(@series.allfandoms.collect(&:name).join(", "), @series.anonymous? ? t(".anonymous") : @series.allpseuds.collect(&:byline).join(", "), @series.title)
|
||||||
|
end
|
||||||
|
|
||||||
|
if current_user.respond_to?(:subscriptions)
|
||||||
|
@subscription = current_user.subscriptions.where(subscribable_id: @series.id,
|
||||||
|
subscribable_type: 'Series').first ||
|
||||||
|
current_user.subscriptions.build(subscribable: @series)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /series/new
|
||||||
|
# GET /series/new.xml
|
||||||
|
def new
|
||||||
|
@series = Series.new
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /series/1/edit
|
||||||
|
def edit
|
||||||
|
if params["remove"] == "me"
|
||||||
|
pseuds_with_author_removed = @series.pseuds - current_user.pseuds
|
||||||
|
if pseuds_with_author_removed.empty?
|
||||||
|
redirect_to controller: 'orphans', action: 'new', series_id: @series.id
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
@series.remove_author(current_user)
|
||||||
|
flash[:notice] = ts("You have been removed as a creator from the series and its works.")
|
||||||
|
redirect_to @series
|
||||||
|
rescue Exception => error
|
||||||
|
flash[:error] = error.message
|
||||||
|
redirect_to @series
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /series/1/manage
|
||||||
|
def manage
|
||||||
|
@serial_works = @series.serial_works.includes(:work).order(:position)
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /series
|
||||||
|
# POST /series.xml
|
||||||
|
def create
|
||||||
|
@series = Series.new(series_params)
|
||||||
|
if @series.save
|
||||||
|
flash[:notice] = ts('Series was successfully created.')
|
||||||
|
redirect_to(@series)
|
||||||
|
else
|
||||||
|
render action: "new"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# PUT /series/1
|
||||||
|
# PUT /series/1.xml
|
||||||
|
def update
|
||||||
|
@series.attributes = series_params
|
||||||
|
if @series.errors.empty? && @series.save
|
||||||
|
flash[:notice] = ts('Series was successfully updated.')
|
||||||
|
redirect_to(@series)
|
||||||
|
else
|
||||||
|
render action: "edit"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_positions
|
||||||
|
if params[:serial_works]
|
||||||
|
@series = Series.find(params[:id])
|
||||||
|
@series.reorder_list(params[:serial_works])
|
||||||
|
flash[:notice] = ts("Series order has been successfully updated.")
|
||||||
|
elsif params[:serial]
|
||||||
|
params[:serial].each_with_index do |id, position|
|
||||||
|
SerialWork.update(id, position: position + 1)
|
||||||
|
(@serial_works ||= []) << SerialWork.find(id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
respond_to do |format|
|
||||||
|
format.html { redirect_to series_path(@series) and return }
|
||||||
|
format.json { head :ok }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /series/1/confirm_delete
|
||||||
|
def confirm_delete
|
||||||
|
end
|
||||||
|
|
||||||
|
# DELETE /series/1
|
||||||
|
# DELETE /series/1.xml
|
||||||
|
def destroy
|
||||||
|
if @series.destroy
|
||||||
|
flash[:notice] = ts("Series was successfully deleted.")
|
||||||
|
redirect_to(current_user)
|
||||||
|
else
|
||||||
|
flash[:error] = ts("Sorry, we couldn't delete the series. Please try again.")
|
||||||
|
redirect_to(@series)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def series_params
|
||||||
|
params.require(:series).permit(
|
||||||
|
:title, :summary, :series_notes, :complete,
|
||||||
|
author_attributes: [:byline, ids: [], coauthors: []]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
223
app/controllers/skins_controller.rb
Normal file
223
app/controllers/skins_controller.rb
Normal file
|
|
@ -0,0 +1,223 @@
|
||||||
|
class SkinsController < ApplicationController
|
||||||
|
before_action :users_only, only: [:new, :create, :destroy]
|
||||||
|
before_action :load_skin, except: [:index, :new, :create, :unset]
|
||||||
|
before_action :check_ownership_or_admin, only: [:edit, :update]
|
||||||
|
before_action :check_ownership, only: [:confirm_delete, :destroy]
|
||||||
|
before_action :check_visibility, only: [:show]
|
||||||
|
before_action :check_editability, only: [:edit, :update, :confirm_delete, :destroy]
|
||||||
|
|
||||||
|
#### ACTIONS
|
||||||
|
|
||||||
|
# GET /skins
|
||||||
|
def index
|
||||||
|
is_work_skin = params[:skin_type] && params[:skin_type] == "WorkSkin"
|
||||||
|
if current_user && current_user.is_a?(User)
|
||||||
|
@preference = current_user.preference
|
||||||
|
end
|
||||||
|
if params[:user_id] && (@user = User.find_by(login: params[:user_id]))
|
||||||
|
redirect_to new_user_session_path and return unless logged_in?
|
||||||
|
if @user != current_user
|
||||||
|
flash[:error] = "You can only browse your own skins and approved public skins."
|
||||||
|
redirect_to skins_path and return
|
||||||
|
end
|
||||||
|
if is_work_skin
|
||||||
|
@skins = @user.work_skins.sort_by_recent.includes(:author).with_attached_icon
|
||||||
|
@title = ts('My Work Skins')
|
||||||
|
else
|
||||||
|
@skins = @user.skins.site_skins.sort_by_recent.includes(:author).with_attached_icon
|
||||||
|
@title = ts('My Site Skins')
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if is_work_skin
|
||||||
|
@skins = WorkSkin.approved_skins.sort_by_recent_featured.includes(:author).with_attached_icon
|
||||||
|
@title = ts('Public Work Skins')
|
||||||
|
else
|
||||||
|
@skins = if logged_in?
|
||||||
|
Skin.approved_skins.usable.site_skins.sort_by_recent_featured.with_attached_icon
|
||||||
|
else
|
||||||
|
Skin.approved_skins.usable.site_skins.cached.sort_by_recent_featured.with_attached_icon
|
||||||
|
end
|
||||||
|
@title = ts('Public Site Skins')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /skins/1
|
||||||
|
def show
|
||||||
|
@page_subtitle = @skin.title.html_safe
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /skins/new
|
||||||
|
def new
|
||||||
|
@skin = Skin.new
|
||||||
|
if params[:wizard]
|
||||||
|
render :new_wizard
|
||||||
|
else
|
||||||
|
render :new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /skins
|
||||||
|
def create
|
||||||
|
unless params[:skin_type].nil? || params[:skin_type] && %w(Skin WorkSkin).include?(params[:skin_type])
|
||||||
|
flash[:error] = ts("What kind of skin did you want to create?")
|
||||||
|
redirect_to :new and return
|
||||||
|
end
|
||||||
|
loaded = load_archive_parents unless params[:skin_type] && params[:skin_type] == 'WorkSkin'
|
||||||
|
if params[:skin_type] == "WorkSkin"
|
||||||
|
@skin = WorkSkin.new(skin_params)
|
||||||
|
else
|
||||||
|
@skin = Skin.new(skin_params)
|
||||||
|
end
|
||||||
|
@skin.author = current_user
|
||||||
|
if @skin.save
|
||||||
|
flash[:notice] = ts("Skin was successfully created.")
|
||||||
|
if loaded
|
||||||
|
flash[:notice] += ts(" We've added all the archive skin components as parents. You probably want to remove some of them now!")
|
||||||
|
redirect_to edit_skin_path(@skin)
|
||||||
|
else
|
||||||
|
redirect_to skin_path(@skin)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if params[:wizard]
|
||||||
|
render :new_wizard
|
||||||
|
else
|
||||||
|
render :new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /skins/1/edit
|
||||||
|
def edit
|
||||||
|
authorize @skin if logged_in_as_admin?
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
authorize @skin if logged_in_as_admin?
|
||||||
|
|
||||||
|
loaded = load_archive_parents
|
||||||
|
if @skin.update(skin_params)
|
||||||
|
@skin.cache! if @skin.cached?
|
||||||
|
@skin.recache_children!
|
||||||
|
flash[:notice] = ts("Skin was successfully updated.")
|
||||||
|
if loaded
|
||||||
|
if flash[:error].present?
|
||||||
|
flash[:notice] = ts("Any other edits were saved.")
|
||||||
|
else
|
||||||
|
flash[:notice] += ts(" We've added all the archive skin components as parents. You probably want to remove some of them now!")
|
||||||
|
end
|
||||||
|
redirect_to edit_skin_path(@skin)
|
||||||
|
else
|
||||||
|
redirect_to @skin
|
||||||
|
end
|
||||||
|
else
|
||||||
|
render action: "edit"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Get /skins/1/preview
|
||||||
|
def preview
|
||||||
|
flash[:notice] = []
|
||||||
|
flash[:notice] << ts("You are previewing the skin %{title}. This is a randomly chosen page.", title: @skin.title)
|
||||||
|
flash[:notice] << ts("Go back or click any link to remove the skin.")
|
||||||
|
flash[:notice] << ts("Tip: You can preview any archive page you want by tacking on '?site_skin=[skin_id]' like you can see in the url above.")
|
||||||
|
flash[:notice] << "<a href='#{skin_path(@skin)}' class='action' role='button'>".html_safe + ts("Return To Skin To Use") + "</a>".html_safe
|
||||||
|
tag = FilterCount.where("public_works_count BETWEEN 10 AND 20").random_order.first.filter
|
||||||
|
redirect_to tag_works_path(tag, site_skin: @skin.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set
|
||||||
|
if @skin.cached?
|
||||||
|
flash[:notice] = ts("The skin %{title} has been set. This will last for your current session.", title: @skin.title)
|
||||||
|
session[:site_skin] = @skin.id
|
||||||
|
else
|
||||||
|
flash[:error] = ts("Sorry, but only certain skins can be used this way (for performance reasons). Please drop a support request if you'd like %{title} to be added!", title: @skin.title)
|
||||||
|
end
|
||||||
|
redirect_back_or_default @skin
|
||||||
|
end
|
||||||
|
|
||||||
|
def unset
|
||||||
|
session[:site_skin] = nil
|
||||||
|
if logged_in? && current_user.preference
|
||||||
|
current_user.preference.skin_id = AdminSetting.default_skin_id
|
||||||
|
current_user.preference.save
|
||||||
|
end
|
||||||
|
flash[:notice] = ts("You are now using the default Archive skin again!")
|
||||||
|
redirect_back_or_default "/"
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /skins/1/confirm_delete
|
||||||
|
def confirm_delete
|
||||||
|
end
|
||||||
|
|
||||||
|
# DELETE /skins/1
|
||||||
|
def destroy
|
||||||
|
@skin = Skin.find_by(id: params[:id])
|
||||||
|
begin
|
||||||
|
@skin.destroy
|
||||||
|
flash[:notice] = ts("The skin was deleted.")
|
||||||
|
rescue
|
||||||
|
flash[:error] = ts("We couldn't delete that right now, sorry! Please try again later.")
|
||||||
|
end
|
||||||
|
|
||||||
|
if current_user && current_user.is_a?(User) && current_user.preference.skin_id == @skin.id
|
||||||
|
current_user.preference.skin_id = AdminSetting.default_skin_id
|
||||||
|
current_user.preference.save
|
||||||
|
end
|
||||||
|
redirect_to user_skins_path(current_user) rescue redirect_to skins_path
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def skin_params
|
||||||
|
params.require(:skin).permit(
|
||||||
|
:title, :description, :public, :css, :role, :ie_condition, :unusable,
|
||||||
|
:font, :base_em, :margin, :paragraph_margin, :background_color,
|
||||||
|
:foreground_color, :headercolor, :accent_color, :icon,
|
||||||
|
media: [],
|
||||||
|
skin_parents_attributes: [
|
||||||
|
:id, :position, :parent_skin_id, :parent_skin_title, :_destroy
|
||||||
|
]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_skin
|
||||||
|
@skin = Skin.find_by(id: params[:id])
|
||||||
|
unless @skin
|
||||||
|
flash[:error] = "Skin not found"
|
||||||
|
redirect_to skins_path and return
|
||||||
|
end
|
||||||
|
@check_ownership_of = @skin
|
||||||
|
@check_visibility_of = @skin
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_editability
|
||||||
|
unless @skin.editable?
|
||||||
|
flash[:error] = ts("Sorry, you don't have permission to edit this skin")
|
||||||
|
redirect_to @skin
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# if we've been asked to load the archive parents, we do so and add them to params
|
||||||
|
def load_archive_parents
|
||||||
|
if params[:add_site_parents]
|
||||||
|
params[:skin][:skin_parents_attributes] ||= ActionController::Parameters.new
|
||||||
|
archive_parents = Skin.get_current_site_skin.get_all_parents
|
||||||
|
skin_parent_titles = params[:skin][:skin_parents_attributes].values.map { |v| v[:parent_skin_title] }
|
||||||
|
skin_parents = skin_parent_titles.empty? ? [] : Skin.where(title: skin_parent_titles).pluck(:id)
|
||||||
|
skin_parents += @skin.get_all_parents.collect(&:id) if @skin
|
||||||
|
unless (skin_parents.uniq & archive_parents.map(&:id)).empty?
|
||||||
|
flash[:error] = ts("You already have some of the archive components as parents, so we couldn't load the others. Please remove the existing components first if you really want to do this!")
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
last_position = params[:skin][:skin_parents_attributes]&.keys&.map(&:to_i)&.max || 0
|
||||||
|
archive_parents.each do |parent_skin|
|
||||||
|
last_position += 1
|
||||||
|
new_skin_parent_hash = ActionController::Parameters.new({ position: last_position, parent_skin_id: parent_skin.id })
|
||||||
|
params[:skin][:skin_parents_attributes].merge!({last_position.to_s => new_skin_parent_hash})
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
130
app/controllers/stats_controller.rb
Normal file
130
app/controllers/stats_controller.rb
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
class StatsController < ApplicationController
|
||||||
|
|
||||||
|
before_action :users_only
|
||||||
|
before_action :load_user
|
||||||
|
before_action :check_ownership
|
||||||
|
|
||||||
|
# only the current user
|
||||||
|
def load_user
|
||||||
|
@user = current_user
|
||||||
|
@check_ownership_of = @user
|
||||||
|
end
|
||||||
|
|
||||||
|
# gather statistics for the user on all their works
|
||||||
|
def index
|
||||||
|
user_works = Work.joins(pseuds: :user).where(users: { id: @user.id }).where(posted: true)
|
||||||
|
user_chapters = Chapter.joins(pseuds: :user).where(users: { id: @user.id }).where(posted: true)
|
||||||
|
work_query = user_works
|
||||||
|
.joins(:taggings)
|
||||||
|
.joins("inner join tags on taggings.tagger_id = tags.id AND tags.type = 'Fandom'")
|
||||||
|
.select("distinct tags.name as fandom, works.id as id, works.title as title")
|
||||||
|
|
||||||
|
# sort
|
||||||
|
|
||||||
|
# NOTE: Because we are going to be eval'ing the @sort variable later we MUST make sure that its content is
|
||||||
|
# checked against the allowlist of valid options
|
||||||
|
sort_options = %w[hits date kudos.count comment_thread_count bookmarks.count subscriptions.count word_count]
|
||||||
|
@sort = sort_options.include?(params[:sort_column]) ? params[:sort_column] : "hits"
|
||||||
|
|
||||||
|
@dir = params[:sort_direction] == "ASC" ? "ASC" : "DESC"
|
||||||
|
params[:sort_column] = @sort
|
||||||
|
params[:sort_direction] = @dir
|
||||||
|
|
||||||
|
# gather works and sort by specified count
|
||||||
|
@years = ["All Years"] + user_chapters.pluck(:published_at).map { |date| date.year.to_s }
|
||||||
|
.uniq.sort
|
||||||
|
@current_year = @years.include?(params[:year]) ? params[:year] : "All Years"
|
||||||
|
if @current_year == "All Years"
|
||||||
|
work_query = work_query.select("works.revised_at as date, works.word_count as word_count")
|
||||||
|
else
|
||||||
|
next_year = @current_year.to_i + 1
|
||||||
|
start_date = DateTime.parse("01/01/#{@current_year}")
|
||||||
|
end_date = DateTime.parse("01/01/#{next_year}")
|
||||||
|
work_query = work_query
|
||||||
|
.joins(:chapters)
|
||||||
|
.where("chapters.posted = 1 AND chapters.published_at >= ? AND chapters.published_at < ?", start_date, end_date)
|
||||||
|
.select("CONVERT(MAX(chapters.published_at), datetime) as date, SUM(chapters.word_count) as word_count")
|
||||||
|
.group(:id, :fandom)
|
||||||
|
end
|
||||||
|
works = work_query.all.sort_by { |w| @dir == "ASC" ? (stat_element(w, @sort) || 0) : (0 - (stat_element(w, @sort) || 0).to_i) }
|
||||||
|
|
||||||
|
# on the off-chance a new user decides to look at their stats and have no works
|
||||||
|
render "no_stats" and return if works.blank?
|
||||||
|
|
||||||
|
# group by fandom or flat view
|
||||||
|
if params[:flat_view]
|
||||||
|
@works = {ts("All Fandoms") => works.uniq}
|
||||||
|
else
|
||||||
|
@works = works.group_by(&:fandom)
|
||||||
|
end
|
||||||
|
|
||||||
|
# gather totals for all works
|
||||||
|
@totals = {}
|
||||||
|
(sort_options - ["date"]).each do |value|
|
||||||
|
# the inject is used to collect the sum in the "result" variable as we iterate over all the works
|
||||||
|
@totals[value.split(".")[0].to_sym] = works.uniq.inject(0) { |result, work| result + (stat_element(work, value) || 0) } # sum the works
|
||||||
|
end
|
||||||
|
@totals[:user_subscriptions] = Subscription.where(subscribable_id: @user.id, subscribable_type: 'User').count
|
||||||
|
|
||||||
|
# graph top 5 works
|
||||||
|
@chart_data = GoogleVisualr::DataTable.new
|
||||||
|
@chart_data.new_column('string', 'Title')
|
||||||
|
|
||||||
|
chart_col = @sort == "date" ? "hits" : @sort
|
||||||
|
chart_col_title = chart_col.split(".")[0].titleize == "Comments" ? ts("Comment Threads") : chart_col.split(".")[0].titleize
|
||||||
|
if @sort == "date" && @dir == "ASC"
|
||||||
|
chart_title = ts("Oldest")
|
||||||
|
elsif @sort == "date" && @dir == "DESC"
|
||||||
|
chart_title = ts("Most Recent")
|
||||||
|
elsif @dir == "ASC"
|
||||||
|
chart_title = ts("Bottom Five By #{chart_col_title}")
|
||||||
|
else
|
||||||
|
chart_title = ts("Top Five By #{chart_col_title}")
|
||||||
|
end
|
||||||
|
@chart_data.new_column('number', chart_col_title)
|
||||||
|
|
||||||
|
# Add Rows and Values
|
||||||
|
@chart_data.add_rows(works.uniq[0..4].map { |w| [w.title, stat_element(w, chart_col)] })
|
||||||
|
|
||||||
|
# image version of bar chart
|
||||||
|
# opts from here: http://code.google.com/apis/chart/image/docs/gallery/bar_charts.html
|
||||||
|
@image_chart = GoogleVisualr::Image::BarChart.new(@chart_data, {isVertical: true}).uri({
|
||||||
|
chtt: chart_title,
|
||||||
|
chs: "800x350",
|
||||||
|
chbh: "a",
|
||||||
|
chxt: "x",
|
||||||
|
chm: "N,000000,0,-1,11"
|
||||||
|
})
|
||||||
|
|
||||||
|
options = {
|
||||||
|
colors: ["#993333"],
|
||||||
|
title: chart_title,
|
||||||
|
vAxis: {
|
||||||
|
viewWindow: { min: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@chart = GoogleVisualr::Interactive::ColumnChart.new(@chart_data, options)
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def stat_element(work, element)
|
||||||
|
case element.downcase
|
||||||
|
when "date"
|
||||||
|
work.date
|
||||||
|
when "hits"
|
||||||
|
work.hits
|
||||||
|
when "kudos.count"
|
||||||
|
work.kudos.count
|
||||||
|
when "comment_thread_count"
|
||||||
|
work.comment_thread_count
|
||||||
|
when "bookmarks.count"
|
||||||
|
work.bookmarks.count
|
||||||
|
when "subscriptions.count"
|
||||||
|
work.subscriptions.count
|
||||||
|
when "word_count"
|
||||||
|
work.word_count
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
111
app/controllers/statuses_controller.rb
Normal file
111
app/controllers/statuses_controller.rb
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
class StatusesController < ApplicationController
|
||||||
|
before_action :users_only, except: [:index, :show, :timeline]
|
||||||
|
before_action :set_status, only: [:show, :edit, :update, :destroy]
|
||||||
|
before_action :set_user, except: [:timeline]
|
||||||
|
before_action :check_ownership, only: [:edit, :update, :destroy]
|
||||||
|
|
||||||
|
|
||||||
|
def redirect_to_current_user
|
||||||
|
if current_user
|
||||||
|
redirect_to user_status_path(current_user)
|
||||||
|
else
|
||||||
|
redirect_to new_user_session_path, alert: "Please log in to view statuses."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
def check_ownership
|
||||||
|
unless @status && @status.user == current_user
|
||||||
|
redirect_to user_status_path(@user || current_user), alert: "You don't have permission to do that silly!"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
@user = current_user
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@user = current_user
|
||||||
|
if @status.update(status_params)
|
||||||
|
redirect_to user_status_path(@user, @status), notice: "Status updated!"
|
||||||
|
else
|
||||||
|
render :edit
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
end
|
||||||
|
def destroy
|
||||||
|
@status.destroy
|
||||||
|
redirect_to user_statuses_path(@user), notice: "Status deleted."
|
||||||
|
end
|
||||||
|
|
||||||
|
def redirect_to_current_user_new
|
||||||
|
if current_user
|
||||||
|
redirect_to new_user_status_path(current_user)
|
||||||
|
else
|
||||||
|
redirect_to new_user_session_path, alert: "Please log in to create a status."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
def index
|
||||||
|
if params[:user_id].present?
|
||||||
|
@user = User.find_by(id: params[:user_id]) || User.find_by(login: params[:user_id])
|
||||||
|
end
|
||||||
|
if @user
|
||||||
|
@statuses = @user.statuses.order(created_at: :desc)
|
||||||
|
else
|
||||||
|
@statuses = []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
def new
|
||||||
|
if @user != current_user
|
||||||
|
redirect_to user_statuses_path(current_user, @status), alert: "You can't do that silly!"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
@status = current_user.statuses.new
|
||||||
|
end
|
||||||
|
def create
|
||||||
|
if @user != current_user
|
||||||
|
redirect_to user_status_path(current_user), alert: "You can't do that silly!"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
@status = current_user.statuses.new(status_params)
|
||||||
|
if @status.save
|
||||||
|
redirect_to user_statuses_path(current_user, @status), notice: "Status created!"
|
||||||
|
else
|
||||||
|
render :new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
def timeline
|
||||||
|
@statuses = Status.includes(:user, :icon_attachment)
|
||||||
|
.order(created_at: :desc)
|
||||||
|
end
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_status
|
||||||
|
return unless params[:id].present?
|
||||||
|
|
||||||
|
@status = Status.find_by(id: params[:id])
|
||||||
|
unless @status
|
||||||
|
redirect_to user_statuses_path(current_user), alert: "Status not found."
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
@user = @status.user
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_user
|
||||||
|
return if @user.present?
|
||||||
|
@user = if params[:user_id].present?
|
||||||
|
User.find_by(id: params[:user_id]) || User.find_by(login: params[:user_id])
|
||||||
|
else
|
||||||
|
current_user
|
||||||
|
end
|
||||||
|
|
||||||
|
unless @user
|
||||||
|
redirect_to root_path, alert: "User not found."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def status_params
|
||||||
|
params.require(:status).permit(:icon, :text, :mood, :music)
|
||||||
|
end
|
||||||
|
end
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue