Merge master

This commit is contained in:
Neil Lalonde 2018-01-03 16:49:06 -05:00
commit f83a39f8ba
20675 changed files with 207959 additions and 70492 deletions

View File

@ -11,7 +11,6 @@ lib/javascripts/messageformat.js
lib/javascripts/moment.js
lib/javascripts/moment_locale/
lib/highlight_js/
lib/es6_module_transpiler/support/es6-module-transpiler.js
public/javascripts/
spec/phantom_js/smoke_test.js
vendor/

View File

@ -14,23 +14,19 @@
{"Ember":true,
"jQuery":true,
"$":true,
"QUnit":true,
"RSVP":true,
"Discourse":true,
"Em":true,
"Handlebars":true,
"I18n":true,
"bootbox":true,
"module":true,
"moduleFor":true,
"moduleForComponent":true,
"Pretender":true,
"sandbox":true,
"controllerFor":true,
"test":true,
"ok":true,
"not":true,
"expect":true,
"equal":true,
"visit":true,
"andThen":true,
"click":true,
@ -45,18 +41,21 @@
"visible":true,
"invisible":true,
"asyncRender":true,
"selectDropdown":true,
"selectKit":true,
"expandSelectKit":true,
"collapseSelectKit":true,
"selectKitSelectRowByValue":true,
"selectKitSelectRowByName":true,
"selectKitSelectRowByIndex":true,
"selectKitSelectNoneRow":true,
"selectKitFillInFilter":true,
"asyncTestDiscourse":true,
"fixture":true,
"find":true,
"sinon":true,
"moment":true,
"start":true,
"_":true,
"alert":true,
"containsInstance":true,
"deepEqual":true,
"notEqual":true,
"define":true,
"require":true,
"requirejs":true,
@ -100,7 +99,9 @@
"wrap-iife": [
2,
"inside"
]
],
"no-mixed-spaces-and-tabs": 2,
"no-trailing-spaces": 2
},
"parser": "babel-eslint"
}

7
.gitignore vendored
View File

@ -36,6 +36,7 @@ config/discourse.conf
*.sql.gz
/db/*.sqlite3
/db/structure.sql
/db/schema.rb
# Ignore all logfiles and tempfiles.
/log/*.log
@ -43,6 +44,7 @@ config/discourse.conf
/logfile
log/
bootsnap-load-path-cache
bootsnap-compile-cache/
# Ignore plugins except for the bundled ones.
/plugins/*
@ -52,6 +54,7 @@ bootsnap-load-path-cache
!/plugins/discourse-details/
!/plugins/discourse-nginx-performance-report
!/plugins/discourse-narrative-bot
!/plugins/discourse-presence
/plugins/*/auto_generated/
/spec/fixtures/plugins/my_plugin/auto_generated
@ -116,3 +119,7 @@ vendor/bundle/*
#ignore jetbrains ide file
*.iml
# ignore nodejs files
/node_modules
/package-lock.json

View File

@ -1,12 +0,0 @@
skip_missing_workers: true
allow_lossy: false
# PNG
advpng: false
optipng:
level: 2
pngcrush: false
pngout: false
pngquant: false
# JPG
jpegrecompress: false
timeout: 15

23
.overcommit.yml Normal file
View File

@ -0,0 +1,23 @@
# Use this file to configure the Overcommit hooks you wish to use. This will
# extend the default configuration defined in:
# https://github.com/brigade/overcommit/blob/master/config/default.yml
#
# At the topmost level of this YAML file is a key representing type of hook
# being run (e.g. pre-commit, commit-msg, etc.). Within each type you can
# customize each hook, such as whether to only run it on certain files (via
# `include`), whether to only display output if it fails (via `quiet`), etc.
#
# For a complete list of hooks, see:
# https://github.com/brigade/overcommit/tree/master/lib/overcommit/hook
#
# For a complete list of options that you can use to customize hooks, see:
# https://github.com/brigade/overcommit#configuration
PreCommit:
RuboCop:
enabled: true
command: ['bundle', 'exec', 'rubocop']
EsLint:
enabled: true
command: ['eslint', '--ext', '.es6', '-f', 'compact']
include: '**/*.es6'

View File

@ -1,14 +1,113 @@
AllCops:
TargetRubyVersion: 2.3
TargetRubyVersion: 2.4
DisabledByDefault: true
Exclude:
- 'db/schema.rb'
- 'bundle/**/*'
- 'vendor/**/*'
- 'node_modules/**/*'
- 'public/**/*'
Metrics/LineLength:
Max: 120
# Prefer &&/|| over and/or.
Style/AndOr:
Enabled: true
Metrics/MethodLength:
# Do not use braces for hash literals when they are the last argument of a
# method call.
Style/BracesAroundHashParameters:
Enabled: true
# Align `when` with `case`.
Layout/CaseIndentation:
Enabled: true
# Align comments with method definitions.
Layout/CommentIndentation:
Enabled: true
# No extra empty lines.
Layout/EmptyLines:
Enabled: true
# Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }.
Style/HashSyntax:
Enabled: true
# Two spaces, no tabs (for indentation).
Layout/IndentationWidth:
Enabled: true
Layout/SpaceAfterColon:
Enabled: true
Layout/SpaceAfterComma:
Enabled: true
Layout/SpaceAroundEqualsInParameterDefault:
Enabled: true
Layout/SpaceAroundKeyword:
Enabled: true
Layout/SpaceAroundOperators:
Enabled: true
Layout/SpaceBeforeFirstArg:
Enabled: true
# Defining a method with parameters needs parentheses.
Style/MethodDefParentheses:
Enabled: true
# Use `foo {}` not `foo{}`.
Layout/SpaceBeforeBlockBraces:
Enabled: true
# Use `foo { bar }` not `foo {bar}`.
Layout/SpaceInsideBlockBraces:
Enabled: true
# Use `{ a: 1 }` not `{a:1}`.
Layout/SpaceInsideHashLiteralBraces:
Enabled: true
Layout/SpaceInsideParens:
Enabled: true
# Detect hard tabs, no hard tabs.
Layout/Tab:
Enabled: true
# Blank lines should not have any spaces.
Layout/TrailingBlankLines:
Enabled: true
# No trailing whitespace.
Layout/TrailingWhitespace:
Enabled: true
Lint/Debugger:
Enabled: true
Lint/BlockAlignment:
Enabled: true
# Align `end` with the matching keyword or starting expression except for
# assignments, where it should be aligned with the LHS.
Lint/EndAlignment:
Enabled: true
EnforcedStyleAlignWith: variable
# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
Lint/RequireParentheses:
Enabled: true
Layout/MultilineMethodCallIndentation:
Enabled: true
EnforcedStyle: indented
Layout/AlignHash:
Enabled: true
Bundler/OrderedGems:
Enabled: false
Style/Documentation:
Enabled: false
Style/FrozenStringLiteralComment:
Enabled: False

View File

@ -10,6 +10,7 @@ env:
- "RAILS_MASTER=0 QUNIT_RUN=0 RUN_LINT=1"
addons:
chrome: stable
postgresql: 9.5
apt:
packages:
@ -20,8 +21,11 @@ addons:
matrix:
fast_finish: true
allow_failures:
- rvm: 2.5.0
rvm:
- 2.5.0
- 2.4.2
- 2.3.4
@ -44,12 +48,14 @@ before_install:
- git clone --depth=1 https://github.com/discourse/discourse-canned-replies.git plugins/discourse-canned-replies
- git clone --depth=1 https://github.com/discourse/discourse-chat-integration.git plugins/discourse-chat-integration
- git clone --depth=1 https://github.com/discourse/discourse-assign.git plugins/discourse-assign
- git clone --depth=1 https://github.com/discourse/discourse-patreon.git plugins/discourse-patreon
- export PATH=$HOME/.yarn/bin:$PATH
install:
- bash -c "if [ '$RAILS_MASTER' == '1' ]; then bundle update --retry=3 --jobs=3 arel rails seed-fu; fi"
- bash -c "if [ '$RAILS_MASTER' == '0' ]; then bundle install --without development --deployment --retry=3 --jobs=3; fi"
- bash -c "if [ '$RUN_LINT' == '1' ]; then yarn global add eslint babel-eslint; fi"
- bash -c "if [ '$QUNIT_RUN' == '1' ]; then yarn install --dev; fi"
script:
- |
@ -65,7 +71,8 @@ script:
bundle exec rake db:create db:migrate
if [ '$QUNIT_RUN' == '1' ]; then
LOAD_PLUGINS=1 bundle exec rake qunit:test['400000']
bundle exec rake qunit:test['400000'] && \
bundle exec rake plugin:spec
else
bundle exec rspec && bundle exec rake plugin:spec
fi

View File

@ -26,12 +26,6 @@ source_file = plugins/poll/config/locales/server.en.yml
source_lang = en
type = YML
[discourse-org.imgurserverenyml]
file_filter = vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.<lang>.yml
source_file = vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.en.yml
source_lang = en
type = YML
[discourse-org.narrativeclientenyml]
file_filter = plugins/discourse-narrative-bot/config/locales/client.<lang>.yml
source_file = plugins/discourse-narrative-bot/config/locales/client.en.yml
@ -44,6 +38,18 @@ source_file = plugins/discourse-narrative-bot/config/locales/server.en.yml
source_lang = en
type = YML
[discourse-org.discourse-presenceclientenyml]
file_filter = plugins/discourse-presence/config/locales/client.<lang>.yml
source_file = plugins/discourse-presence/config/locales/client.en.yml
source_lang = en
type = YML
[discourse-org.discourse-presenceserverenyml]
file_filter = plugins/discourse-presence/config/locales/server.<lang>.yml
source_file = plugins/discourse-presence/config/locales/server.en.yml
source_lang = en
type = YML
[discourse-org.403html]
file_filter = public/403.<lang>.html
source_file = public/403.html

View File

@ -14,6 +14,3 @@ brew 'postgresql'
# install the Redis datastore
brew 'redis'
# install headless Javascript testing library
brew 'phantomjs'

77
Gemfile
View File

@ -14,28 +14,21 @@ if rails_master?
gem 'rails', git: 'https://github.com/rails/rails.git'
gem 'seed-fu', git: 'https://github.com/SamSaffron/seed-fu.git', branch: 'discourse'
else
# Rails 5 is going to ship with Action Cable, we have no use for it as
# we already ship MessageBus, AC introduces dependencies on Event Machine,
# Celluloid and Faye Web Sockets.
#
# Note this means upgrading Rails is more annoying, to do so, comment out the
# explicit dependencies, and add gem 'rails', bundle update rails and then
# comment back the explicit dependencies. Leaving this in a comment till we
# upgrade to Rails 5
#
# gem 'activesupport'
# gem 'actionpack'
# gem 'activerecord'
# gem 'actionmailer'
# gem 'activejob'
# gem 'railties'
# gem 'sprockets-rails'
gem 'rails', '~> 4.2'
gem 'seed-fu', '~> 2.3.5'
gem 'actionmailer', '~> 5.1'
gem 'actionpack', '~> 5.1'
gem 'actionview', '~> 5.1'
gem 'activemodel', '~> 5.1'
gem 'activerecord', '~> 5.1'
gem 'activesupport', '~> 5.1'
gem 'railties', '~> 5.1'
gem 'sprockets-rails'
gem 'seed-fu'
end
gem 'mail'
gem 'mime-types', require: 'mime/types/columnar'
gem 'mini_mime'
gem 'mini_suffix'
gem 'hiredis'
gem 'redis', require: ["redis", "redis/connection/hiredis"]
@ -43,7 +36,7 @@ gem 'redis-namespace'
gem 'active_model_serializers', '~> 0.8.3'
gem 'onebox'
gem 'onebox', '1.8.33'
gem 'http_accept_language', '~>2.0.5', require: false
@ -51,7 +44,6 @@ gem 'ember-rails', '0.18.5'
gem 'ember-source'
gem 'ember-handlebars-template', '0.7.5'
gem 'barber'
gem 'babel-transpiler'
gem 'message_bus'
@ -61,28 +53,31 @@ gem 'fast_xs'
gem 'fast_xor'
gem 'fastimage', '2.1.0'
gem 'aws-sdk', require: false
# Forked until https://github.com/sdsykes/fastimage/pull/93 is merged
gem 'discourse_fastimage', require: 'fastimage'
gem 'aws-sdk-s3', require: false
gem 'excon', require: false
gem 'unf', require: false
gem 'email_reply_trimmer', '0.1.6'
gem 'email_reply_trimmer', '0.1.8'
# TODO Use official image_optim gem once https://github.com/toy/image_optim/pull/149
# is merged.
# Forked until https://github.com/toy/image_optim/pull/149 is merged
gem 'discourse_image_optim', require: 'image_optim'
gem 'multi_json'
gem 'mustache'
gem 'nokogiri'
# this may end up deprecating nokogiri
gem 'oga', require: false
gem 'omniauth'
gem 'omniauth-openid'
gem 'openid-redis-store'
gem 'omniauth-facebook'
gem 'omniauth-twitter'
gem 'omniauth-instagram'
# forked while https://github.com/intridea/omniauth-github/pull/41 is being upstreamd
gem 'omniauth-github-discourse', require: 'omniauth-github'
gem 'omniauth-github'
gem 'omniauth-oauth2', require: false
@ -94,21 +89,20 @@ gem 'r2', '~> 0.2.5', require: false
gem 'rake'
gem 'thor', require: false
gem 'rest-client'
gem 'rinku'
gem 'sanitize'
gem 'sidekiq'
# for sidekiq web
gem 'sinatra', require: false
gem 'tilt', require: false
gem 'execjs', require: false
gem 'mini_racer'
gem 'highline', require: false
gem 'rack-protection' # security
# Gems used only for assets and not required
# in production environments by default.
# allow everywhere for now cause we are allowing asset debugging in prd
# Gems used only for assets and not required in production environments by default.
# Allow everywhere for now cause we are allowing asset debugging in production
group :assets do
gem 'uglifier'
gem 'rtlit', require: false # for css rtling
@ -118,9 +112,6 @@ group :test do
gem 'webmock', require: false
gem 'fakeweb', '~> 1.3.0', require: false
gem 'minitest', require: false
gem 'timecop'
# TODO: Remove once we upgrade to Rails 5.
gem 'test_after_commit'
end
group :test, :development do
@ -137,12 +128,13 @@ group :test, :development do
gem 'rspec-rails', require: false
gem 'shoulda', require: false
gem 'rspec-html-matchers'
gem 'spork-rails'
gem 'pry-nav'
gem 'byebug', require: ENV['RM_INFO'].nil?
gem 'rubocop', require: false
end
group :development do
gem 'ruby-prof', require: false
gem 'bullet', require: !!ENV['BULLET']
gem 'better_errors'
gem 'binding_of_caller'
@ -153,7 +145,7 @@ end
# this is an optional gem, it provides a high performance replacement
# to String#blank? a method that is called quite frequently in current
# ActiveRecord, this may change in the future
gem 'fast_blank' #, github: "SamSaffron/fast_blank"
gem 'fast_blank'
# this provides a very efficient lru cache
gem 'lru_redux'
@ -173,22 +165,23 @@ gem 'rbtrace', require: false, platform: :mri
gem 'gc_tracer', require: false, platform: :mri
# required for feed importing and embedding
#
gem 'ruby-readability', require: false
gem 'simple-rss', require: false
gem 'stackprof', require: false, platform: :mri
gem 'memory_profiler', require: false, platform: :mri
gem 'rmmseg-cpp', require: false
gem 'cppjieba_rb', require: false
gem 'lograge', require: false
gem 'logstash-event', require: false
gem 'logstash-logger', require: false
gem 'logster'
gem 'sassc', require: false
if ENV["IMPORT"] == "1"
gem 'mysql2'
gem 'redcarpet'
gem 'sqlite3', '~> 1.3.13'
gem 'ruby-bbcode-to-md', github: 'nlalonde/ruby-bbcode-to-md'
end

View File

@ -1,58 +1,62 @@
GEM
remote: https://rubygems.org/
specs:
actionmailer (4.2.8)
actionpack (= 4.2.8)
actionview (= 4.2.8)
activejob (= 4.2.8)
actionmailer (5.1.4)
actionpack (= 5.1.4)
actionview (= 5.1.4)
activejob (= 5.1.4)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 1.0, >= 1.0.5)
actionpack (4.2.8)
actionview (= 4.2.8)
activesupport (= 4.2.8)
rack (~> 1.6)
rack-test (~> 0.6.2)
rails-dom-testing (~> 1.0, >= 1.0.5)
rails-dom-testing (~> 2.0)
actionpack (5.1.4)
actionview (= 5.1.4)
activesupport (= 5.1.4)
rack (~> 2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (4.2.8)
activesupport (= 4.2.8)
actionview (5.1.4)
activesupport (= 5.1.4)
builder (~> 3.1)
erubis (~> 2.7.0)
rails-dom-testing (~> 1.0, >= 1.0.5)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
active_model_serializers (0.8.3)
activemodel (>= 3.0)
activejob (4.2.8)
activesupport (= 4.2.8)
globalid (>= 0.3.0)
activemodel (4.2.8)
activesupport (= 4.2.8)
builder (~> 3.1)
activerecord (4.2.8)
activemodel (= 4.2.8)
activesupport (= 4.2.8)
arel (~> 6.0)
activesupport (4.2.8)
activejob (5.1.4)
activesupport (= 5.1.4)
globalid (>= 0.3.6)
activemodel (5.1.4)
activesupport (= 5.1.4)
activerecord (5.1.4)
activemodel (= 5.1.4)
activesupport (= 5.1.4)
arel (~> 8.0)
activesupport (5.1.4)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (~> 0.7)
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
addressable (2.5.1)
public_suffix (~> 2.0, >= 2.0.2)
annotate (2.7.1)
annotate (2.7.2)
activerecord (>= 3.2, < 6.0)
rake (>= 10.4, < 12.0)
arel (6.0.4)
aws-sdk (2.5.3)
aws-sdk-resources (= 2.5.3)
aws-sdk-core (2.5.3)
rake (>= 10.4, < 13.0)
ansi (1.5.0)
arel (8.0.0)
ast (2.3.0)
aws-partitions (1.24.0)
aws-sdk-core (3.6.0)
aws-partitions (~> 1.0)
aws-sigv4 (~> 1.0)
jmespath (~> 1.0)
aws-sdk-resources (2.5.3)
aws-sdk-core (= 2.5.3)
babel-source (5.8.34)
babel-transpiler (0.7.0)
babel-source (>= 4.0, < 6)
execjs (~> 2.0)
aws-sdk-kms (1.2.0)
aws-sdk-core (~> 3)
aws-sigv4 (~> 1.0)
aws-sdk-s3 (1.4.0)
aws-sdk-core (~> 3)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.0)
aws-sigv4 (1.0.2)
barber (0.11.2)
ember-source (>= 1.0, < 3)
execjs (>= 1.2, < 3)
@ -62,33 +66,33 @@ GEM
rack (>= 0.9.0)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootsnap (0.3.0)
bootsnap (1.0.0)
msgpack (~> 1.0)
builder (3.2.3)
bullet (5.4.2)
bullet (5.5.1)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.10.0)
byebug (9.0.6)
certified (1.0.0)
coderay (1.1.1)
concurrent-ruby (1.0.5)
connection_pool (2.2.0)
connection_pool (2.2.1)
cppjieba_rb (0.3.0)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.2)
debug_inspector (0.0.2)
debug_inspector (0.0.3)
diff-lcs (1.3)
discourse-qunit-rails (0.0.9)
discourse-qunit-rails (0.0.11)
railties
discourse_fastimage (2.1.0)
discourse_image_optim (0.24.5)
exifr (~> 1.2, >= 1.2.2)
fspath (~> 3.0)
image_size (~> 1.5)
in_threads (~> 1.3)
progress (~> 3.0, >= 3.0.1)
domain_name (0.5.25)
unf (>= 0.0.5, < 1.0.0)
email_reply_trimmer (0.1.6)
email_reply_trimmer (0.1.8)
ember-data-source (2.2.1)
ember-source (>= 1.8, < 3.0)
ember-handlebars-template (0.7.5)
@ -101,9 +105,10 @@ GEM
ember-source (>= 1.1.0)
jquery-rails (>= 1.0.17)
railties (>= 3.1)
ember-source (2.10.0)
ember-source (2.13.3)
erubi (1.6.1)
erubis (2.7.0)
excon (0.55.0)
excon (0.56.0)
execjs (2.7.0)
exifr (1.2.5)
fabrication (2.9.8)
@ -115,68 +120,78 @@ GEM
rake
rake-compiler
fast_xs (0.8.0)
fastimage (2.1.0)
ffi (1.9.18)
flamegraph (0.9.5)
foreman (0.82.0)
foreman (0.84.0)
thor (~> 0.19.1)
fspath (3.1.0)
gc_tracer (1.5.1)
globalid (0.3.7)
activesupport (>= 4.1.0)
globalid (0.4.0)
activesupport (>= 4.2.0)
guess_html_encoding (0.0.11)
hashdiff (0.3.4)
hashie (3.5.5)
highline (1.7.8)
hiredis (0.6.1)
htmlentities (4.3.4)
http-cookie (1.0.2)
domain_name (~> 0.5)
http_accept_language (2.0.5)
i18n (0.8.1)
i18n (0.8.6)
image_size (1.5.0)
in_threads (1.4.0)
jmespath (1.3.1)
jquery-rails (4.2.1)
jquery-rails (4.3.1)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
jwt (1.5.6)
kgio (2.11.0)
libv8 (5.3.332.38.5)
libv8 (5.9.211.38.1)
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
ruby_dep (~> 1.2)
logster (1.2.7)
loofah (2.0.3)
lograge (0.7.1)
actionpack (>= 4, < 5.2)
activesupport (>= 4, < 5.2)
railties (>= 4, < 5.2)
request_store (~> 1.0)
logstash-event (1.2.02)
logstash-logger (0.25.1)
logstash-event (~> 1.2)
logster (1.2.9)
loofah (2.1.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
lru_redux (1.1.0)
mail (2.6.6.rc1)
mail (2.6.6)
mime-types (>= 1.16, < 4)
memory_profiler (0.9.7)
message_bus (2.0.2)
memory_profiler (0.9.8)
message_bus (2.1.1)
rack (>= 1.1.3)
metaclass (0.0.4)
method_source (0.8.2)
mime-types (2.99.3)
mime-types (3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mini_mime (0.1.3)
mini_portile2 (2.3.0)
mini_racer (0.1.9)
libv8 (~> 5.3)
minitest (5.10.1)
mocha (1.1.0)
mini_racer (0.1.11)
libv8 (~> 5.7)
mini_suffix (0.3.0)
ffi (~> 1.9)
minitest (5.10.3)
mocha (1.2.1)
metaclass (~> 0.0.1)
mock_redis (0.15.4)
mock_redis (0.17.3)
moneta (1.0.0)
msgpack (1.1.0)
multi_json (1.12.1)
multi_xml (0.6.0)
multipart-post (2.0.0)
mustache (1.0.5)
netrc (0.11.0)
nokogiri (1.8.1)
mini_portile2 (~> 2.3.0)
nokogumbo (1.4.10)
nokogumbo (1.4.13)
nokogiri
oauth (0.5.1)
oauth2 (1.3.1)
@ -185,15 +200,18 @@ GEM
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
oj (3.0.5)
oga (2.10)
ast
ruby-ll (~> 2.1)
oj (3.1.0)
omniauth (1.6.1)
hashie (>= 3.4.6, < 3.6.0)
rack (>= 1.6.2, < 3)
omniauth-facebook (4.0.0)
omniauth-oauth2 (~> 1.2)
omniauth-github-discourse (1.1.2)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.1)
omniauth-github (1.3.0)
omniauth (~> 1.5)
omniauth-oauth2 (>= 1.4.0, < 2.0)
omniauth-google-oauth2 (0.3.1)
jwt (~> 1.0)
multi_json (~> 1.3)
@ -214,7 +232,7 @@ GEM
omniauth-twitter (1.3.0)
omniauth-oauth (~> 1.1)
rack
onebox (1.8.12)
onebox (1.8.33)
fast_blank (>= 1.0.0)
htmlentities (~> 4.3)
moneta (~> 1.0)
@ -225,7 +243,11 @@ GEM
openid-redis-store (0.0.2)
redis
ruby-openid
pg (0.19.0)
parallel (1.12.0)
parser (2.4.0.0)
ast (~> 2.2)
pg (0.20.0)
powerpack (0.1.1)
progress (3.3.1)
pry (0.10.4)
coderay (~> 1.1.0)
@ -236,128 +258,117 @@ GEM
pry-rails (0.3.4)
pry (>= 0.9.10)
public_suffix (2.0.5)
puma (3.6.0)
puma (3.9.1)
r2 (0.2.6)
rack (1.6.8)
rack-mini-profiler (0.10.4)
rack (2.0.3)
rack-mini-profiler (0.10.7)
rack (>= 1.2.0)
rack-openid (1.3.1)
rack (>= 1.1.0)
ruby-openid (>= 2.1.8)
rack-protection (1.5.3)
rack-protection (2.0.0)
rack
rack-test (0.6.3)
rack (>= 1.0)
rails (4.2.8)
actionmailer (= 4.2.8)
actionpack (= 4.2.8)
actionview (= 4.2.8)
activejob (= 4.2.8)
activemodel (= 4.2.8)
activerecord (= 4.2.8)
activesupport (= 4.2.8)
bundler (>= 1.3.0, < 2.0)
railties (= 4.2.8)
sprockets-rails
rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha)
rails-dom-testing (1.0.8)
activesupport (>= 4.2.0.beta, < 5.0)
nokogiri (~> 1.6)
rails-deprecated_sanitizer (>= 1.0.1)
rack-test (0.7.0)
rack (>= 1.0, < 3)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.0.3)
loofah (~> 2.0)
rails_multisite (1.0.6)
rails (> 4.2, < 5)
railties (4.2.8)
actionpack (= 4.2.8)
activesupport (= 4.2.8)
rails_multisite (1.1.2)
activerecord (> 4.2, < 6)
railties (> 4.2, < 6)
railties (5.1.4)
actionpack (= 5.1.4)
activesupport (= 5.1.4)
method_source
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
raindrops (0.18.0)
rake (11.3.0)
rake-compiler (0.9.9)
rainbow (2.2.2)
rake
rb-fsevent (0.9.7)
rb-inotify (0.9.7)
raindrops (0.19.0)
rake (12.1.0)
rake-compiler (1.0.4)
rake
rb-fsevent (0.9.8)
rb-inotify (0.9.8)
ffi (>= 0.5.0)
rbtrace (0.4.8)
ffi (>= 1.0.6)
msgpack (>= 0.4.3)
trollop (>= 1.16.2)
redis (3.3.3)
redis-namespace (1.5.2)
redis (3.3.5)
redis-namespace (1.5.3)
redis (~> 3.0, >= 3.0.4)
rest-client (1.8.0)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 3.0)
netrc (~> 0.7)
rinku (2.0.0)
rmmseg-cpp (0.2.9)
rspec (3.4.0)
rspec-core (~> 3.4.0)
rspec-expectations (~> 3.4.0)
rspec-mocks (~> 3.4.0)
rspec-core (3.4.4)
rspec-support (~> 3.4.0)
rspec-expectations (3.4.0)
request_store (1.3.2)
rinku (2.0.2)
rspec (3.6.0)
rspec-core (~> 3.6.0)
rspec-expectations (~> 3.6.0)
rspec-mocks (~> 3.6.0)
rspec-core (3.6.0)
rspec-support (~> 3.6.0)
rspec-expectations (3.6.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.4.0)
rspec-html-matchers (0.7.0)
rspec-support (~> 3.6.0)
rspec-html-matchers (0.9.1)
nokogiri (~> 1)
rspec (~> 3)
rspec-mocks (3.4.1)
rspec (>= 3.0.0.a, < 4)
rspec-mocks (3.6.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.4.0)
rspec-rails (3.4.2)
actionpack (>= 3.0, < 4.3)
activesupport (>= 3.0, < 4.3)
railties (>= 3.0, < 4.3)
rspec-core (~> 3.4.0)
rspec-expectations (~> 3.4.0)
rspec-mocks (~> 3.4.0)
rspec-support (~> 3.4.0)
rspec-support (3.4.1)
rspec-support (~> 3.6.0)
rspec-rails (3.6.1)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
rspec-core (~> 3.6.0)
rspec-expectations (~> 3.6.0)
rspec-mocks (~> 3.6.0)
rspec-support (~> 3.6.0)
rspec-support (3.6.0)
rtlit (0.0.5)
rubocop (0.51.0)
parallel (~> 1.10)
parser (>= 2.3.3.1, < 3.0)
powerpack (~> 0.1)
rainbow (>= 2.2.2, < 3.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
ruby-ll (2.1.2)
ansi
ast
ruby-openid (2.7.0)
ruby-prof (0.16.2)
ruby-progressbar (1.9.0)
ruby-readability (0.7.0)
guess_html_encoding (>= 0.0.4)
nokogiri (>= 1.6.0)
ruby_dep (1.5.0)
safe_yaml (1.0.4)
sanitize (4.4.0)
sanitize (4.5.0)
crass (~> 1.0.2)
nokogiri (>= 1.4.4)
nokogumbo (~> 1.4.1)
sass (3.4.23)
sass (3.4.24)
sassc (1.11.2)
bundler
ffi (~> 1.9.6)
sass (>= 3.3.0)
seed-fu (2.3.5)
activerecord (>= 3.1, < 4.3)
activesupport (>= 3.1, < 4.3)
seed-fu (2.3.7)
activerecord (>= 3.1)
activesupport (>= 3.1)
shoulda (3.5.0)
shoulda-context (~> 1.0, >= 1.0.1)
shoulda-matchers (>= 1.4.1, < 3.0)
shoulda-context (1.2.2)
shoulda-matchers (2.8.0)
activesupport (>= 3.0.0)
sidekiq (4.2.4)
sidekiq (5.0.5)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0)
redis (~> 3.2, >= 3.2.1)
simple-rss (1.3.1)
sinatra (1.4.6)
rack (~> 1.4)
rack-protection (~> 1.4)
tilt (>= 1.3, < 3)
redis (>= 3.3.4, < 5)
slop (3.6.0)
spork (1.0.0rc4)
spork-rails (4.0.0)
rails (>= 3.0.0, < 5)
spork (>= 1.0rc0)
sprockets (3.7.1)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
@ -366,21 +377,19 @@ GEM
activesupport (>= 4.0)
sprockets (>= 3.0.0)
stackprof (0.2.10)
test_after_commit (1.1.0)
activerecord (>= 3.2)
thor (0.19.4)
thread_safe (0.3.6)
tilt (2.0.5)
timecop (0.8.1)
tilt (2.0.7)
trollop (2.1.2)
tzinfo (1.2.3)
thread_safe (~> 0.1)
uglifier (3.0.2)
uglifier (3.2.0)
execjs (>= 0.3.0, < 3)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.1)
unicorn (5.3.0)
unf_ext (0.0.7.4)
unicode-display_width (1.3.0)
unicorn (5.3.1)
kgio (~> 2.6)
raindrops (~> 0.7)
uniform_notifier (1.10.0)
@ -393,10 +402,15 @@ PLATFORMS
ruby
DEPENDENCIES
actionmailer (~> 5.1)
actionpack (~> 5.1)
actionview (~> 5.1)
active_model_serializers (~> 0.8.3)
activemodel (~> 5.1)
activerecord (~> 5.1)
activesupport (~> 5.1)
annotate
aws-sdk
babel-transpiler
aws-sdk-s3
barber
better_errors
binding_of_caller
@ -404,9 +418,11 @@ DEPENDENCIES
bullet
byebug
certified
cppjieba_rb
discourse-qunit-rails
discourse_fastimage
discourse_image_optim
email_reply_trimmer (= 0.1.6)
email_reply_trimmer (= 0.1.8)
ember-handlebars-template (= 0.7.5)
ember-rails (= 0.18.5)
ember-source
@ -417,7 +433,6 @@ DEPENDENCIES
fast_blank
fast_xor
fast_xs
fastimage (= 2.1.0)
flamegraph
foreman
gc_tracer
@ -426,29 +441,35 @@ DEPENDENCIES
htmlentities
http_accept_language (~> 2.0.5)
listen
lograge
logstash-event
logstash-logger
logster
lru_redux
mail
memory_profiler
message_bus
mime-types
mini_mime
mini_racer
mini_suffix
minitest
mocha
mock_redis
multi_json
mustache
nokogiri
oga
oj
omniauth
omniauth-facebook
omniauth-github-discourse
omniauth-github
omniauth-google-oauth2
omniauth-instagram
omniauth-oauth2
omniauth-openid
omniauth-twitter
onebox
onebox (= 1.8.33)
openid-redis-store
pg
pry-nav
@ -457,38 +478,35 @@ DEPENDENCIES
r2 (~> 0.2.5)
rack-mini-profiler
rack-protection
rails (~> 4.2)
rails_multisite
railties (~> 5.1)
rake
rb-fsevent
rb-inotify (~> 0.9)
rbtrace
redis
redis-namespace
rest-client
rinku
rmmseg-cpp
rspec
rspec-html-matchers
rspec-rails
rtlit
rubocop
ruby-prof
ruby-readability
sanitize
sassc
seed-fu (~> 2.3.5)
seed-fu
shoulda
sidekiq
simple-rss
sinatra
spork-rails
sprockets-rails
stackprof
test_after_commit
thor
timecop
tilt
uglifier
unf
unicorn
webmock
BUNDLED WITH
1.15.4
1.16.0

View File

@ -9,4 +9,3 @@ Discourse::Application.load_tasks
# this prevents crashes when migrating a database in production in certain
# PostgreSQL configuations when trying to create structure.sql
Rake::Task["db:structure:dump"].clear if Rails.env.production?

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,38 @@
import RestAdapter from 'discourse/adapters/rest';
export default RestAdapter.extend({
pathFor(store, type, findArgs) {
let args = _.merge({ rest_api: true }, findArgs);
delete args.filter;
return `/admin/flags/${findArgs.filter}.json?${$.param(args)}`;
},
afterFindAll(results, helper) {
results.forEach(flag => {
let conversations = [];
flag.post_actions.forEach(pa => {
if (pa.conversation) {
let conversation = {
permalink: pa.permalink,
hasMore: pa.conversation.has_more,
response: {
excerpt: pa.conversation.response.excerpt,
user: helper.lookup('user', pa.conversation.response.user_id)
}
};
if (pa.conversation.reply) {
conversation.reply = {
excerpt: pa.conversation.reply.excerpt,
user: helper.lookup('user', pa.conversation.reply.user_id)
};
}
conversations.push(conversation);
}
});
flag.set('conversations', conversations);
});
return results;
}
});

View File

@ -1,13 +1,14 @@
import loadScript from 'discourse/lib/load-script';
import { observes } from 'ember-addons/ember-computed-decorators';
const LOAD_ASYNC = !Ember.Test;
const LOAD_ASYNC = !Ember.testing;
export default Ember.Component.extend({
mode: 'css',
classNames: ['ace-wrapper'],
_editor: null,
_skipContentChangeEvent: null,
disabled: false,
@observes('editorId')
editorIdChanged() {
@ -30,6 +31,24 @@ export default Ember.Component.extend({
}
},
@observes('disabled')
disabledStateChanged() {
this.changeDisabledState();
},
changeDisabledState() {
const editor = this._editor;
if (editor) {
const disabled = this.get('disabled');
editor.setOptions({
readOnly: disabled,
highlightActiveLine: !disabled,
highlightGutterLine: !disabled
});
editor.container.parentNode.setAttribute("data-disabled", disabled);
}
},
_destroyEditor: function() {
if (this._editor) {
this._editor.destroy();
@ -76,6 +95,7 @@ export default Ember.Component.extend({
this.$().data('editor', editor);
this._editor = editor;
this.changeDisabledState();
$(window).off('ace:resize').on('ace:resize', ()=>{
this.appEvents.trigger('ace:resize');

View File

@ -1,4 +1,4 @@
import { iconHTML } from 'discourse-common/helpers/fa-icon';
import { iconHTML } from 'discourse-common/lib/icon-library';
import { bufferedRender } from 'discourse-common/lib/buffered-render';
export default Ember.Component.extend(bufferedRender({

View File

@ -1,43 +0,0 @@
export default Ember.Component.extend({
tagName: 'div',
_init: function(){
this.$("input").select2({
multiple: true,
width: '100%',
query: function(opts) {
opts.callback({
results: this.get("available").filter(function(o) {
return -1 !== o.name.toLowerCase().indexOf(opts.term.toLowerCase());
}).map(this._format)
});
}.bind(this)
}).on("change", function(evt) {
if (evt.added){
this.triggerAction({
action: "groupAdded",
actionContext: this.get("available").findBy("id", evt.added.id)
});
} else if (evt.removed) {
this.triggerAction({
action:"groupRemoved",
actionContext: evt.removed.id
});
}
}.bind(this));
this._refreshOnReset();
}.on("didInsertElement"),
_format(item) {
return {
"text": item.name,
"id": item.id,
"locked": item.automatic
};
},
_refreshOnReset: function() {
this.$("input").select2("data", this.get("selected").map(this._format));
}.observes("selected")
});

View File

@ -0,0 +1,3 @@
export default Ember.Component.extend({
tagName: ''
});

View File

@ -0,0 +1,19 @@
import { iconHTML } from 'discourse-common/lib/icon-library';
import { bufferedRender } from 'discourse-common/lib/buffered-render';
export default Ember.Component.extend(bufferedRender({
classNames: ['watched-word'],
buildBuffer(buffer) {
buffer.push(iconHTML('times'));
buffer.push(' ' + this.get('word.word'));
},
click() {
this.get('word').destroy().then(() => {
this.sendAction('action', this.get('word'));
}).catch(e => {
bootbox.alert(I18n.t("generic_error_with_reason", {error: `http: ${e.status} - ${e.body}`}));
});;
}
}));

View File

@ -1,5 +1,5 @@
import computed from 'ember-addons/ember-computed-decorators';
import { iconHTML } from 'discourse-common/helpers/fa-icon';
import { iconHTML } from 'discourse-common/lib/icon-library';
import { bufferedRender } from 'discourse-common/lib/buffered-render';
export default Ember.Component.extend(bufferedRender({

View File

@ -0,0 +1,3 @@
export default Ember.Component.extend({
classNames: ['flagged-post-response']
});

View File

@ -0,0 +1,72 @@
import showModal from 'discourse/lib/show-modal';
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
adminTools: Ember.inject.service(),
expanded: false,
suspended: false,
tagName: 'div',
classNameBindings: [
':flagged-post',
'flaggedPost.hidden:hidden-post',
'flaggedPost.deleted'
],
@computed('filter')
canAct(filter) {
return filter === 'active';
},
removeAfter(promise) {
return promise.then(() => {
this.attrs.removePost();
}).catch(() => {
bootbox.alert(I18n.t("admin.flags.error"));
});
},
_spawnModal(name, model, modalClass) {
let controller = showModal(name, { model, admin: true, modalClass });
controller.removeAfter = (p) => this.removeAfter(p);
},
actions: {
removeAfter(promise) {
this.removeAfter(promise);
},
disagree() {
this.removeAfter(this.get('flaggedPost').disagreeFlags());
},
defer() {
this.removeAfter(this.get('flaggedPost').deferFlags());
},
expand() {
this.get('flaggedPost').expandHidden().then(() => {
this.set('expanded', true);
});
},
showModerationHistory() {
this.get('adminTools').showModerationHistory({
filter: 'post',
post_id: this.get('flaggedPost.id')
});
},
showSuspendModal() {
let post = this.get('flaggedPost');
let user = post.get('user');
this.get('adminTools').showSuspendModal(
user,
{
post,
successCallback: result => this.set('suspended', result.suspended)
}
);
}
}
});

View File

@ -1,4 +1,5 @@
import { ajax } from 'discourse/lib/ajax';
import AdminUser from 'admin/models/admin-user';
export default Ember.Component.extend({
classNames: ["ip-lookup"],
@ -44,7 +45,6 @@ export default Ember.Component.extend({
self.set("totalOthersWithSameIP", result.total);
});
const AdminUser = require('admin/models/admin-user').default;
AdminUser.findAll("active", data).then(function (users) {
self.setProperties({
other_accounts: users,

View File

@ -1,54 +0,0 @@
/**
Provide a nice GUI for a pipe-delimited list in the site settings.
@param settingValue is a reference to SiteSetting.value.
@param choices is a reference to SiteSetting.choices
**/
export default Ember.Component.extend({
_select2FormatSelection: function(selectedObject, jqueryWrapper, htmlEscaper) {
var text = selectedObject.text;
if (text.length <= 6) {
jqueryWrapper.closest('li.select2-search-choice').css({"border-bottom": '7px solid #'+text});
}
return htmlEscaper(text);
},
_initializeSelect2: function(){
var options = {
multiple: false,
separator: "|",
tokenSeparators: ["|"],
tags : this.get("choices") || [],
width: 'off',
dropdownCss: this.get("choices") ? {} : {display: 'none'},
selectOnBlur: this.get("choices") ? false : true
};
var settingName = this.get('settingName');
if (typeof settingName === 'string' && settingName.indexOf('colors') > -1) {
options.formatSelection = this._select2FormatSelection;
}
var self = this;
this.$("input").select2(options).on("change", function(obj) {
self.set("settingValue", obj.val.join("|"));
self.refreshSortables();
});
this.refreshSortables();
}.on('didInsertElement'),
refreshOnReset: function() {
this.$("input").select2("val", this.get("settingValue").split("|"));
}.observes("settingValue"),
refreshSortables: function() {
var self = this;
this.$("ul.select2-choices").sortable().on('sortupdate', function() {
self.$("input").select2("onSortEnd");
});
}
});

View File

@ -0,0 +1,3 @@
export default Ember.Component.extend({
tagName: 'tr',
});

View File

@ -1,3 +1,5 @@
import Permalink from 'admin/models/permalink';
export default Ember.Component.extend({
classNames: ['permalink-form'],
formSubmitted: false,
@ -18,8 +20,6 @@ export default Ember.Component.extend({
actions: {
submit: function() {
const Permalink = require('admin/models/permalink').default;
if (!this.get('formSubmitted')) {
const self = this;
self.set('formSubmitted', true);

View File

@ -1,3 +1,4 @@
import { iconHTML } from 'discourse-common/lib/icon-library';
import { bufferedRender } from 'discourse-common/lib/buffered-render';
/*global Resumable:true */
@ -40,7 +41,7 @@ export default Ember.Component.extend(bufferedRender({
buildBuffer(buffer) {
const icon = this.get("isUploading") ? "times" : "upload";
buffer.push(`<i class="fa fa-${icon}"></i>`);
buffer.push(iconHTML(icon));
buffer.push("<span class='ru-label'>" + this.get("text") + "</span>");
buffer.push("<span class='ru-progress' style='width:" + this.get("progress") + "%'></span>");
},

View File

@ -2,6 +2,7 @@ import BufferedContent from 'discourse/mixins/buffered-content';
import SiteSetting from 'admin/models/site-setting';
import { propertyNotEqual } from 'discourse/lib/computed';
import computed from 'ember-addons/ember-computed-decorators';
import { categoryLinkHTML } from 'discourse/helpers/category-link';
const CustomTypes = ['bool', 'enum', 'list', 'url_list', 'host_list', 'category_list', 'value_list'];
@ -11,8 +12,19 @@ export default Ember.Component.extend(BufferedContent, {
dirty: propertyNotEqual('buffered.value', 'setting.value'),
validationMessage: null,
@computed("setting.preview", "buffered.value")
preview(preview, value) {
@computed("setting", "buffered.value")
preview(setting, value) {
// A bit hacky, but allows us to use helpers
if (setting.get('setting') === 'category_style') {
let category = this.site.get('categories.firstObject');
if (category) {
return categoryLinkHTML(category, {
categoryStyle: value
});
}
}
let preview = setting.get('preview');
if (preview) {
return new Handlebars.SafeString("<div class='preview'>" + preview.replace(/\{\{value\}\}/g, value) + "</div>");
}
@ -52,16 +64,16 @@ export default Ember.Component.extend(BufferedContent, {
}.on("willDestroyElement"),
_save() {
const self = this,
setting = this.get('buffered');
SiteSetting.update(setting.get('setting'), setting.get('value')).then(function() {
self.set('validationMessage', null);
self.commitBuffer();
}).catch(function(e) {
const setting = this.get('buffered'),
action = SiteSetting.update(setting.get('setting'), setting.get('value'));
action.then(() => {
this.set('validationMessage', null);
this.commitBuffer();
}).catch((e) => {
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
self.set('validationMessage', e.jqXHR.responseJSON.errors[0]);
this.set('validationMessage', e.jqXHR.responseJSON.errors[0]);
} else {
self.set('validationMessage', I18n.t('generic_error'));
this.set('validationMessage', I18n.t('generic_error'));
}
});
},

View File

@ -0,0 +1,55 @@
import WatchedWord from 'admin/models/watched-word';
import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
classNames: ['watched-word-form'],
formSubmitted: false,
actionKey: null,
showSuccessMessage: false,
@computed('regularExpressions')
placeholderKey(regularExpressions) {
return "admin.watched_words.form.placeholder" +
(regularExpressions ? "_regexp" : "");
},
@observes('word')
removeSuccessMessage() {
if (this.get('showSuccessMessage') && !Ember.isEmpty(this.get('word'))) {
this.set('showSuccessMessage', false);
}
},
actions: {
submit() {
if (!this.get('formSubmitted')) {
this.set('formSubmitted', true);
const watchedWord = WatchedWord.create({ word: this.get('word'), action: this.get('actionKey') });
watchedWord.save().then(result => {
this.setProperties({ word: '', formSubmitted: false, showSuccessMessage: true });
this.sendAction('action', WatchedWord.create(result));
Ember.run.schedule('afterRender', () => this.$('.watched-word-input').focus());
}).catch(e => {
this.set('formSubmitted', false);
const msg = (e.responseJSON && e.responseJSON.errors) ?
I18n.t("generic_error_with_reason", {error: e.responseJSON.errors.join('. ')}) :
I18n.t("generic_error");
bootbox.alert(msg, () => this.$('.watched-word-input').focus());
});
}
}
},
@on("didInsertElement")
_init() {
Ember.run.schedule('afterRender', () => {
this.$('.watched-word-input').keydown(e => {
if (e.keyCode === 13) {
this.send('submit');
}
});
});
}
});

View File

@ -0,0 +1,25 @@
import computed from "ember-addons/ember-computed-decorators";
import UploadMixin from "discourse/mixins/upload";
export default Em.Component.extend(UploadMixin, {
type: 'csv',
classNames: 'watched-words-uploader',
uploadUrl: '/admin/logs/watched_words/upload',
addDisabled: Em.computed.alias("uploading"),
validateUploadedFilesOptions() {
return { csvOnly: true };
},
@computed('actionKey')
data(actionKey) {
return { action_key: actionKey };
},
uploadDone() {
if (this) {
bootbox.alert(I18n.t("admin.watched_words.form.upload_successful"));
this.sendAction("done");
}
}
});

View File

@ -15,6 +15,7 @@ export default Ember.Controller.extend(bufferedProperty('emailTemplate'), {
actions: {
saveChanges() {
this.set('saved', false);
const buffered = this.get('buffered');
this.get('emailTemplate').save(buffered.getProperties('subject', 'body')).then(() => {
this.set('saved', true);

View File

@ -7,7 +7,7 @@ import computed from 'ember-addons/ember-computed-decorators';
const PROBLEMS_CHECK_MINUTES = 1;
const ATTRIBUTES = [ 'disk_space','admins', 'moderators', 'blocked', 'suspended', 'top_traffic_sources',
const ATTRIBUTES = [ 'disk_space','admins', 'moderators', 'silenced', 'suspended', 'top_traffic_sources',
'top_referred_topics', 'updated_at'];
const REPORTS = [ 'global_reports', 'page_view_reports', 'private_message_reports', 'http_reports',

View File

@ -2,11 +2,13 @@ import EmailPreview from 'admin/models/email-preview';
import { popupAjaxError } from 'discourse/lib/ajax-error';
export default Ember.Controller.extend({
username: null,
lastSeen: null,
emailEmpty: Em.computed.empty('email'),
sendEmailDisabled: Em.computed.or('emailEmpty', 'sendingEmail'),
showSendEmailForm: Em.computed.notEmpty('model.html_content'),
htmlEmpty: Em.computed.empty('model.html_content'),
emailEmpty: Ember.computed.empty('email'),
sendEmailDisabled: Ember.computed.or('emailEmpty', 'sendingEmail'),
showSendEmailForm: Ember.computed.notEmpty('model.html_content'),
htmlEmpty: Ember.computed.empty('model.html_content'),
actions: {
refresh() {
@ -14,7 +16,14 @@ export default Ember.Controller.extend({
this.set('loading', true);
this.set('sentEmail', false);
EmailPreview.findDigest(this.get('lastSeen'), this.get('username')).then(email => {
let username = this.get('username');
if (!username) {
username = this.currentUser.get('username');
this.set('username', username);
}
EmailPreview.findDigest(username, this.get('lastSeen')).then(email => {
model.setProperties(email.getProperties('html_content', 'text_content'));
this.set('loading', false);
});
@ -28,16 +37,14 @@ export default Ember.Controller.extend({
this.set('sendingEmail', true);
this.set('sentEmail', false);
const self = this;
EmailPreview.sendDigest(this.get('lastSeen'), this.get('username'), this.get('email')).then(result => {
EmailPreview.sendDigest(this.get('username'), this.get('lastSeen'), this.get('email')).then(result => {
if (result.errors) {
bootbox.alert(result.errors);
} else {
self.set('sentEmail', true);
this.set('sentEmail', true);
}
}).catch(popupAjaxError).finally(function() {
self.set('sendingEmail', false);
}).catch(popupAjaxError).finally(() => {
this.set('sendingEmail', false);
});
}
}

View File

@ -1,41 +0,0 @@
import FlaggedPost from 'admin/models/flagged-post';
export default Ember.Controller.extend({
query: null,
adminOldFlagsView: Em.computed.equal("query", "old"),
adminActiveFlagsView: Em.computed.equal("query", "active"),
actions: {
disagreeFlags(flaggedPost) {
flaggedPost.disagreeFlags().then(() => {
this.get('model').removeObject(flaggedPost);
}, function () {
bootbox.alert(I18n.t("admin.flags.error"));
});
},
deferFlags(flaggedPost) {
flaggedPost.deferFlags().then(() => {
this.get('model').removeObject(flaggedPost);
}, function () {
bootbox.alert(I18n.t("admin.flags.error"));
});
},
doneTopicFlags(item) {
this.send("disagreeFlags", item);
},
loadMore() {
const flags = this.get('model');
return FlaggedPost.findAll(this.get('query'), flags.length+1).then(data => {
if (data.length===0) {
flags.set("allLoaded",true);
}
flags.addObjects(data);
});
}
}
});

View File

@ -15,6 +15,15 @@ export default Ember.Controller.extend({
];
}.property(),
visibilityLevelOptions: function() {
return [
{ name: I18n.t("groups.visibility_levels.public"), value: 0 },
{ name: I18n.t("groups.visibility_levels.members"), value: 1 },
{ name: I18n.t("groups.visibility_levels.staff"), value: 2 },
{ name: I18n.t("groups.visibility_levels.owners"), value: 3 }
];
}.property(),
trustLevelOptions: function() {
return [
{ name: I18n.t("groups.trust_levels.none"), value: 0 },
@ -22,14 +31,16 @@ export default Ember.Controller.extend({
];
}.property(),
@computed('model.visible', 'model.public')
disableMembershipRequestSetting(visible, publicGroup) {
return !visible || publicGroup;
@computed('model.visibility_level', 'model.public_admission')
disableMembershipRequestSetting(visibility_level, publicAdmission) {
visibility_level = parseInt(visibility_level);
return (visibility_level !== 0) || publicAdmission;
},
@computed('model.visible', 'model.allow_membership_requests')
disablePublicSetting(visible, allowMembershipRequests) {
return !visible || allowMembershipRequests;
@computed('model.visibility_level', 'model.allow_membership_requests')
disablePublicSetting(visibility_level, allowMembershipRequests) {
visibility_level = parseInt(visibility_level);
return (visibility_level !== 0) || allowMembershipRequests;
},
actions: {

View File

@ -30,7 +30,7 @@ export default Ember.Controller.extend({
showInstructions: Ember.computed.gt('model.length', 0),
refresh: function() {
_refresh() {
this.set('loading', true);
var filters = this.get('filters'),
@ -65,14 +65,18 @@ export default Ember.Controller.extend({
});
},
scheduleRefresh() {
Ember.run.scheduleOnce('afterRender', this, this._refresh);
},
resetFilters: function() {
this.set('filters', Ember.Object.create());
this.refresh();
this.scheduleRefresh();
}.on('init'),
_changeFilters: function(props) {
this.get('filters').setProperties(props);
this.refresh();
this.scheduleRefresh();
},
actions: {
@ -91,7 +95,7 @@ export default Ember.Controller.extend({
this._changeFilters(changed);
},
clearAllFilters: function() {
clearAllFilters() {
this.set("filterActionId", null);
this.resetFilters();
},

View File

@ -0,0 +1,11 @@
export default Ember.Controller.extend({
loading: false,
period: "all",
searchType: "all",
searchTypeOptions: [
{id: 'all', name: I18n.t('admin.logs.search_logs.types.all_search_types')},
{id: 'header', name: I18n.t('admin.logs.search_logs.types.header')},
{id: 'full_page', name: I18n.t('admin.logs.search_logs.types.full_page')}
]
});

View File

@ -0,0 +1,13 @@
export default Ember.Controller.extend({
loading: false,
term: null,
period: "yearly",
searchType: "all",
searchTypeOptions: [
{id: 'all', name: I18n.t('admin.logs.search_logs.types.all_search_types')},
{id: 'header', name: I18n.t('admin.logs.search_logs.types.header')},
{id: 'full_page', name: I18n.t('admin.logs.search_logs.types.full_page')},
{id: 'click_through_only', name: I18n.t('admin.logs.search_logs.types.click_through_only')}
]
});

View File

@ -6,6 +6,7 @@ import { popupAjaxError } from 'discourse/lib/ajax-error';
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Controller.extend(CanCheckEmails, {
adminTools: Ember.inject.service(),
editingUsername: false,
editingName: false,
editingTitle: false,
@ -57,13 +58,22 @@ export default Ember.Controller.extend(CanCheckEmails, {
saveTrustLevel() { return this.get("model").saveTrustLevel(); },
restoreTrustLevel() { return this.get("model").restoreTrustLevel(); },
lockTrustLevel(locked) { return this.get("model").lockTrustLevel(locked); },
unsuspend() { return this.get("model").unsuspend(); },
unblock() { return this.get("model").unblock(); },
block() { return this.get("model").block(); },
unsilence() { return this.get("model").unsilence(); },
silence() { return this.get("model").silence(); },
deleteAllPosts() { return this.get("model").deleteAllPosts(); },
anonymize() { return this.get('model').anonymize(); },
destroy() { return this.get('model').destroy(); },
showSuspendModal() {
this.get('adminTools').showSuspendModal(this.get('model'));
},
unsuspend() {
this.get("model").unsuspend().catch(popupAjaxError);
},
showSilenceModal() {
this.get('adminTools').showSilenceModal(this.get('model'));
},
toggleUsernameEdit() {
this.set('userUsernameValue', this.get('model.username'));
this.toggleProperty('editingUsername');

View File

@ -0,0 +1,65 @@
import computed from 'ember-addons/ember-computed-decorators';
import WatchedWord from 'admin/models/watched-word';
export default Ember.Controller.extend({
actionNameKey: null,
adminWatchedWords: Ember.inject.controller(),
showWordsList: Ember.computed.or('adminWatchedWords.filtered', 'adminWatchedWords.showWords'),
findAction(actionName) {
return (this.get('adminWatchedWords.model') || []).findBy('nameKey', actionName);
},
@computed('adminWatchedWords.model', 'actionNameKey')
filteredContent() {
if (!this.get('actionNameKey')) { return []; }
const a = this.findAction(this.get('actionNameKey'));
return a ? a.words : [];
},
@computed('actionNameKey')
actionDescription(actionNameKey) {
return I18n.t('admin.watched_words.action_descriptions.' + actionNameKey);
},
actions: {
recordAdded(arg) {
const a = this.findAction(this.get('actionNameKey'));
if (a) {
a.words.unshiftObject(arg);
a.incrementProperty('count');
Em.run.schedule('afterRender', () => {
// remove from other actions lists
let match = null;
this.get('adminWatchedWords.model').forEach(action => {
if (match) return;
if (action.nameKey !== this.get('actionNameKey')) {
match = action.words.findBy('id', arg.id);
if (match) {
action.words.removeObject(match);
action.decrementProperty('count');
}
}
});
});
}
},
recordRemoved(arg) {
const a = this.findAction(this.get('actionNameKey'));
if (a) {
a.words.removeObject(arg);
a.decrementProperty('count');
}
},
uploadComplete() {
WatchedWord.findAll().then(data => {
this.set('adminWatchedWords.model', data);
});
}
}
});

View File

@ -0,0 +1,52 @@
import debounce from 'discourse/lib/debounce';
export default Ember.Controller.extend({
filter: null,
filtered: false,
showWords: false,
disableShowWords: Ember.computed.alias('filtered'),
regularExpressions: null,
filterContentNow() {
if (!!Ember.isEmpty(this.get('allWatchedWords'))) return;
let filter;
if (this.get('filter')) {
filter = this.get('filter').toLowerCase();
}
if (filter === undefined || filter.length < 1) {
this.set('model', this.get('allWatchedWords'));
return;
}
const matchesByAction = [];
this.get('allWatchedWords').forEach(wordsForAction => {
const wordRecords = wordsForAction.words.filter(wordRecord => {
return (wordRecord.word.indexOf(filter) > -1);
});
matchesByAction.pushObject( Ember.Object.create({
nameKey: wordsForAction.nameKey,
name: wordsForAction.name,
words: wordRecords,
count: wordRecords.length
}) );
});
this.set('model', matchesByAction);
},
filterContent: debounce(function() {
this.filterContentNow();
this.set('filtered', !Ember.isEmpty(this.get('filter')));
}, 250).observes('filter'),
actions: {
clearFilter() {
this.setProperties({ filter: '' });
}
}
});

View File

@ -1,5 +1,20 @@
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Controller.extend({
showBadges: function() {
return this.get('currentUser.admin') && this.siteSettings.enable_badges;
}.property()
application: Ember.inject.controller(),
@computed
showBadges() {
return this.currentUser.get('admin') && this.siteSettings.enable_badges;
},
@computed('application.currentPath')
adminContentsClassName(currentPath) {
return currentPath.split('.').filter(segment => {
return segment !== 'index' &&
segment !== 'loading' &&
segment !== 'show' &&
segment !== 'admin';
}).map(Ember.String.dasherize).join(' ');
}
});

View File

@ -16,16 +16,17 @@ export default Ember.Controller.extend(ModalFunctionality, {
@computed('name')
nameValid(name) {
return name && name.match(/^[a-zA-Z0-9-_]+$/);
return name && name.match(/^[a-z_][a-z0-9_-]*$/i);
},
@observes('name')
uploadChanged(){
let file = $('#file-input')[0];
uploadChanged() {
const file = $('#file-input')[0];
this.set('fileSelected', file && file.files[0]);
},
actions: {
updateName() {
let name = this.get('name');
if (Em.isEmpty(name)) {
@ -34,20 +35,21 @@ export default Ember.Controller.extend(ModalFunctionality, {
}
this.uploadChanged();
},
upload() {
let options = {
type: 'POST'
upload() {
const file = $('#file-input')[0].files[0];
const options = {
type: 'POST',
processData: false,
contentType: false,
data: new FormData()
};
options.processData = false;
options.contentType = false;
options.data = new FormData();
let file = $('#file-input')[0].files[0];
options.data.append('file', file);
ajax('/admin/themes/upload_asset', options).then(result=>{
let upload = {
ajax('/admin/themes/upload_asset', options).then(result => {
const upload = {
upload_id: result.upload_id,
name: this.get('name'),
original_filename: file.name
@ -57,7 +59,6 @@ export default Ember.Controller.extend(ModalFunctionality, {
}).catch(e => {
popupAjaxError(e);
});
}
}
});

View File

@ -1,24 +0,0 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality';
export default Ember.Controller.extend(ModalFunctionality, {
adminFlagsList: Ember.inject.controller(),
_agreeFlag: function (actionOnPost) {
const adminFlagController = this.get("adminFlagsList");
const post = this.get("content");
return post.agreeFlags(actionOnPost).then(() => {
adminFlagController.get('model').removeObject(post);
this.send("closeModal");
}, function () {
bootbox.alert(I18n.t("admin.flags.error"));
});
},
actions: {
agreeFlagHidePost: function () { return this._agreeFlag("hide"); },
agreeFlagKeepPost: function () { return this._agreeFlag("keep"); },
agreeFlagRestorePost: function () { return this._agreeFlag("restore"); }
}
});

View File

@ -1,31 +0,0 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality';
export default Ember.Controller.extend(ModalFunctionality, {
adminFlagsList: Ember.inject.controller(),
actions: {
deletePostDeferFlag() {
const adminFlagController = this.get("adminFlagsList");
const post = this.get("content");
return post.deferFlags(true).then(() => {
adminFlagController.get('model').removeObject(post);
this.send("closeModal");
}, function () {
bootbox.alert(I18n.t("admin.flags.error"));
});
},
deletePostAgreeFlag() {
const adminFlagController = this.get("adminFlagsList");
const post = this.get("content");
return post.agreeFlags("delete").then(() => {
adminFlagController.get('model').removeObject(post);
this.send("closeModal");
}, function () {
bootbox.alert(I18n.t("admin.flags.error"));
});
}
}
});

View File

@ -0,0 +1,18 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality';
export default Ember.Controller.extend(ModalFunctionality, {
loading: null,
historyTarget: null,
history: null,
onShow() {
this.set('loading', true);
this.set('history', null);
},
loadHistory(target) {
this.store.findAll('moderation-history', target).then(result => {
this.set('history', result);
}).finally(() => this.set('loading', false));
}
});

View File

@ -0,0 +1,50 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality';
import computed from 'ember-addons/ember-computed-decorators';
import { popupAjaxError } from 'discourse/lib/ajax-error';
export default Ember.Controller.extend(ModalFunctionality, {
silenceUntil: null,
reason: null,
message: null,
silencing: false,
user: null,
post: null,
successCallback: null,
onShow() {
this.setProperties({
silenceUntil: null,
reason: null,
message: null,
silencing: false,
loadingUser: true,
post: null,
successCallback: null,
});
},
@computed('silenceUntil', 'reason', 'silencing')
submitDisabled(silenceUntil, reason, silencing) {
return (silencing || Ember.isEmpty(silenceUntil) || !reason || reason.length < 1);
},
actions: {
silence() {
if (this.get('submitDisabled')) { return; }
this.set('silencing', true);
this.get('user').silence({
silenced_till: this.get('silenceUntil'),
reason: this.get('reason'),
message: this.get('message'),
post_id: this.get('post.id')
}).then(result => {
this.send('closeModal');
let callback = this.get('successCallback');
if (callback) {
callback(result);
}
}).catch(popupAjaxError).finally(() => this.set('silencing', false));
}
}
});

View File

@ -1,25 +1,50 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality';
import computed from 'ember-addons/ember-computed-decorators';
import { popupAjaxError } from 'discourse/lib/ajax-error';
export default Ember.Controller.extend(ModalFunctionality, {
suspendUntil: null,
reason: null,
message: null,
suspending: false,
user: null,
post: null,
successCallback: null,
submitDisabled: function() {
return (!this.get('reason') || this.get('reason').length < 1);
}.property('reason'),
onShow() {
this.setProperties({
suspendUntil: null,
reason: null,
message: null,
suspending: false,
loadingUser: true,
post: null,
successCallback: null,
});
},
@computed('suspendUntil', 'reason', 'suspending')
submitDisabled(suspendUntil, reason, suspending) {
return (suspending || Ember.isEmpty(suspendUntil) || !reason || reason.length < 1);
},
actions: {
suspend: function() {
if (this.get('submitDisabled')) return;
var duration = parseInt(this.get('duration'), 10);
if (duration > 0) {
var self = this;
this.send('hideModal');
this.get('model').suspend(duration, this.get('reason')).then(function() {
window.location.reload();
}, function(e) {
var error = I18n.t('admin.user.suspend_failed', { error: "http: " + e.status + " - " + e.body });
bootbox.alert(error, function() { self.send('reopenModal'); });
});
}
suspend() {
if (this.get('submitDisabled')) { return; }
this.set('suspending', true);
this.get('user').suspend({
suspend_until: this.get('suspendUntil'),
reason: this.get('reason'),
message: this.get('message'),
post_id: this.get('post.id')
}).then(result => {
this.send('closeModal');
let callback = this.get('successCallback');
if (callback) {
callback(result);
}
}).catch(popupAjaxError).finally(() => this.set('suspending', false));
}
}

View File

@ -0,0 +1,7 @@
import { registerUnbound } from 'discourse-common/lib/helpers';
import { renderIcon } from 'discourse-common/lib/icon-library';
registerUnbound('check-icon', function(value) {
let icon = value ? "check" : "times";
return new Handlebars.SafeString(renderIcon('string', icon));
});

View File

@ -0,0 +1,15 @@
import { iconHTML } from 'discourse-common/lib/icon-library';
export default Ember.Helper.extend({
compute([disposition]) {
if (!disposition) { return null; }
let icon;
let title = 'admin.flags.dispositions.' + disposition;
switch (disposition) {
case "deferred": { icon = "external-link"; break; }
case "agreed": { icon = "thumbs-o-up"; break; }
case "disagreed": { icon = "thumbs-o-down"; break; }
}
return iconHTML(icon, { title }).htmlSafe();
}
});

View File

@ -0,0 +1,12 @@
function postActionTitle([id, nameKey]) {
let title = I18n.t(`admin.flags.short_names.${nameKey}`, { defaultValue: null });
// TODO: We can remove this once other translations have been updated
if (!title) {
return I18n.t(`admin.flags.summary.action_type_${id}`, { count: 1 });
}
return title;
}
export default Ember.Helper.helper(postActionTitle);

View File

@ -1,3 +1,4 @@
import { iconHTML } from 'discourse-common/lib/icon-library';
import { ajax } from 'discourse/lib/ajax';
import computed from 'ember-addons/ember-computed-decorators';
import { propertyNotEqual } from 'discourse/lib/computed';
@ -7,8 +8,10 @@ import Group from 'discourse/models/group';
import TL3Requirements from 'admin/models/tl3-requirements';
import { userPath } from 'discourse/lib/url';
const AdminUser = Discourse.User.extend({
const wrapAdmin = user => user ? AdminUser.create(user) : null;
const AdminUser = Discourse.User.extend({
adminUserView: true,
customGroups: Ember.computed.filter("groups", g => !g.automatic && Group.create(g)),
automaticGroups: Ember.computed.filter("groups", g => g.automatic && Group.create(g)),
@ -105,10 +108,10 @@ const AdminUser = Discourse.User.extend({
message = I18n.messageFormat('admin.user.delete_all_posts_confirm_MF', { "POSTS": user.get('post_count'), "TOPICS": user.get('topic_count') }),
buttons = [{
"label": I18n.t("composer.cancel"),
"class": "cancel-inline",
"class": "d-modal-cancel",
"link": true
}, {
"label": '<i class="fa fa-exclamation-triangle"></i> ' + I18n.t("admin.user.delete_all_posts"),
"label": `${iconHTML('exclamation-triangle')} ` + I18n.t("admin.user.delete_all_posts"),
"class": "btn btn-danger",
"callback": function() {
ajax("/admin/users/" + user.get('id') + "/delete_all_posts", {
@ -231,6 +234,7 @@ const AdminUser = Discourse.User.extend({
}.property('trust_level'),
isSuspended: Em.computed.equal('suspended', true),
isSilenced: Ember.computed.equal('silenced', true),
canSuspend: Em.computed.not('staff'),
suspendDuration: function() {
@ -239,22 +243,17 @@ const AdminUser = Discourse.User.extend({
return suspended_at.format('L') + " - " + suspended_till.format('L');
}.property('suspended_till', 'suspended_at'),
suspend(duration, reason) {
return ajax("/admin/users/" + this.id + "/suspend", {
suspend(data) {
return ajax(`/admin/users/${this.id}/suspend`, {
type: 'PUT',
data: { duration: duration, reason: reason }
});
data
}).then(result => this.setProperties(result.suspension));
},
unsuspend() {
return ajax("/admin/users/" + this.id + "/unsuspend", {
return ajax(`/admin/users/${this.id}/unsuspend`, {
type: 'PUT'
}).then(function() {
window.location.reload();
}).catch(function(e) {
var error = I18n.t('admin.user.unsuspend_failed', { error: "http: " + e.status + " - " + e.body });
bootbox.alert(error);
});
}).then(result => this.setProperties(result.suspension));
},
logOut() {
@ -303,46 +302,38 @@ const AdminUser = Discourse.User.extend({
});
},
unblock() {
this.set('blockingUser', true);
return ajax('/admin/users/' + this.id + '/unblock', {
unsilence() {
this.set('silencingUser', true);
return ajax(`/admin/users/${this.id}/unsilence`, {
type: 'PUT'
}).then(function() {
window.location.reload();
}).catch(function(e) {
var error = I18n.t('admin.user.unblock_failed', { error: "http: " + e.status + " - " + e.body });
}).then(result => {
this.setProperties(result.unsilence);
}).catch(e => {
let error = I18n.t('admin.user.unsilence_failed', {
error: `http: ${e.status} - ${e.body}`
});
bootbox.alert(error);
}).finally(() => {
this.set('silencingUser', false);
});
},
block() {
const user = this,
message = I18n.t("admin.user.block_confirm");
const performBlock = function() {
user.set('blockingUser', true);
return ajax('/admin/users/' + user.id + '/block', {
type: 'PUT'
}).then(function() {
window.location.reload();
}).catch(function(e) {
var error = I18n.t('admin.user.block_failed', { error: "http: " + e.status + " - " + e.body });
bootbox.alert(error);
user.set('blockingUser', false);
silence(data) {
this.set('silencingUser', true);
return ajax(`/admin/users/${this.id}/silence`, {
type: 'PUT',
data
}).then(result => {
this.setProperties(result.silence);
}).catch(e => {
let error = I18n.t('admin.user.silence_failed', {
error: `http: ${e.status} - ${e.body}`
});
};
const buttons = [{
"label": I18n.t("composer.cancel"),
"class": "cancel",
"link": true
}, {
"label": '<i class="fa fa-exclamation-triangle"></i>' + I18n.t('admin.user.block_accept'),
"class": "btn btn-danger",
"callback": function() { performBlock(); }
}];
bootbox.dialog(message, buttons, { "classes": "delete-user-modal" });
bootbox.alert(error);
}).finally(() => {
this.set('silencingUser', false);
});
},
sendActivationEmail() {
@ -386,7 +377,7 @@ const AdminUser = Discourse.User.extend({
"class": "cancel",
"link": true
}, {
"label": '<i class="fa fa-exclamation-triangle"></i>' + I18n.t('admin.user.anonymize_yes'),
"label": `${iconHTML('exclamation-triangle')} ` + I18n.t('admin.user.anonymize_yes'),
"class": "btn btn-danger",
"callback": function() { performAnonymize(); }
}];
@ -450,7 +441,7 @@ const AdminUser = Discourse.User.extend({
"class": "btn",
"link": true
}, {
"label": '<i class="fa fa-exclamation-triangle"></i>' + I18n.t('admin.user.delete_and_block'),
"label": `${iconHTML('exclamation-triangle')} ` + I18n.t('admin.user.delete_and_block'),
"class": "btn btn-danger",
"callback": function(){ performDestroy(true); }
}, {
@ -462,52 +453,6 @@ const AdminUser = Discourse.User.extend({
bootbox.dialog(message, buttons, { "classes": "delete-user-modal" });
},
deleteAsSpammer(successCallback) {
const user = this;
user.checkEmail().then(function() {
const data = {
"POSTS": user.get('post_count'),
"TOPICS": user.get('topic_count'),
email: user.get('email') || I18n.t("flagging.hidden_email_address"),
ip_address: user.get('ip_address') || I18n.t("flagging.ip_address_missing")
};
const message = I18n.messageFormat('flagging.delete_confirm_MF', data),
buttons = [{
"label": I18n.t("composer.cancel"),
"class": "cancel-inline",
"link": true
}, {
"label": '<i class="fa fa-exclamation-triangle"></i> ' + I18n.t("flagging.yes_delete_spammer"),
"class": "btn btn-danger",
"callback": function() {
return ajax("/admin/users/" + user.get('id') + '.json', {
type: 'DELETE',
data: {
delete_posts: true,
block_email: true,
block_urls: true,
block_ip: true,
delete_as_spammer: true,
context: window.location.pathname
}
}).then(function(result) {
if (result.deleted) {
if (successCallback) successCallback();
} else {
bootbox.alert(I18n.t("admin.user.delete_failed"));
}
}).catch(function() {
bootbox.alert(I18n.t("admin.user.delete_failed"));
});
}
}];
bootbox.dialog(message, buttons, {"classes": "flagging-delete-spammer"});
});
},
loadDetails() {
const user = this;
@ -525,17 +470,14 @@ const AdminUser = Discourse.User.extend({
}
}.property('tl3_requirements'),
suspendedBy: function() {
if (this.get('suspended_by')) {
return AdminUser.create(this.get('suspended_by'));
}
}.property('suspended_by'),
@computed('suspended_by')
suspendedBy: wrapAdmin,
approvedBy: function() {
if (this.get('approved_by')) {
return AdminUser.create(this.get('approved_by'));
}
}.property('approved_by')
@computed('silenced_by')
silencedBy: wrapAdmin,
@computed('approved_by')
approvedBy: wrapAdmin,
});

View File

@ -1,4 +1,6 @@
import AdminUser from 'admin/models/admin-user';
import { ajax } from 'discourse/lib/ajax';
const ApiKey = Discourse.Model.extend({
/**
@ -36,8 +38,7 @@ ApiKey.reopenClass({
@param {...} var_args the properties to initialize this with
@returns {ApiKey} the ApiKey instance
**/
create: function() {
const AdminUser = require('admin/models/admin-user').default;
create() {
var result = this._super.apply(this, arguments);
if (result.user) {
result.user = AdminUser.create(result.user);

View File

@ -1,42 +1,24 @@
import { ajax } from 'discourse/lib/ajax';
const EmailPreview = Discourse.Model.extend({});
export function oneWeekAgo() {
return moment().locale('en').subtract(7, 'days').format('YYYY-MM-DD');
}
EmailPreview.reopenClass({
findDigest: function(lastSeenAt, username) {
if (Em.isEmpty(lastSeenAt)) {
lastSeenAt = this.oneWeekAgo();
}
if (Em.isEmpty(username)) {
username = Discourse.User.current().username;
}
findDigest(username, lastSeenAt) {
return ajax("/admin/email/preview-digest.json", {
data: { last_seen_at: lastSeenAt, username: username }
}).then(function (result) {
return EmailPreview.create(result);
});
data: { last_seen_at: lastSeenAt || oneWeekAgo(), username }
}).then(result => EmailPreview.create(result));
},
sendDigest: function(lastSeenAt, username, email) {
if (Em.isEmpty(lastSeenAt)) {
lastSeenAt = this.oneWeekAgo();
}
if (Em.isEmpty(username)) {
username = Discourse.User.current().username;
}
sendDigest(username, lastSeenAt, email) {
return ajax("/admin/email/send-digest.json", {
data: { last_seen_at: lastSeenAt, username: username, email: email }
data: { last_seen_at: lastSeenAt || oneWeekAgo(), username, email }
});
},
oneWeekAgo() {
const en = moment().locale('en');
return en.subtract(7, 'days').format('YYYY-MM-DD');
}
});
export default EmailPreview;

View File

@ -0,0 +1,10 @@
import RestModel from 'discourse/models/rest';
import computed from 'ember-addons/ember-computed-decorators';
export default RestModel.extend({
@computed('id')
name(id) {
return I18n.t(`admin.flags.summary.action_type_${id}`, { count: 1});
}
});

View File

@ -1,111 +1,50 @@
import { ajax } from 'discourse/lib/ajax';
import AdminUser from 'admin/models/admin-user';
import Topic from 'discourse/models/topic';
import Post from 'discourse/models/post';
import computed from 'ember-addons/ember-computed-decorators';
import { popupAjaxError } from 'discourse/lib/ajax-error';
export default Post.extend({
const FlaggedPost = Post.extend({
summary: function () {
@computed
summary() {
return _(this.post_actions)
.groupBy(function (a) { return a.post_action_type_id; })
.map(function (v,k) { return I18n.t('admin.flags.summary.action_type_' + k, { count: v.length }); })
.join(',');
}.property(),
flaggers: function () {
var self = this;
var flaggers = [];
_.each(this.post_actions, function (postAction) {
flaggers.push({
user: self.userLookup[postAction.user_id],
topic: self.topicLookup[postAction.topic_id],
flagType: I18n.t('admin.flags.summary.action_type_' + postAction.post_action_type_id, { count: 1 }),
flaggedAt: postAction.created_at,
disposedBy: postAction.disposed_by_id ? self.userLookup[postAction.disposed_by_id] : null,
disposedAt: postAction.disposed_at,
dispositionIcon: self.dispositionIcon(postAction.disposition),
tookAction: postAction.staff_took_action
});
});
return flaggers;
}.property(),
dispositionIcon: function (disposition) {
if (!disposition) { return null; }
var icon, title = I18n.t('admin.flags.dispositions.' + disposition);
switch (disposition) {
case "deferred": { icon = "fa-external-link"; break; }
case "agreed": { icon = "fa-thumbs-o-up"; break; }
case "disagreed": { icon = "fa-thumbs-o-down"; break; }
}
return "<i class='fa " + icon + "' title='" + title + "'></i>";
},
wasEdited: function () {
@computed('last_revised_at', 'post_actions.@each.created_at')
wasEdited(lastRevisedAt) {
if (Ember.isEmpty(this.get("last_revised_at"))) { return false; }
var lastRevisedAt = Date.parse(this.get("last_revised_at"));
lastRevisedAt = Date.parse(lastRevisedAt);
return _.some(this.get("post_actions"), function (postAction) {
return Date.parse(postAction.created_at) < lastRevisedAt;
});
}.property("last_revised_at", "post_actions.@each.created_at"),
},
conversations: function () {
var self = this;
var conversations = [];
@computed('post_actions.@each.name_key')
flaggedForSpam() {
return this.get('post_actions').every(action => action.name_key === 'spam');
},
_.each(this.post_actions, function (postAction) {
if (postAction.conversation) {
var conversation = {
permalink: postAction.permalink,
hasMore: postAction.conversation.has_more,
response: {
excerpt: postAction.conversation.response.excerpt,
user: self.userLookup[postAction.conversation.response.user_id]
}
};
if (postAction.conversation.reply) {
conversation["reply"] = {
excerpt: postAction.conversation.reply.excerpt,
user: self.userLookup[postAction.conversation.reply.user_id]
};
}
conversations.push(conversation);
}
});
return conversations;
}.property(),
user: function() {
return this.userLookup[this.user_id];
}.property(),
topic: function () {
return this.topicLookup[this.topic_id];
}.property(),
flaggedForSpam: function() {
return !_.every(this.get('post_actions'), function(action) { return action.name_key !== 'spam'; });
}.property('post_actions.@each.name_key'),
topicFlagged: function() {
@computed('post_actions.@each.targets_topic')
topicFlagged() {
return _.any(this.get('post_actions'), function(action) { return action.targets_topic; });
}.property('post_actions.@each.targets_topic'),
},
postAuthorFlagged: function() {
@computed('post_actions.@each.targets_topic')
postAuthorFlagged() {
return _.any(this.get('post_actions'), function(action) { return !action.targets_topic; });
}.property('post_actions.@each.targets_topic'),
},
canDeleteAsSpammer: function() {
return Discourse.User.currentProp('staff') && this.get('flaggedForSpam') && this.get('user.can_delete_all_posts') && this.get('user.can_be_deleted');
}.property('flaggedForSpam'),
@computed('flaggedForSpam')
canDeleteAsSpammer(flaggedForSpam) {
return flaggedForSpam &&
this.get('user.can_delete_all_posts') &&
this.get('user.can_be_deleted');
},
deletePost: function() {
deletePost() {
if (this.get('post_number') === 1) {
return ajax('/t/' + this.topic_id, { type: 'DELETE', cache: false });
} else {
@ -113,64 +52,20 @@ const FlaggedPost = Post.extend({
}
},
disagreeFlags: function () {
return ajax('/admin/flags/disagree/' + this.id, { type: 'POST', cache: false });
disagreeFlags() {
return ajax('/admin/flags/disagree/' + this.id, { type: 'POST', cache: false }).catch(popupAjaxError);
},
deferFlags: function (deletePost) {
return ajax('/admin/flags/defer/' + this.id, { type: 'POST', cache: false, data: { delete_post: deletePost } });
deferFlags(deletePost) {
return ajax('/admin/flags/defer/' + this.id, { type: 'POST', cache: false, data: { delete_post: deletePost } }).catch(popupAjaxError);
},
agreeFlags: function (actionOnPost) {
return ajax('/admin/flags/agree/' + this.id, { type: 'POST', cache: false, data: { action_on_post: actionOnPost } });
agreeFlags(actionOnPost) {
return ajax('/admin/flags/agree/' + this.id, { type: 'POST', cache: false, data: { action_on_post: actionOnPost } }).catch(popupAjaxError);
},
postHidden: Em.computed.alias('hidden'),
postHidden: Ember.computed.alias('hidden'),
extraClasses: function() {
var classes = [];
if (this.get('hidden')) { classes.push('hidden-post'); }
if (this.get('deleted')) { classes.push('deleted'); }
return classes.join(' ');
}.property(),
deleted: Em.computed.or('deleted_at', 'topic_deleted_at')
deleted: Ember.computed.or('deleted_at', 'topic_deleted_at'),
});
FlaggedPost.reopenClass({
findAll: function (filter, offset) {
offset = offset || 0;
var result = Em.A();
result.set('loading', true);
return ajax('/admin/flags/' + filter + '.json?offset=' + offset).then(function (data) {
// users
var userLookup = {};
_.each(data.users, function (user) {
userLookup[user.id] = AdminUser.create(user);
});
// topics
var topicLookup = {};
_.each(data.topics, function (topic) {
topicLookup[topic.id] = Topic.create(topic);
});
// posts
_.each(data.posts, function (post) {
var f = FlaggedPost.create(post);
f.userLookup = userLookup;
f.topicLookup = topicLookup;
result.pushObject(f);
});
result.set('loading', false);
return result;
});
}
});
export default FlaggedPost;

View File

@ -48,7 +48,7 @@ SiteSetting.reopenClass({
update(key, value) {
const data = {};
data[key] = value;
return ajax("/admin/site_settings/" + key, { type: 'PUT', data });
return ajax(`/admin/site_settings/${key}`, { type: 'PUT', data });
}
});

View File

@ -8,18 +8,6 @@ const VersionCheck = Discourse.Model.extend({
return updatedAt === null;
},
@computed('updated_at', 'version_check_pending')
dataIsOld(updatedAt, versionCheckPending) {
return versionCheckPending || moment().diff(moment(updatedAt), 'hours') >= 48;
},
@computed('dataIsOld', 'installed_version', 'latest_version', 'missing_versions_count')
staleData(dataIsOld, installedVersion, latestVersion, missingVersionsCount) {
return dataIsOld ||
(installedVersion !== latestVersion && missingVersionsCount === 0) ||
(installedVersion === latestVersion && missingVersionsCount !== 0);
},
@computed('missing_versions_count')
upToDate(missingVersionsCount) {
return missingVersionsCount === 0 || missingVersionsCount === null;

View File

@ -0,0 +1,43 @@
import { ajax } from 'discourse/lib/ajax';
const WatchedWord = Discourse.Model.extend({
save() {
return ajax("/admin/logs/watched_words" + (this.id ? '/' + this.id : '') + ".json", {
type: this.id ? 'PUT' : 'POST',
data: {word: this.get('word'), action_key: this.get('action')},
dataType: 'json'
});
},
destroy() {
return ajax("/admin/logs/watched_words/" + this.get('id') + ".json", {type: 'DELETE'});
}
});
WatchedWord.reopenClass({
findAll() {
return ajax("/admin/logs/watched_words").then(list => {
const actions = {};
list.words.forEach(s => {
if (!actions[s.action]) { actions[s.action] = []; }
actions[s.action].pushObject(WatchedWord.create(s));
});
list.actions.forEach(a => {
if (!actions[a]) { actions[a] = []; }
});
return Object.keys(actions).map(n => {
return Ember.Object.create({
nameKey: n,
name: I18n.t('admin.watched_words.actions.' + n),
words: actions[n],
count: actions[n].length,
regularExpressions: list.regular_expressions
});
});
});
}
});
export default WatchedWord;

View File

@ -37,7 +37,7 @@ export default RestModel.extend({
},
groupFinder(term) {
return Group.findAll({search: term, ignore_automatic: false});
return Group.findAll({ term: term, ignore_automatic: false });
},
@computed('wildcard_web_hook', 'web_hook_event_types.[]')
@ -82,4 +82,3 @@ export default RestModel.extend({
return this.createProperties();
}
});

View File

@ -68,7 +68,7 @@ export default Discourse.Route.extend({
function(confirmed) {
if (confirmed) {
backup.destroy().then(function() {
self.controllerFor("adminBackupsIndex").removeObject(backup);
self.controllerFor("adminBackupsIndex").get('model').removeObject(backup);
});
}
}

View File

@ -1,16 +1,17 @@
import EmailPreview from 'admin/models/email-preview';
import { default as EmailPreview, oneWeekAgo } from 'admin/models/email-preview';
export default Discourse.Route.extend({
model() {
return EmailPreview.findDigest();
return EmailPreview.findDigest(this.currentUser.get('username'));
},
afterModel(model) {
const controller = this.controllerFor('adminEmailPreviewDigest');
controller.setProperties({
model: model,
lastSeen: moment().subtract(7, 'days').format('YYYY-MM-DD'),
model,
username: this.currentUser.get('username'),
lastSeen: oneWeekAgo(),
showHtml: true
});
}

View File

@ -1,5 +1,6 @@
export default Discourse.Route.extend({
redirect() {
this.replaceWith('adminFlags.list', 'active');
let segment = this.siteSettings.flags_default_topics ? 'topics' : 'postsActive';
this.replaceWith(`adminFlags.${segment}`);
}
});

View File

@ -1,27 +0,0 @@
import showModal from 'discourse/lib/show-modal';
import FlaggedPost from 'admin/models/flagged-post';
export default Discourse.Route.extend({
model(params) {
this.filter = params.filter;
return FlaggedPost.findAll(params.filter);
},
setupController(controller, model) {
controller.set('model', model);
controller.set('query', this.filter);
},
actions: {
showAgreeFlagModal(model) {
showModal('admin-agree-flag', { model, admin: true });
this.controllerFor('modal').set('modalClass', 'agree-flag-modal');
},
showDeleteFlagModal(model) {
showModal('admin-delete-flag', { model, admin: true });
this.controllerFor('modal').set('modalClass', 'delete-flag-modal');
}
}
});

View File

@ -0,0 +1,5 @@
export default Discourse.Route.extend({
model() {
return this.store.findAll('flagged-post', { filter: 'active' });
}
});

View File

@ -0,0 +1,5 @@
export default Discourse.Route.extend({
model() {
return this.store.findAll('flagged-post', { filter: 'old' });
},
});

View File

@ -0,0 +1,9 @@
export default Discourse.Route.extend({
model() {
return this.store.findAll('flagged-topic');
},
setupController(controller, model) {
controller.set('flaggedTopics', model);
}
});

View File

@ -0,0 +1,20 @@
import { loadTopicView } from 'discourse/models/topic';
export default Ember.Route.extend({
model(params) {
let topicRecord = this.store.createRecord('topic', { id: params.id });
let topic = loadTopicView(topicRecord).then(() => topicRecord);
return Ember.RSVP.hash({
topic,
flaggedPosts: this.store.findAll('flagged-post', {
filter: 'active',
topic_id: params.id
})
});
},
setupController(controller, hash) {
controller.setProperties(hash);
}
});

View File

@ -4,7 +4,7 @@ export default Discourse.Route.extend({
model(params) {
if (params.name === 'new') {
return Group.create({ automatic: false, visible: true });
return Group.create({ automatic: false, visibility_level: 0 });
}
const group = this.modelFor('adminGroupsType').findBy('name', params.name);

View File

@ -1,16 +1,9 @@
/**
Handles routes for admin reports
import Report from 'admin/models/report';
@class AdminReportsRoute
@extends Discourse.Route
@namespace Discourse
@module Discourse
**/
export default Discourse.Route.extend({
queryParams: { mode: {}, "start_date": {}, "end_date": {}, "category_id": {}, "group_id": {} },
model: function(params) {
const Report = require('admin/models/report').default;
model(params) {
return Report.find(params.type, params['start_date'], params['end_date'], params['category_id'], params['group_id']);
},

View File

@ -54,7 +54,11 @@ export default function() {
this.route('adminReports', { path: '/reports/:type', resetNamespace: true });
this.route('adminFlags', { path: '/flags', resetNamespace: true }, function() {
this.route('list', { path: '/:filter' });
this.route('postsActive', { path: 'active' });
this.route('postsOld', { path: 'old' });
this.route('topics', { path: 'topics' }, function() {
this.route('show', { path: ":id" });
});
});
this.route('adminLogs', { path: '/logs', resetNamespace: true }, function() {
@ -62,6 +66,14 @@ export default function() {
this.route('screenedEmails', { path: '/screened_emails' });
this.route('screenedIpAddresses', { path: '/screened_ip_addresses' });
this.route('screenedUrls', { path: '/screened_urls' });
this.route('adminSearchLogs', { path: '/search_logs', resetNamespace: true}, function() {
this.route('index', { path: '/' });
this.route('term', { path: '/term/:term' });
});
this.route('adminWatchedWords', { path: '/watched_words', resetNamespace: true}, function() {
this.route('index', { path: '/' });
this.route('action', { path: '/action/:action_id' });
});
});
this.route('adminGroups', { path: '/groups', resetNamespace: true }, function() {

View File

@ -0,0 +1,20 @@
import { ajax } from 'discourse/lib/ajax';
export default Discourse.Route.extend({
queryParams: {
period: { refreshModel: true },
searchType: { refreshModel: true }
},
model(params) {
this._params = params;
return ajax('/admin/logs/search_logs.json', { data: { period: params.period, search_type: params.searchType } }).then(search_logs => {
return search_logs.map(sl => Ember.Object.create(sl));
});
},
setupController(controller, model) {
const params = this._params;
controller.setProperties({ model, period: params.period, searchType: params.searchType });
}
});

View File

@ -0,0 +1,33 @@
import { ajax } from 'discourse/lib/ajax';
export default Discourse.Route.extend({
queryParams: {
period: { refreshModel: true },
searchType: { refreshModel: true }
},
model(params) {
this._params = params;
return ajax(`/admin/logs/search_logs/term/${params.term}.json`, {
data: {
period: params.period,
search_type: params.searchType
}
}).then(json => {
const model = Ember.Object.create({ type: "search_log_term" });
model.setProperties(json.term);
return model;
});
},
setupController(controller, model) {
const params = this._params;
controller.setProperties({
model,
term: params.term,
period: params.period,
searchType: params.searchType
});
}
});

View File

@ -1,4 +1,3 @@
import showModal from 'discourse/lib/show-modal';
import Group from 'discourse/models/group';
export default Discourse.Route.extend({
@ -25,11 +24,6 @@ export default Discourse.Route.extend({
},
actions: {
showSuspendModal(model) {
showModal('admin-suspend-user', { model, admin: true });
this.controllerFor('modal').set('modalClass', 'suspend-user-modal');
},
viewActionLogs(username) {
const controller = this.controllerFor('adminLogs.staffActionLogs');
this.transitionTo('adminLogs.staffActionLogs').then(() => {

View File

@ -0,0 +1,5 @@
export default Discourse.Route.extend({
redirect: function() {
this.transitionTo('adminUsersList');
}
});

View File

@ -0,0 +1,11 @@
export default Discourse.Route.extend({
model(params) {
this.controllerFor('adminWatchedWordsAction').set('actionNameKey', params.action_id);
let filteredContent = this.controllerFor('adminWatchedWordsAction').get('filteredContent');
return Ember.Object.create({
nameKey: params.action_id,
name: I18n.t('admin.watched_words.actions.' + params.action_id),
words: filteredContent
});
}
});

View File

@ -0,0 +1,5 @@
export default Discourse.Route.extend({
beforeModel() {
this.replaceWith('adminWatchedWords.action', this.modelFor('adminWatchedWords')[0].nameKey);
}
});

View File

@ -0,0 +1,22 @@
import WatchedWord from 'admin/models/watched-word';
export default Discourse.Route.extend({
queryParams: {
filter: { replace: true }
},
model() {
return WatchedWord.findAll();
},
setupController(controller, model) {
controller.set('model', model);
if (model && model.length) {
controller.set('regularExpressions', model[0].get('regularExpressions'));
}
},
afterModel(watchedWordsList) {
this.controllerFor('adminWatchedWords').set('allWatchedWords', watchedWordsList);
}
});

View File

@ -0,0 +1,127 @@
// A service that can act as a bridge between the front end Discourse application
// and the admin application. Use this if you need front end code to access admin
// modules. Inject it optionally, and if it exists go to town!
import AdminUser from 'admin/models/admin-user';
import { iconHTML } from 'discourse-common/lib/icon-library';
import { ajax } from 'discourse/lib/ajax';
import showModal from 'discourse/lib/show-modal';
import { getOwner } from 'discourse-common/lib/get-owner';
export default Ember.Service.extend({
init() {
this._super();
// TODO: Make `siteSettings` a service that can be injected
this.siteSettings = getOwner(this).lookup('site-settings:main');
},
checkSpammer(userId) {
return AdminUser.find(userId).then(au => this.spammerDetails(au));
},
spammerDetails(adminUser) {
return {
deleteUser: () => this._deleteSpammer(adminUser),
canDelete: adminUser.get('can_be_deleted') && adminUser.get('can_delete_all_posts')
};
},
_showControlModal(type, user, opts) {
opts = opts || {};
let controller = showModal(`admin-${type}-user`, {
admin: true,
modalClass: `${type}-user-modal`
});
if (opts.post) {
controller.set('post', opts.post);
}
let promise = user.adminUserView ?
Ember.RSVP.resolve(user) :
AdminUser.find(user.get('id'));
promise.then(loadedUser => {
controller.setProperties({
user: loadedUser,
loadingUser: false,
successCallback: opts.successCallback
});
});
},
showSilenceModal(user, opts) {
this._showControlModal('silence', user, opts);
},
showSuspendModal(user, opts) {
this._showControlModal('suspend', user, opts);
},
showModerationHistory(target) {
let controller = showModal('admin-moderation-history', { admin: true });
controller.loadHistory(target);
},
_deleteSpammer(adminUser) {
// Try loading the email if the site supports it
let tryEmail = this.siteSettings.show_email_on_profile ?
adminUser.checkEmail() :
Ember.RSVP.resolve();
return tryEmail.then(() => {
let message = I18n.messageFormat('flagging.delete_confirm_MF', {
"POSTS": adminUser.get('post_count'),
"TOPICS": adminUser.get('topic_count'),
email: adminUser.get('email') || I18n.t("flagging.hidden_email_address"),
ip_address: adminUser.get('ip_address') || I18n.t("flagging.ip_address_missing")
});
let userId = adminUser.get('id');
return new Ember.RSVP.Promise((resolve, reject) => {
const buttons = [
{
label: I18n.t("composer.cancel"),
class: "d-modal-cancel",
link: true
},
{
label: `${iconHTML('exclamation-triangle')} ` + I18n.t("flagging.yes_delete_spammer"),
class: "btn btn-danger confirm-delete",
callback() {
return ajax(`/admin/users/${userId}.json`, {
type: 'DELETE',
data: {
delete_posts: true,
block_email: true,
block_urls: true,
block_ip: true,
delete_as_spammer: true,
context: window.location.pathname
}
}).then(result => {
if (result.deleted) {
resolve();
} else {
throw 'failed to delete';
}
}).catch(() => {
bootbox.alert(I18n.t("admin.user.delete_failed"));
reject();
});
}
}
];
bootbox.dialog(message, buttons, {classes: "flagging-delete-spammer"});
});
});
}
});

View File

@ -20,14 +20,16 @@
{{#if currentUser.admin}}
{{nav-item route='adminCustomize' label='admin.customize.title'}}
{{nav-item route='adminApi' label='admin.api.title'}}
{{nav-item route='admin.backups' label='admin.backups.title'}}
{{#if siteSettings.enable_backups}}
{{nav-item route='admin.backups' label='admin.backups.title'}}
{{/if}}
{{/if}}
{{nav-item route='adminPlugins' label='admin.plugins.title'}}
{{plugin-outlet name="admin-menu" connectorTagName="li"}}
</ul>
<div class='boxed white admin-content'>
<div class='admin-contents'>
<div class='admin-contents {{adminContentsClassName}}'>
{{outlet}}
</div>
</div>

View File

@ -29,5 +29,5 @@
{{/if}}
{{#unless hasMasterKey}}
<button class='btn' {{action "generateMasterKey"}}><i class="fa fa-key"></i>{{i18n 'admin.api.generate_master'}}</button>
<button class='btn' {{action "generateMasterKey"}}>{{d-icon "key"}}</button>
{{/unless}}

View File

@ -1,10 +1,8 @@
<div class="api">
{{#admin-nav}}
{{nav-item route='adminApiKeys' label='admin.api.title'}}
{{nav-item route='adminWebHooks' label='admin.web_hooks.title'}}
{{/admin-nav}}
{{#admin-nav}}
{{nav-item route='adminApiKeys' label='admin.api.title'}}
{{nav-item route='adminWebHooks' label='admin.web_hooks.title'}}
{{/admin-nav}}
<div class="admin-container">
{{outlet}}
</div>
<div class="admin-container">
{{outlet}}
</div>

View File

@ -4,6 +4,7 @@
<ul class="nav nav-pills">
{{nav-item route='admin.backups.index' label='admin.backups.menu.backups'}}
{{nav-item route='admin.backups.logs' label='admin.backups.menu.logs'}}
{{plugin-outlet name="downloader" tagName=""}}
</ul>
</div>
<div class="pull-right">

View File

@ -3,7 +3,7 @@
<div>
{{#link-to 'adminBadges.show' 'new' class="btn"}}
{{fa-icon "plus"}} {{i18n 'admin.badges.new'}}
{{d-icon "plus"}} {{i18n 'admin.badges.new'}}
{{/link-to}}
</div>
{{/d-section}}

View File

@ -26,9 +26,8 @@
{{combo-box name="badge_type_id"
value=buffered.badge_type_id
content=badgeTypes
optionValuePath="content.id"
optionLabelPath="content.name"
disabled=readOnly}}
allowInitialValueMutation=true
isDisabled=readOnly}}
</div>
<div>
@ -36,9 +35,8 @@
{{combo-box name="badge_grouping_id"
value=buffered.badge_grouping_id
content=badgeGroupings
optionValuePath="content.id"
optionLabelPath="content.displayName"}}
&nbsp;<button {{action "editGroupings"}} class='btn'>{{fa-icon 'pencil'}}</button>
nameProperty="name"}}
&nbsp;<button {{action "editGroupings"}} class='btn'>{{d-icon 'pencil'}}</button>
</div>
@ -63,7 +61,7 @@
{{#if siteSettings.enable_badge_sql}}
<div>
<label for="query">{{i18n 'admin.badges.query'}}</label>
{{textarea name="query" value=buffered.query disabled=readOnly}}
{{ace-editor content=buffered.query mode="sql" disabled=readOnly}}
</div>
{{#if hasQuery}}

View File

@ -15,7 +15,7 @@
{{/each}}
</ul>
{{#link-to 'adminBadges.show' 'new' class="btn"}}
{{fa-icon "plus"}} {{i18n 'admin.badges.new'}}
{{d-icon "plus"}} {{i18n 'admin.badges.new'}}
{{/link-to}}
<br>
<br>

View File

@ -1,6 +1,6 @@
<td class="title">
{{#if report.icon}}
{{fa-icon report.icon}}
{{d-icon report.icon}}
{{/if}}
<a href="{{report.reportUrl}}">{{report.title}}</a>
</td>
@ -8,15 +8,15 @@
<td class="value">{{number report.todayCount}}</td>
<td class="value {{report.yesterdayTrend}}" title={{report.yesterdayCountTitle}}>
{{number report.yesterdayCount}} {{fa-icon "caret-up" class="up"}} {{fa-icon "caret-down" class="down"}}
{{number report.yesterdayCount}} {{d-icon "caret-up" class="up"}} {{d-icon "caret-down" class="down"}}
</td>
<td class="value {{report.sevenDayTrend}}" title={{report.sevenDayCountTitle}}>
{{number report.lastSevenDaysCount}} {{fa-icon "caret-up" class="up"}} {{fa-icon "caret-down" class="down"}}
{{number report.lastSevenDaysCount}} {{d-icon "caret-up" class="up"}} {{d-icon "caret-down" class="down"}}
</td>
<td class="value {{report.thirtyDayTrend}}" title={{report.thirtyDayCountTitle}}>
{{number report.lastThirtyDaysCount}} {{fa-icon "caret-up" class="up"}} {{fa-icon "caret-down" class="down"}}
{{number report.lastThirtyDaysCount}} {{d-icon "caret-up" class="up"}} {{d-icon "caret-down" class="down"}}
</td>
<td class="value">{{number report.total}}</td>

View File

@ -1,6 +1,6 @@
{{#if editing}}
{{#admin-form-row label="admin.user_fields.type"}}
{{combo-box content=fieldTypes valueAttribute="id" value=buffered.field_type}}
{{combo-box content=fieldTypes value=buffered.field_type}}
{{/admin-form-row}}
{{#admin-form-row label="admin.user_fields.name"}}

View File

@ -9,7 +9,7 @@
{{input value=buffered.path_whitelist placeholder="/blog/.*" enter="save" class="path-whitelist"}}
</td>
<td>
{{category-chooser value=categoryId}}
{{category-chooser value=categoryId class="small"}}
</td>
<td>
{{d-button icon="check" action="save" class="btn-primary" disabled=cantSave}}

View File

@ -0,0 +1,18 @@
<div class='flag-user'>
{{#link-to 'adminUser' user.id user.username class='flag-user-avatar'}}
{{avatar user imageSize="small"}}
{{/link-to}}
<div class='flag-user-details'>
<div class='flag-user-who'>
{{#link-to 'adminUser' user.id user.username class="flag-user-username"}}
{{user.username}}
{{/link-to}}
<div class='flag-user-date'>
{{format-age date}}
</div>
</div>
<div class='flag-user-extra'>
{{yield}}
</div>
</div>
</div>

View File

@ -0,0 +1,7 @@
{{#link-to 'adminUser' response.user.id response.user.username class="response-avatar"}}
{{avatar response.user imageSize="small"}}
{{/link-to}}
<div class='excerpt'>{{{response.excerpt}}}</div>
{{#if hasMore}}
<a href={{permalink}} class="has-more">{{i18n 'admin.flags.more'}}</a>
{{/if}}

View File

@ -0,0 +1,163 @@
<div class='flagged-post-details'>
<div class="flagged-post-avatar">
{{#if flaggedPost.postAuthorFlagged}}
{{#if flaggedPost.user}}
{{#link-to 'adminUser' flaggedPost.user.id flaggedPost.user.username}}
{{avatar flaggedPost.user imageSize="large"}}
{{/link-to}}
{{#if flaggedPost.wasEdited}}
<div class='edited-after'>
{{d-icon "pencil" title="admin.flags.was_edited"}}
</div>
{{/if}}
{{/if}}
{{/if}}
{{#if canAct}}
{{#if flaggedPost.previous_flags_count}}
<span title="{{i18n 'admin.flags.previous_flags_count' count=flaggedPost.previous_flags_count}}" class="badge-notification previous-flagged-posts">{{flaggedPost.previous_flags_count}}</span>
{{/if}}
{{/if}}
</div>
<div class="flagged-post-contents">
<div class='flagged-post-user-details'>
<a class='username' href={{user.path}} data-user-card={{flaggedPost.user.username}}>{{format-username flaggedPost.user.username}}</a>
</div>
<div class='flagged-post-excerpt'>
{{#unless hideTitle}}
<h3>
{{#if flaggedPost.topic.isPrivateMessage}}
<span class="private-message-glyph">{{d-icon "envelope"}}</span>
{{/if}}
{{topic-status topic=flaggedPost.topic}}
<a href='{{unbound flaggedPost.url}}'>{{{unbound flaggedPost.topic.fancyTitle}}}</a>
</h3>
{{/unless}}
{{#if flaggedPost.postAuthorFlagged}}
{{#if expanded}}
{{{flaggedPost.cooked}}}
{{else}}
<p>
{{{flaggedPost.excerpt}}}
<a href {{action "expand"}}>{{i18n "admin.flags.show_full"}}</a>
</p>
{{/if}}
{{/if}}
</div>
{{#if flaggedPost.topicFlagged}}
<div class='flagged-post-message'>
<span class='text'>{{{i18n 'admin.flags.topic_flagged'}}}</span>
<a href={{flaggedPost.url}} class="btn">{{i18n 'admin.flags.visit_topic'}}</a>
</div>
{{/if}}
{{#each flaggedPost.conversations as |c|}}
<div class='flag-conversation'>
{{#if c.response}}
{{flagged-post-response response=c.response}}
{{#if c.reply}}
{{flagged-post-response response=c.reply hasMore=c.hasMore permalink=c.permalink}}
{{/if}}
<a href={{c.permalink}} class="btn reply-conversation btn-small">
{{d-icon "reply"}}
{{i18n "admin.flags.reply_message"}}
</a>
{{/if}}
</div>
{{/each}}
<div class='flag-user-lists'>
<div class='flagged-by'>
<div class='user-list-title'>
{{i18n "admin.flags.flagged_by"}}
</div>
<div class='flag-users'>
{{#each flaggedPost.post_actions as |postAction|}}
{{#flag-user user=postAction.user date=postAction.created_at}}
<div class='flagger-flag-type'>
{{post-action-title postAction.post_action_type_id postAction.name_key}}
</div>
{{/flag-user}}
{{/each}}
</div>
</div>
{{#if showResolvedBy}}
<div class='flagged-post-resolved-by'>
<div class='user-list-title'>
{{i18n "admin.flags.resolved_by"}}
</div>
<div class='flag-users'>
{{#each flaggedPost.post_actions as |postAction|}}
{{#flag-user user=postAction.disposed_by date=postAction.disposed_at}}
{{disposition-icon postAction.disposition}}
{{#if postAction.staff_took_action}}
{{d-icon "gavel" title="admin.flags.took_action"}}
{{/if}}
{{/flag-user}}
{{/each}}
</div>
</div>
{{/if}}
</div>
{{#if suspended}}
<div class='suspended-message'>
The user was suspended for this post.
</div>
{{/if}}
<div class='flagged-post-controls'>
{{#if canAct}}
{{admin-agree-flag-dropdown
post=flaggedPost
removeAfter=(action "removeAfter") }}
{{#if flaggedPost.postHidden}}
{{d-button
title="admin.flags.disagree_flag_unhide_post_title"
class="disagree-flag"
action="disagree"
icon="thumbs-o-down"
label="admin.flags.disagree_flag_unhide_post"}}
{{else}}
{{d-button
title="admin.flags.disagree_flag_title"
class="disagree-flag"
action="disagree"
icon="thumbs-o-down"
label="admin.flags.disagree_flag"}}
{{/if}}
{{d-button
class="defer-flag"
title="admin.flags.defer_flag_title"
action="defer"
icon="external-link"
label="admin.flags.defer_flag"}}
{{admin-delete-flag-dropdown post=flaggedPost removeAfter=(action "removeAfter")}}
{{#unless suspended}}
{{d-button
class="btn-danger suspend-user"
icon="ban"
label="admin.flags.suspend_user"
title="admin.flags.suspend_user_title"
action=(action "showSuspendModal")}}
{{/unless}}
{{/if}}
{{d-button
icon="list"
label="admin.flags.moderation_history"
action=(action "showModerationHistory")}}
</div>
{{plugin-outlet
name="flagged-post-below-controls"
tagName=""
args=(hash flaggedPost=flaggedPost canAct=canAct)}}
</div>
</div>

View File

@ -0,0 +1,16 @@
{{#if flaggedPosts}}
{{#load-more selector=".flagged-post" action=(action "loadMore")}}
<div class='flagged-posts'>
{{#each flaggedPosts as |flaggedPost|}}
{{flagged-post
flaggedPost=flaggedPost
filter=filter
showResolvedBy=showResolvedBy
removePost=(action "removePost" flaggedPost)
hideTitle=topic}}
{{/each}}
</div>
{{/load-more}}
{{else}}
<p>{{i18n 'admin.flags.no_results'}}</p>
{{/if}}

View File

@ -0,0 +1,5 @@
{{#each users as |u|}}
{{#link-to 'adminUser' u.id u.username class="flagged-topic-user"}}
{{avatar u imageSize="small"}}
{{/link-to}}
{{/each}}

View File

@ -0,0 +1,17 @@
<td class='date'>
{{format-date item.created_at}}
</td>
<td class='history-item-action'>
<div class='action-name'>
{{i18n (concat "admin.moderation_history.actions." item.action_name)}}
</div>
<div class='action-details'>{{item.details}}</div>
</td>
<td class='history-item-actor'>
{{#if item.acting_user}}
{{#user-link user=item.acting_user}}
{{avatar item.acting_user imageSize="small"}}
<span>{{format-username item.acting_user.username}}</span>
{{/user-link}}
{{/if}}
</td>

View File

@ -1,4 +1,4 @@
<div class="validation-error {{unless message 'hidden'}}">
{{fa-icon "times"}}
{{d-icon "times"}}
{{message}}
</div>

View File

@ -2,7 +2,7 @@
<h3>{{unbound settingName}}</h3>
</div>
<div class="setting-value">
{{component componentName setting=setting value=buffered.value validationMessage=validationMessage}}
{{component componentName setting=setting value=buffered.value validationMessage=validationMessage preview=preview}}
</div>
{{#if dirty}}
<div class='setting-controls'>

View File

@ -1,3 +1,3 @@
{{category-selector categories=selectedCategories blacklist=selectedCategories}}
{{category-selector categories=selectedCategories}}
<div class='desc'>{{{unbound setting.description}}}</div>
{{setting-validation-message message=validationMessage}}

View File

@ -1,4 +1,4 @@
{{combo-box valueAttribute="value" content=setting.validValues value=value none=setting.allowsNone}}
{{combo-box castInteger=true valueAttribute="value" content=setting.validValues value=value none=setting.allowsNone}}
{{preview}}
{{setting-validation-message message=validationMessage}}
<div class='desc'>{{{unbound setting.description}}}</div>

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