Merge master
@ -1,7 +1,5 @@
|
||||
app/assets/javascripts/env.js
|
||||
app/assets/javascripts/main_include.js
|
||||
app/assets/javascripts/main_include_admin.js
|
||||
app/assets/javascripts/pagedown_custom.js
|
||||
app/assets/javascripts/vendor.js
|
||||
app/assets/javascripts/locales/i18n.js
|
||||
app/assets/javascripts/ember-addons/
|
||||
@ -9,13 +7,12 @@ app/assets/javascripts/discourse/lib/autosize.js.es6
|
||||
lib/javascripts/locale/
|
||||
lib/javascripts/messageformat.js
|
||||
lib/javascripts/moment.js
|
||||
lib/javascripts/moment-timezone-with-data.js
|
||||
lib/javascripts/moment_locale/
|
||||
lib/highlight_js/
|
||||
plugins/**/lib/javascripts/locale
|
||||
public/javascripts/
|
||||
spec/phantom_js/smoke_test.js
|
||||
vendor/
|
||||
test/javascripts/test_helper.js
|
||||
test/javascripts/test_helper.js
|
||||
test/javascripts/fixtures
|
||||
test/javascripts/helpers/assertions.js
|
||||
app/assets/javascripts/ember-addons/
|
||||
|
7
.gitattributes
vendored
@ -1,11 +1,14 @@
|
||||
# Set default behaviour, in case users don't have core.autocrlf set.
|
||||
* text=auto
|
||||
|
||||
# Explicitly declare text files we want to always be normalized and converted
|
||||
# Treat email fixtures as binary files so CRLF are not converted to LF.
|
||||
*.eml binary
|
||||
|
||||
# Explicitly declare text files we want to always be normalized and converted
|
||||
# to native line endings on checkout.
|
||||
*.yml text
|
||||
|
||||
# Custom for Visual Studio, very unlikely, but lets keep it
|
||||
# Custom for Visual Studio, very unlikely, but lets keep it
|
||||
*.cs diff=csharp
|
||||
*.sln merge=union
|
||||
*.csproj merge=union
|
||||
|
13
.gitignore
vendored
@ -19,8 +19,6 @@ public/tombstone/*
|
||||
|
||||
# Ignore bundler config
|
||||
/.bundle
|
||||
/.vagrant
|
||||
/.vagrantfile
|
||||
/cache
|
||||
/coverage/*
|
||||
|
||||
@ -48,13 +46,13 @@ bootsnap-compile-cache/
|
||||
|
||||
# Ignore plugins except for the bundled ones.
|
||||
/plugins/*
|
||||
!/plugins/emoji/
|
||||
!/plugins/lazyYT/
|
||||
!/plugins/poll/
|
||||
!/plugins/discourse-details/
|
||||
!/plugins/discourse-nginx-performance-report
|
||||
!/plugins/discourse-narrative-bot
|
||||
!/plugins/discourse-presence
|
||||
!/plugins/discourse-local-dates
|
||||
/plugins/*/auto_generated/
|
||||
|
||||
/spec/fixtures/plugins/my_plugin/auto_generated
|
||||
@ -80,6 +78,7 @@ discourse.sublime-workspace
|
||||
*~
|
||||
*.swp
|
||||
*.swo
|
||||
*.swm
|
||||
|
||||
# don't check in multisite config
|
||||
config/multisite.yml
|
||||
@ -95,9 +94,6 @@ config/fog_credentials.yml
|
||||
script/download_db
|
||||
script/refresh_db
|
||||
|
||||
# temp directory for chef (used to configure vagrant VM)
|
||||
chef/tmp/*
|
||||
|
||||
# .procfile
|
||||
.procfile
|
||||
|
||||
@ -117,9 +113,12 @@ bundler_stubs/*
|
||||
vendor/bundle/*
|
||||
*.db
|
||||
|
||||
#ignore jetbrains ide file
|
||||
# ignore jetbrains ide file
|
||||
*.iml
|
||||
|
||||
# vim swap
|
||||
*.swn
|
||||
|
||||
# ignore nodejs files
|
||||
/node_modules
|
||||
/package-lock.json
|
||||
|
@ -89,12 +89,12 @@ Layout/TrailingWhitespace:
|
||||
Lint/Debugger:
|
||||
Enabled: true
|
||||
|
||||
Lint/BlockAlignment:
|
||||
Layout/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:
|
||||
Layout/EndAlignment:
|
||||
Enabled: true
|
||||
EnforcedStyleAlignWith: variable
|
||||
|
||||
|
35
.travis.yml
@ -1,9 +1,15 @@
|
||||
language: ruby
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- beta
|
||||
- stable
|
||||
|
||||
env:
|
||||
global:
|
||||
- DISCOURSE_HOSTNAME=www.example.com
|
||||
- RUBY_GC_MALLOC_LIMIT=50000000
|
||||
- RUBY_GLOBAL_METHOD_CACHE_SIZE=131072
|
||||
matrix:
|
||||
- "RAILS_MASTER=0 QUNIT_RUN=0 RUN_LINT=0"
|
||||
- "RAILS_MASTER=0 QUNIT_RUN=1 RUN_LINT=0"
|
||||
@ -11,8 +17,9 @@ env:
|
||||
|
||||
addons:
|
||||
chrome: stable
|
||||
postgresql: 9.5
|
||||
postgresql: 9.6
|
||||
apt:
|
||||
update: true
|
||||
packages:
|
||||
- gifsicle
|
||||
- jpegoptim
|
||||
@ -21,13 +28,10 @@ addons:
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- rvm: 2.5.0
|
||||
|
||||
rvm:
|
||||
- 2.5.0
|
||||
- 2.4.2
|
||||
- 2.3.4
|
||||
- 2.5.1
|
||||
- 2.4.4
|
||||
|
||||
services:
|
||||
- redis-server
|
||||
@ -36,6 +40,7 @@ sudo: required
|
||||
dist: trusty
|
||||
|
||||
cache:
|
||||
apt: true
|
||||
yarn: true
|
||||
directories:
|
||||
- vendor/bundle
|
||||
@ -49,13 +54,15 @@ before_install:
|
||||
- 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
|
||||
- git clone --depth=1 https://github.com/discourse/discourse-staff-notes.git plugins/discourse-staff-notes
|
||||
- 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"
|
||||
- bash -c "if [ '$RAILS_MASTER' == '1' ]; then bundle update --retry=3 --jobs=3 arel rails seed-fu > /dev/null; fi"
|
||||
- bash -c "if [ '$RAILS_MASTER' == '0' ]; then bundle install --without development --deployment --retry=3 --jobs=3 > /dev/null; fi"
|
||||
- bash -c "if [ '$RUN_LINT' == '1' ]; then yarn global add eslint babel-eslint > /dev/null; fi"
|
||||
- bash -c "if [ '$QUNIT_RUN' == '1' ]; then yarn install --dev > /dev/null; fi"
|
||||
- bash -c "if [ '$RUN_LINT' != '1' ]; then bundle exec rake db:create db:migrate > /dev/null; fi"
|
||||
|
||||
script:
|
||||
- |
|
||||
@ -68,11 +75,9 @@ script:
|
||||
eslint --ext .es6 plugins/**/test/javascripts && \
|
||||
eslint app/assets/javascripts test/javascripts
|
||||
else
|
||||
bundle exec rake db:create db:migrate
|
||||
|
||||
if [ '$QUNIT_RUN' == '1' ]; then
|
||||
bundle exec rake qunit:test['400000'] && \
|
||||
bundle exec rake plugin:spec
|
||||
bundle exec rake qunit:test['500000'] && \
|
||||
bundle exec rake plugin:qunit
|
||||
else
|
||||
bundle exec rspec && bundle exec rake plugin:spec
|
||||
fi
|
||||
|
34
.tx/config
@ -2,13 +2,13 @@
|
||||
host = https://www.transifex.com
|
||||
lang_map = el_GR: el, es_ES: es, fr_FR: fr, ko_KR: ko, pt_PT: pt, sk_SK: sk, vi_VN: vi
|
||||
|
||||
[discourse-org.clientenyml]
|
||||
[discourse-org.core-client-yml]
|
||||
file_filter = config/locales/client.<lang>.yml
|
||||
source_file = config/locales/client.en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
|
||||
[discourse-org.serverenyml]
|
||||
[discourse-org.core-server-yml]
|
||||
file_filter = config/locales/server.<lang>.yml
|
||||
source_file = config/locales/server.en.yml
|
||||
source_lang = en
|
||||
@ -50,6 +50,36 @@ source_file = plugins/discourse-presence/config/locales/server.en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
|
||||
[discourse-org.coreplugindetailsclientyml]
|
||||
file_filter = plugins/discourse-details/config/locales/client.<lang>.yml
|
||||
source_file = plugins/discourse-details/config/locales/client.en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
|
||||
[discourse-org.coreplugindetailsserveryml]
|
||||
file_filter = plugins/discourse-details/config/locales/server.<lang>.yml
|
||||
source_file = plugins/discourse-details/config/locales/server.en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
|
||||
[discourse-org.corepluginnginx-performance-reportserveryml]
|
||||
file_filter = plugins/discourse-nginx-performance-report/config/locales/server.<lang>.yml
|
||||
source_file = plugins/discourse-nginx-performance-report/config/locales/server.en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
|
||||
[discourse-org.core-plugin-local-dates-client-yml]
|
||||
file_filter = plugins/discourse-local-dates/config/locales/client.<lang>.yml
|
||||
source_file = plugins/discourse-local-dates/config/locales/client.en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
|
||||
[discourse-org.core-plugin-local-dates-server-yml]
|
||||
file_filter = plugins/discourse-local-dates/config/locales/server.<lang>.yml
|
||||
source_file = plugins/discourse-local-dates/config/locales/server.en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
|
||||
[discourse-org.403html]
|
||||
file_filter = public/403.<lang>.html
|
||||
source_file = public/403.html
|
||||
|
6
Brewfile
@ -1,11 +1,5 @@
|
||||
# Install development dependencies on Mac OS X using Homebrew (http://mxcl.github.com/homebrew)
|
||||
|
||||
# add this repo to Homebrew's sources
|
||||
tap 'homebrew/dupes'
|
||||
|
||||
# install the gcc compiler required for ruby
|
||||
brew 'apple-gcc42'
|
||||
|
||||
# you probably already have git installed; ensure that it is the latest version
|
||||
brew 'git'
|
||||
|
||||
|
40
Gemfile
@ -2,8 +2,7 @@ source 'https://rubygems.org'
|
||||
# if there is a super emergency and rubygems is playing up, try
|
||||
#source 'http://production.cf.rubygems.org'
|
||||
|
||||
# does not install in linux ATM, so hack this for now
|
||||
gem 'bootsnap', require: false
|
||||
gem 'bootsnap', require: false, platform: :mri
|
||||
|
||||
def rails_master?
|
||||
ENV["RAILS_MASTER"] == '1'
|
||||
@ -25,8 +24,7 @@ else
|
||||
gem 'seed-fu'
|
||||
end
|
||||
|
||||
gem 'mail'
|
||||
gem 'mime-types', require: 'mime/types/columnar'
|
||||
gem 'mail', '2.7.1.rc1', require: false
|
||||
gem 'mini_mime'
|
||||
gem 'mini_suffix'
|
||||
|
||||
@ -36,12 +34,12 @@ gem 'redis-namespace'
|
||||
|
||||
gem 'active_model_serializers', '~> 0.8.3'
|
||||
|
||||
gem 'onebox', '1.8.33'
|
||||
gem 'onebox', '1.8.48'
|
||||
|
||||
gem 'http_accept_language', '~>2.0.5', require: false
|
||||
|
||||
gem 'ember-rails', '0.18.5'
|
||||
gem 'ember-source'
|
||||
gem 'ember-source', '2.13.3'
|
||||
gem 'ember-handlebars-template', '0.7.5'
|
||||
gem 'barber'
|
||||
|
||||
@ -49,18 +47,18 @@ gem 'message_bus'
|
||||
|
||||
gem 'rails_multisite'
|
||||
|
||||
gem 'fast_xs'
|
||||
gem 'fast_xs', platform: :mri
|
||||
|
||||
gem 'fast_xor'
|
||||
# may move to xorcist post: https://github.com/fny/xorcist/issues/4
|
||||
gem 'fast_xor', platform: :mri
|
||||
|
||||
# Forked until https://github.com/sdsykes/fastimage/pull/93 is merged
|
||||
gem 'discourse_fastimage', require: 'fastimage'
|
||||
gem 'fastimage'
|
||||
|
||||
gem 'aws-sdk-s3', require: false
|
||||
gem 'excon', require: false
|
||||
gem 'unf', require: false
|
||||
|
||||
gem 'email_reply_trimmer', '0.1.8'
|
||||
gem 'email_reply_trimmer', '~> 0.1'
|
||||
|
||||
# Forked until https://github.com/toy/image_optim/pull/149 is merged
|
||||
gem 'discourse_image_optim', require: 'image_optim'
|
||||
@ -68,9 +66,6 @@ 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'
|
||||
@ -83,7 +78,7 @@ gem 'omniauth-oauth2', require: false
|
||||
|
||||
gem 'omniauth-google-oauth2'
|
||||
gem 'oj'
|
||||
gem 'pg'
|
||||
gem 'pg', '~> 0.21.0'
|
||||
gem 'pry-rails', require: false
|
||||
gem 'r2', '~> 0.2.5', require: false
|
||||
gem 'rake'
|
||||
@ -120,8 +115,7 @@ group :test, :development do
|
||||
gem 'listen', require: false
|
||||
gem 'certified', require: false
|
||||
# later appears to break Fabricate(:topic, category: category)
|
||||
gem 'fabrication', '2.9.8', require: false
|
||||
gem 'discourse-qunit-rails', require: 'qunit-rails'
|
||||
gem 'fabrication', require: false
|
||||
gem 'mocha', require: false
|
||||
gem 'rb-fsevent', require: RUBY_PLATFORM =~ /darwin/i ? 'rb-fsevent' : false
|
||||
gem 'rb-inotify', '~> 0.9', require: RUBY_PLATFORM =~ /linux/i ? 'rb-inotify' : false
|
||||
@ -145,7 +139,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'
|
||||
gem 'fast_blank', platform: :mri
|
||||
|
||||
# this provides a very efficient lru cache
|
||||
gem 'lru_redux'
|
||||
@ -159,7 +153,7 @@ gem 'htmlentities', require: false
|
||||
gem 'flamegraph', require: false
|
||||
gem 'rack-mini-profiler', require: false
|
||||
|
||||
gem 'unicorn', require: false
|
||||
gem 'unicorn', require: false, platform: :mri
|
||||
gem 'puma', require: false
|
||||
gem 'rbtrace', require: false, platform: :mri
|
||||
gem 'gc_tracer', require: false, platform: :mri
|
||||
@ -179,9 +173,17 @@ gem 'logster'
|
||||
|
||||
gem 'sassc', require: false
|
||||
|
||||
gem 'rotp'
|
||||
gem 'rqrcode'
|
||||
|
||||
gem 'sshkey', 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'
|
||||
gem 'reverse_markdown'
|
||||
end
|
||||
|
||||
gem 'webpush', require: false
|
||||
|
134
Gemfile.lock
@ -41,9 +41,8 @@ GEM
|
||||
annotate (2.7.2)
|
||||
activerecord (>= 3.2, < 6.0)
|
||||
rake (>= 10.4, < 13.0)
|
||||
ansi (1.5.0)
|
||||
arel (8.0.0)
|
||||
ast (2.3.0)
|
||||
ast (2.4.0)
|
||||
aws-partitions (1.24.0)
|
||||
aws-sdk-core (3.6.0)
|
||||
aws-partitions (~> 1.0)
|
||||
@ -64,9 +63,9 @@ GEM
|
||||
coderay (>= 1.0.0)
|
||||
erubis (>= 2.6.6)
|
||||
rack (>= 0.9.0)
|
||||
binding_of_caller (0.7.2)
|
||||
binding_of_caller (0.8.0)
|
||||
debug_inspector (>= 0.0.1)
|
||||
bootsnap (1.0.0)
|
||||
bootsnap (1.1.8)
|
||||
msgpack (~> 1.0)
|
||||
builder (3.2.3)
|
||||
bullet (5.5.1)
|
||||
@ -74,25 +73,23 @@ GEM
|
||||
uniform_notifier (~> 1.10.0)
|
||||
byebug (9.0.6)
|
||||
certified (1.0.0)
|
||||
coderay (1.1.1)
|
||||
chunky_png (1.3.8)
|
||||
coderay (1.1.2)
|
||||
concurrent-ruby (1.0.5)
|
||||
connection_pool (2.2.1)
|
||||
cppjieba_rb (0.3.0)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
crass (1.0.2)
|
||||
crass (1.0.3)
|
||||
debug_inspector (0.0.3)
|
||||
diff-lcs (1.3)
|
||||
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)
|
||||
email_reply_trimmer (0.1.8)
|
||||
email_reply_trimmer (0.1.12)
|
||||
ember-data-source (2.2.1)
|
||||
ember-source (>= 1.8, < 3.0)
|
||||
ember-handlebars-template (0.7.5)
|
||||
@ -120,6 +117,7 @@ GEM
|
||||
rake
|
||||
rake-compiler
|
||||
fast_xs (0.8.0)
|
||||
fastimage (2.1.1)
|
||||
ffi (1.9.18)
|
||||
flamegraph (0.9.5)
|
||||
foreman (0.84.0)
|
||||
@ -133,6 +131,7 @@ GEM
|
||||
hashie (3.5.5)
|
||||
highline (1.7.8)
|
||||
hiredis (0.6.1)
|
||||
hkdf (0.3.0)
|
||||
htmlentities (4.3.4)
|
||||
http_accept_language (2.0.5)
|
||||
i18n (0.8.6)
|
||||
@ -144,7 +143,7 @@ GEM
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
jwt (1.5.6)
|
||||
kgio (2.11.0)
|
||||
kgio (2.11.1)
|
||||
libv8 (6.3.292.48.1)
|
||||
listen (3.1.5)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
@ -159,21 +158,18 @@ GEM
|
||||
logstash-logger (0.25.1)
|
||||
logstash-event (~> 1.2)
|
||||
logster (1.2.9)
|
||||
loofah (2.1.1)
|
||||
loofah (2.2.2)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
lru_redux (1.1.0)
|
||||
mail (2.6.6)
|
||||
mime-types (>= 1.16, < 4)
|
||||
memory_profiler (0.9.8)
|
||||
message_bus (2.1.1)
|
||||
mail (2.7.1.rc1)
|
||||
mini_mime (>= 0.1.1)
|
||||
memory_profiler (0.9.10)
|
||||
message_bus (2.1.5)
|
||||
rack (>= 1.1.3)
|
||||
metaclass (0.0.4)
|
||||
method_source (0.8.2)
|
||||
mime-types (3.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2016.0521)
|
||||
mini_mime (0.1.3)
|
||||
mini_mime (1.0.0)
|
||||
mini_portile2 (2.3.0)
|
||||
mini_racer (0.1.15)
|
||||
libv8 (~> 6.3)
|
||||
@ -184,14 +180,14 @@ GEM
|
||||
metaclass (~> 0.0.1)
|
||||
mock_redis (0.17.3)
|
||||
moneta (1.0.0)
|
||||
msgpack (1.1.0)
|
||||
multi_json (1.12.1)
|
||||
msgpack (1.2.4)
|
||||
multi_json (1.13.1)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.0.0)
|
||||
mustache (1.0.5)
|
||||
nokogiri (1.8.1)
|
||||
nokogiri (1.8.2)
|
||||
mini_portile2 (~> 2.3.0)
|
||||
nokogumbo (1.4.13)
|
||||
nokogumbo (1.5.0)
|
||||
nokogiri
|
||||
oauth (0.5.1)
|
||||
oauth2 (1.3.1)
|
||||
@ -200,10 +196,7 @@ GEM
|
||||
multi_json (~> 1.3)
|
||||
multi_xml (~> 0.5)
|
||||
rack (>= 1.2, < 3)
|
||||
oga (2.10)
|
||||
ast
|
||||
ruby-ll (~> 2.1)
|
||||
oj (3.1.0)
|
||||
oj (3.4.0)
|
||||
omniauth (1.6.1)
|
||||
hashie (>= 3.4.6, < 3.6.0)
|
||||
rack (>= 1.6.2, < 3)
|
||||
@ -232,8 +225,7 @@ GEM
|
||||
omniauth-twitter (1.3.0)
|
||||
omniauth-oauth (~> 1.1)
|
||||
rack
|
||||
onebox (1.8.33)
|
||||
fast_blank (>= 1.0.0)
|
||||
onebox (1.8.48)
|
||||
htmlentities (~> 4.3)
|
||||
moneta (~> 1.0)
|
||||
multi_json (~> 1.11)
|
||||
@ -243,10 +235,10 @@ GEM
|
||||
openid-redis-store (0.0.2)
|
||||
redis
|
||||
ruby-openid
|
||||
parallel (1.12.0)
|
||||
parser (2.4.0.0)
|
||||
ast (~> 2.2)
|
||||
pg (0.20.0)
|
||||
parallel (1.12.1)
|
||||
parser (2.5.0.3)
|
||||
ast (~> 2.4.0)
|
||||
pg (0.21.0)
|
||||
powerpack (0.1.1)
|
||||
progress (3.3.1)
|
||||
pry (0.10.4)
|
||||
@ -260,22 +252,22 @@ GEM
|
||||
public_suffix (2.0.5)
|
||||
puma (3.9.1)
|
||||
r2 (0.2.6)
|
||||
rack (2.0.3)
|
||||
rack-mini-profiler (0.10.7)
|
||||
rack (2.0.5)
|
||||
rack-mini-profiler (1.0.0)
|
||||
rack (>= 1.2.0)
|
||||
rack-openid (1.3.1)
|
||||
rack (>= 1.1.0)
|
||||
ruby-openid (>= 2.1.8)
|
||||
rack-protection (2.0.0)
|
||||
rack-protection (2.0.1)
|
||||
rack
|
||||
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.1.2)
|
||||
rails-html-sanitizer (1.0.4)
|
||||
loofah (~> 2.2, >= 2.2.2)
|
||||
rails_multisite (2.0.4)
|
||||
activerecord (> 4.2, < 6)
|
||||
railties (> 4.2, < 6)
|
||||
railties (5.1.4)
|
||||
@ -284,24 +276,26 @@ GEM
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.18.1, < 2.0)
|
||||
rainbow (2.2.2)
|
||||
rake
|
||||
rainbow (3.0.0)
|
||||
raindrops (0.19.0)
|
||||
rake (12.1.0)
|
||||
rake (12.3.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)
|
||||
rbtrace (0.4.10)
|
||||
ffi (>= 1.0.6)
|
||||
msgpack (>= 0.4.3)
|
||||
trollop (>= 1.16.2)
|
||||
redis (3.3.5)
|
||||
redis-namespace (1.5.3)
|
||||
redis (~> 3.0, >= 3.0.4)
|
||||
redis (4.0.1)
|
||||
redis-namespace (1.6.0)
|
||||
redis (>= 3.0.4)
|
||||
request_store (1.3.2)
|
||||
rinku (2.0.2)
|
||||
rotp (3.3.0)
|
||||
rqrcode (0.10.1)
|
||||
chunky_png (~> 1.0)
|
||||
rspec (3.6.0)
|
||||
rspec-core (~> 3.6.0)
|
||||
rspec-expectations (~> 3.6.0)
|
||||
@ -327,16 +321,13 @@ GEM
|
||||
rspec-support (~> 3.6.0)
|
||||
rspec-support (3.6.0)
|
||||
rtlit (0.0.5)
|
||||
rubocop (0.51.0)
|
||||
rubocop (0.53.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 2.3.3.1, < 3.0)
|
||||
parser (>= 2.5)
|
||||
powerpack (~> 0.1)
|
||||
rainbow (>= 2.2.2, < 3.0)
|
||||
rainbow (>= 2.2.2, < 4.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)
|
||||
@ -345,10 +336,10 @@ GEM
|
||||
nokogiri (>= 1.6.0)
|
||||
ruby_dep (1.5.0)
|
||||
safe_yaml (1.0.4)
|
||||
sanitize (4.5.0)
|
||||
sanitize (4.6.4)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.4.4)
|
||||
nokogumbo (~> 1.4.1)
|
||||
nokogumbo (~> 1.4)
|
||||
sass (3.4.24)
|
||||
sassc (1.11.2)
|
||||
bundler
|
||||
@ -363,11 +354,11 @@ GEM
|
||||
shoulda-context (1.2.2)
|
||||
shoulda-matchers (2.8.0)
|
||||
activesupport (>= 3.0.0)
|
||||
sidekiq (5.0.5)
|
||||
sidekiq (5.1.3)
|
||||
concurrent-ruby (~> 1.0)
|
||||
connection_pool (~> 2.2, >= 2.2.0)
|
||||
rack-protection (>= 1.5.0)
|
||||
redis (>= 3.3.4, < 5)
|
||||
redis (>= 3.3.5, < 5)
|
||||
slop (3.6.0)
|
||||
sprockets (3.7.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
@ -376,6 +367,7 @@ GEM
|
||||
actionpack (>= 4.0)
|
||||
activesupport (>= 4.0)
|
||||
sprockets (>= 3.0.0)
|
||||
sshkey (1.9.0)
|
||||
stackprof (0.2.10)
|
||||
thor (0.19.4)
|
||||
thread_safe (0.3.6)
|
||||
@ -389,7 +381,7 @@ GEM
|
||||
unf_ext
|
||||
unf_ext (0.0.7.4)
|
||||
unicode-display_width (1.3.0)
|
||||
unicorn (5.3.1)
|
||||
unicorn (5.4.0)
|
||||
kgio (~> 2.6)
|
||||
raindrops (~> 0.7)
|
||||
uniform_notifier (1.10.0)
|
||||
@ -397,6 +389,9 @@ GEM
|
||||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff
|
||||
webpush (0.3.2)
|
||||
hkdf (~> 0.2)
|
||||
jwt
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
@ -419,20 +414,19 @@ DEPENDENCIES
|
||||
byebug
|
||||
certified
|
||||
cppjieba_rb
|
||||
discourse-qunit-rails
|
||||
discourse_fastimage
|
||||
discourse_image_optim
|
||||
email_reply_trimmer (= 0.1.8)
|
||||
email_reply_trimmer (~> 0.1)
|
||||
ember-handlebars-template (= 0.7.5)
|
||||
ember-rails (= 0.18.5)
|
||||
ember-source
|
||||
ember-source (= 2.13.3)
|
||||
excon
|
||||
execjs
|
||||
fabrication (= 2.9.8)
|
||||
fabrication
|
||||
fakeweb (~> 1.3.0)
|
||||
fast_blank
|
||||
fast_xor
|
||||
fast_xs
|
||||
fastimage
|
||||
flamegraph
|
||||
foreman
|
||||
gc_tracer
|
||||
@ -446,10 +440,9 @@ DEPENDENCIES
|
||||
logstash-logger
|
||||
logster
|
||||
lru_redux
|
||||
mail
|
||||
mail (= 2.7.1.rc1)
|
||||
memory_profiler
|
||||
message_bus
|
||||
mime-types
|
||||
mini_mime
|
||||
mini_racer
|
||||
mini_suffix
|
||||
@ -459,7 +452,6 @@ DEPENDENCIES
|
||||
multi_json
|
||||
mustache
|
||||
nokogiri
|
||||
oga
|
||||
oj
|
||||
omniauth
|
||||
omniauth-facebook
|
||||
@ -469,9 +461,9 @@ DEPENDENCIES
|
||||
omniauth-oauth2
|
||||
omniauth-openid
|
||||
omniauth-twitter
|
||||
onebox (= 1.8.33)
|
||||
onebox (= 1.8.48)
|
||||
openid-redis-store
|
||||
pg
|
||||
pg (~> 0.21.0)
|
||||
pry-nav
|
||||
pry-rails
|
||||
puma
|
||||
@ -487,6 +479,8 @@ DEPENDENCIES
|
||||
redis
|
||||
redis-namespace
|
||||
rinku
|
||||
rotp
|
||||
rqrcode
|
||||
rspec
|
||||
rspec-html-matchers
|
||||
rspec-rails
|
||||
@ -500,6 +494,7 @@ DEPENDENCIES
|
||||
shoulda
|
||||
sidekiq
|
||||
sprockets-rails
|
||||
sshkey
|
||||
stackprof
|
||||
thor
|
||||
tilt
|
||||
@ -507,6 +502,7 @@ DEPENDENCIES
|
||||
unf
|
||||
unicorn
|
||||
webmock
|
||||
webpush
|
||||
|
||||
BUNDLED WITH
|
||||
1.16.0
|
||||
1.16.1
|
||||
|
@ -22,7 +22,7 @@ Browse [lots more notable Discourse instances](https://www.discourse.org/custome
|
||||
|
||||
## Development
|
||||
|
||||
1. If you're **brand new to Ruby and Rails**, please see [**Discourse as Your First Rails App**](http://blog.discourse.org/2013/04/discourse-as-your-first-rails-app/) or our [**Discourse Vagrant Developer Guide**](docs/VAGRANT.md), which includes a development environment in a virtual machine.
|
||||
1. If you're **brand new to Ruby and Rails**, please see [**Discourse as Your First Rails App**](http://blog.discourse.org/2013/04/discourse-as-your-first-rails-app/).
|
||||
|
||||
2. If you're familiar with how Rails works and are comfortable setting up your own environment, use our [**Discourse Advanced Developer Guide**](docs/DEVELOPER-ADVANCED.md).
|
||||
|
||||
@ -57,7 +57,6 @@ Plus *lots* of Ruby Gems, a complete list of which is at [/master/Gemfile](https
|
||||
## Contributing
|
||||
|
||||
[](https://travis-ci.org/discourse/discourse)
|
||||
[](https://codeclimate.com/github/discourse/discourse)
|
||||
|
||||
Discourse is **100% free** and **open source**. We encourage and support an active, healthy community that
|
||||
accepts contributions from the public – including you!
|
||||
@ -84,7 +83,7 @@ The original Discourse code contributors can be found in [**AUTHORS.MD**](docs/A
|
||||
|
||||
## Copyright / License
|
||||
|
||||
Copyright 2014 - 2017 Civilized Discourse Construction Kit, Inc.
|
||||
Copyright 2014 - 2018 Civilized Discourse Construction Kit, Inc.
|
||||
|
||||
Licensed under the GNU General Public License Version 2.0 (or later);
|
||||
you may not use this work except in compliance with the License.
|
||||
|
48
Vagrantfile
vendored
@ -1,48 +0,0 @@
|
||||
# -*- mode: ruby -*-
|
||||
# vi: set ft=ruby :
|
||||
# See https://github.com/discourse/discourse/blob/master/docs/VAGRANT.md
|
||||
#
|
||||
Vagrant.configure("2") do |config|
|
||||
config.vm.box = 'discourse-16.04'
|
||||
config.vm.box_url = "https://www.dropbox.com/s/2132770g1e05c6d/discourse.box?dl=1"
|
||||
|
||||
# Make this VM reachable on the host network as well, so that other
|
||||
# VM's running other browsers can access our dev server.
|
||||
config.vm.network :private_network, ip: "192.168.10.200"
|
||||
|
||||
# Make it so that network access from the vagrant guest is able to
|
||||
# use SSH private keys that are present on the host without copying
|
||||
# them into the VM.
|
||||
config.ssh.forward_agent = true
|
||||
|
||||
config.vm.provider :virtualbox do |v|
|
||||
# This setting gives the VM 1024MB of RAM instead of the default 384.
|
||||
v.customize ["modifyvm", :id, "--memory", [ENV['DISCOURSE_VM_MEM'].to_i, 1024].max]
|
||||
|
||||
# Who has a single core cpu these days anyways?
|
||||
cpu_count = 2
|
||||
|
||||
# Determine the available cores in host system.
|
||||
# This mostly helps on linux, but it couldn't hurt on MacOSX.
|
||||
if RUBY_PLATFORM =~ /linux/
|
||||
cpu_count = `nproc`.to_i
|
||||
elsif RUBY_PLATFORM =~ /darwin/
|
||||
cpu_count = `sysctl -n hw.ncpu`.to_i
|
||||
end
|
||||
|
||||
# Assign additional cores to the guest OS.
|
||||
v.customize ["modifyvm", :id, "--cpus", cpu_count]
|
||||
v.customize ["modifyvm", :id, "--ioapic", "on"]
|
||||
|
||||
# This setting makes it so that network access from inside the vagrant guest
|
||||
# is able to resolve DNS using the hosts VPN connection.
|
||||
v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
|
||||
end
|
||||
|
||||
config.vm.network :forwarded_port, guest: 3000, host: 4000
|
||||
config.vm.network :forwarded_port, guest: 1080, host: 4080 # Mailcatcher
|
||||
|
||||
nfs_setting = RUBY_PLATFORM =~ /darwin/ || RUBY_PLATFORM =~ /linux/
|
||||
config.vm.synced_folder ".", "/vagrant", id: "vagrant-root"
|
||||
|
||||
end
|
BIN
app/assets/images/favicons/instagram.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
app/assets/images/push-notifications/check.png
Normal file
After Width: | Height: | Size: 844 B |
BIN
app/assets/images/push-notifications/discourse.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
BIN
app/assets/images/push-notifications/group_mentioned.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
app/assets/images/push-notifications/linked.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
app/assets/images/push-notifications/mentioned.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
app/assets/images/push-notifications/posted.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
app/assets/images/push-notifications/private_message.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
app/assets/images/push-notifications/quoted.png
Normal file
After Width: | Height: | Size: 1000 B |
BIN
app/assets/images/push-notifications/replied.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
@ -1,4 +1,5 @@
|
||||
import loadScript from 'discourse/lib/load-script';
|
||||
import { number } from 'discourse/lib/formatter';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: 'canvas',
|
||||
@ -22,10 +23,16 @@ export default Ember.Component.extend({
|
||||
data: data,
|
||||
options: {
|
||||
responsive: true,
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
title: (context) => moment(context[0].xLabel, "YYYY-MM-DD").format("LL")
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
yAxes: [{
|
||||
display: true,
|
||||
ticks: {
|
||||
callback: (label) => number(label),
|
||||
suggestedMin: 0
|
||||
}
|
||||
}]
|
||||
|
@ -1,4 +1,5 @@
|
||||
export default Ember.Component.extend({
|
||||
allTime: true,
|
||||
tagName: 'tr',
|
||||
reverseColors: Ember.computed.match('report.type', /^(time_to_first_response|topics_with_no_response)$/),
|
||||
classNameBindings: ['reverseColors']
|
||||
|
@ -1,3 +1,3 @@
|
||||
export default Ember.Component.extend({
|
||||
classNames: ['latest-topic-list']
|
||||
tagName: ''
|
||||
});
|
@ -0,0 +1,19 @@
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import AsyncReport from "admin/mixins/async-report";
|
||||
|
||||
export default Ember.Component.extend(AsyncReport, {
|
||||
classNames: ["dashboard-inline-table"],
|
||||
|
||||
fetchReport() {
|
||||
this._super();
|
||||
|
||||
let payload = this.buildPayload(["total", "prev30Days"]);
|
||||
|
||||
return Ember.RSVP.Promise.all(this.get("dataSources").map(dataSource => {
|
||||
return ajax(dataSource, payload)
|
||||
.then(response => {
|
||||
this.get("reports").pushObject(this.loadReport(response.report));
|
||||
});
|
||||
}));
|
||||
}
|
||||
});
|
@ -0,0 +1,164 @@
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import AsyncReport from "admin/mixins/async-report";
|
||||
import Report from "admin/models/report";
|
||||
import { number } from 'discourse/lib/formatter';
|
||||
import loadScript from "discourse/lib/load-script";
|
||||
import { registerTooltip, unregisterTooltip } from "discourse/lib/tooltip";
|
||||
|
||||
function collapseWeekly(data, average) {
|
||||
let aggregate = [];
|
||||
let bucket, i;
|
||||
let offset = data.length % 7;
|
||||
for(i = offset; i < data.length; i++) {
|
||||
|
||||
if (bucket && (i % 7 === offset)) {
|
||||
if (average) {
|
||||
bucket.y = parseFloat((bucket.y / 7.0).toFixed(2));
|
||||
}
|
||||
aggregate.push(bucket);
|
||||
bucket = null;
|
||||
}
|
||||
|
||||
bucket = bucket || { x: data[i].x, y: 0 };
|
||||
bucket.y += data[i].y;
|
||||
}
|
||||
return aggregate;
|
||||
}
|
||||
|
||||
export default Ember.Component.extend(AsyncReport, {
|
||||
classNames: ["chart", "dashboard-mini-chart"],
|
||||
total: 0,
|
||||
|
||||
init() {
|
||||
this._super();
|
||||
|
||||
this._colorsPool = ["rgb(0,136,204)", "rgb(235,83,148)"];
|
||||
},
|
||||
|
||||
didRender() {
|
||||
this._super();
|
||||
registerTooltip($(this.element).find("[data-tooltip]"));
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super();
|
||||
unregisterTooltip($(this.element).find("[data-tooltip]"));
|
||||
},
|
||||
|
||||
pickColorAtIndex(index) {
|
||||
return this._colorsPool[index] || this._colorsPool[0];
|
||||
},
|
||||
|
||||
fetchReport() {
|
||||
this._super();
|
||||
|
||||
let payload = this.buildPayload(["prev_period"]);
|
||||
|
||||
if (this._chart) {
|
||||
this._chart.destroy();
|
||||
this._chart = null;
|
||||
}
|
||||
|
||||
return Ember.RSVP.Promise.all(this.get("dataSources").map(dataSource => {
|
||||
return ajax(dataSource, payload)
|
||||
.then(response => {
|
||||
this.get("reports").pushObject(this.loadReport(response.report));
|
||||
});
|
||||
}));
|
||||
},
|
||||
|
||||
loadReport(report, previousReport) {
|
||||
Report.fillMissingDates(report);
|
||||
|
||||
if (report.data && report.data.length > 40) {
|
||||
report.data = collapseWeekly(report.data, report.average);
|
||||
}
|
||||
|
||||
if (previousReport && previousReport.color.length) {
|
||||
report.color = previousReport.color;
|
||||
} else {
|
||||
const dataSourceNameIndex = this.get("dataSourceNames").split(",").indexOf(report.type);
|
||||
report.color = this.pickColorAtIndex(dataSourceNameIndex);
|
||||
}
|
||||
|
||||
return Report.create(report);
|
||||
},
|
||||
|
||||
renderReport() {
|
||||
this._super();
|
||||
|
||||
Ember.run.schedule("afterRender", () => {
|
||||
const $chartCanvas = this.$(".chart-canvas");
|
||||
if (!$chartCanvas.length) return;
|
||||
const context = $chartCanvas[0].getContext("2d");
|
||||
|
||||
const reportsForPeriod = this.get("reportsForPeriod");
|
||||
|
||||
const labels = Ember.makeArray(reportsForPeriod.get("firstObject.data")).map(d => d.x);
|
||||
|
||||
const data = {
|
||||
labels,
|
||||
datasets: reportsForPeriod.map(report => {
|
||||
return {
|
||||
data: Ember.makeArray(report.data).map(d => number(d.y, { ceil: true })),
|
||||
backgroundColor: "rgba(200,220,240,0.3)",
|
||||
borderColor: report.color
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
if (this._chart) {
|
||||
this._chart.destroy();
|
||||
this._chart = null;
|
||||
}
|
||||
|
||||
loadScript("/javascripts/Chart.min.js").then(() => {
|
||||
if (this._chart) {
|
||||
this._chart.destroy();
|
||||
}
|
||||
this._chart = new window.Chart(context, this._buildChartConfig(data));
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_buildChartConfig(data) {
|
||||
return {
|
||||
type: "line",
|
||||
data,
|
||||
options: {
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
title: (context) => moment(context[0].xLabel, "YYYY-MM-DD").format("LL")
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
yAxes: [{
|
||||
display: true,
|
||||
ticks: { callback: (label) => number(label, { ceil: true }) }
|
||||
}],
|
||||
xAxes: [{
|
||||
display: true,
|
||||
gridLines: { display: false },
|
||||
type: "time",
|
||||
time: {
|
||||
parser: "YYYY-MM-DD"
|
||||
}
|
||||
}],
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
@ -0,0 +1,19 @@
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import AsyncReport from "admin/mixins/async-report";
|
||||
|
||||
export default Ember.Component.extend(AsyncReport, {
|
||||
classNames: ["dashboard-table"],
|
||||
|
||||
fetchReport() {
|
||||
this._super();
|
||||
|
||||
let payload = this.buildPayload(["total", "prev30Days"]);
|
||||
|
||||
return Ember.RSVP.Promise.all(this.get("dataSources").map(dataSource => {
|
||||
return ajax(dataSource, payload)
|
||||
.then(response => {
|
||||
this.get("reports").pushObject(this.loadReport(response.report));
|
||||
});
|
||||
}));
|
||||
}
|
||||
});
|
@ -0,0 +1,3 @@
|
||||
export default Ember.Component.extend({
|
||||
classNames: ['flag-user-lists']
|
||||
});
|
@ -0,0 +1,3 @@
|
||||
export default Ember.Component.extend({
|
||||
tagName: 'h3'
|
||||
});
|
@ -4,8 +4,6 @@ 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',
|
||||
@ -13,17 +11,15 @@ export default Ember.Component.extend({
|
||||
'flaggedPost.deleted'
|
||||
],
|
||||
|
||||
canAct: Ember.computed.alias('actableFilter'),
|
||||
|
||||
@computed('filter')
|
||||
canAct(filter) {
|
||||
actableFilter(filter) {
|
||||
return filter === 'active';
|
||||
},
|
||||
|
||||
removeAfter(promise) {
|
||||
return promise.then(() => {
|
||||
this.attrs.removePost();
|
||||
}).catch(() => {
|
||||
bootbox.alert(I18n.t("admin.flags.error"));
|
||||
});
|
||||
return promise.then(() => this.attrs.removePost());
|
||||
},
|
||||
|
||||
_spawnModal(name, model, modalClass) {
|
||||
@ -33,7 +29,7 @@ export default Ember.Component.extend({
|
||||
|
||||
actions: {
|
||||
removeAfter(promise) {
|
||||
this.removeAfter(promise);
|
||||
return this.removeAfter(promise);
|
||||
},
|
||||
|
||||
disagree() {
|
||||
@ -55,18 +51,6 @@ export default Ember.Component.extend({
|
||||
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)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -0,0 +1,32 @@
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
const ACTIONS = ['delete', 'edit', 'none'];
|
||||
export default Ember.Component.extend({
|
||||
postAction: null,
|
||||
postEdit: null,
|
||||
|
||||
@computed
|
||||
penaltyActions() {
|
||||
return ACTIONS.map(id => {
|
||||
return { id, name: I18n.t(`admin.user.penalty_post_${id}`) };
|
||||
});
|
||||
},
|
||||
|
||||
editing: Ember.computed.equal('postAction', 'edit'),
|
||||
|
||||
actions: {
|
||||
penaltyChanged() {
|
||||
let postAction = this.get('postAction');
|
||||
|
||||
// If we switch to edit mode, jump to the edit textarea
|
||||
if (postAction === 'edit') {
|
||||
Ember.run.scheduleOnce('afterRender', () => {
|
||||
let $elem = this.$();
|
||||
let body = $elem.closest('.modal-body');
|
||||
body.scrollTop(body.height());
|
||||
$elem.find('.post-editor').focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
@ -48,13 +48,17 @@ export default Ember.Component.extend({
|
||||
action_name: this.get('actionName')
|
||||
});
|
||||
screenedIpAddress.save().then(result => {
|
||||
this.setProperties({ ip_address: '', formSubmitted: false });
|
||||
this.sendAction('action', ScreenedIpAddress.create(result.screened_ip_address));
|
||||
Ember.run.schedule('afterRender', () => this.$('.ip-address-input').focus());
|
||||
if (result.success) {
|
||||
this.setProperties({ ip_address: '', formSubmitted: false });
|
||||
this.sendAction('action', ScreenedIpAddress.create(result.screened_ip_address));
|
||||
Ember.run.schedule('afterRender', () => this.$('.ip-address-input').focus());
|
||||
} else {
|
||||
bootbox.alert(result.errors);
|
||||
}
|
||||
}).catch(e => {
|
||||
this.set('formSubmitted', false);
|
||||
const msg = (e.responseJSON && e.responseJSON.errors) ?
|
||||
I18n.t("generic_error_with_reason", {error: e.responseJSON.errors.join('. ')}) :
|
||||
const msg = (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) ?
|
||||
I18n.t("generic_error_with_reason", {error: e.jqXHR.responseJSON.errors.join('. ')}) :
|
||||
I18n.t("generic_error");
|
||||
bootbox.alert(msg, () => this.$('.ip-address-input').focus());
|
||||
});
|
||||
|
@ -0,0 +1,3 @@
|
||||
export default Ember.Component.extend({
|
||||
tagName: ''
|
||||
});
|
@ -1,96 +1,10 @@
|
||||
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'];
|
||||
|
||||
export default Ember.Component.extend(BufferedContent, {
|
||||
classNameBindings: [':row', ':setting', 'setting.overridden', 'typeClass'],
|
||||
content: Ember.computed.alias('setting'),
|
||||
dirty: propertyNotEqual('buffered.value', 'setting.value'),
|
||||
validationMessage: null,
|
||||
|
||||
@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>");
|
||||
}
|
||||
},
|
||||
|
||||
@computed('componentType')
|
||||
typeClass(componentType) {
|
||||
return componentType.replace(/\_/g, '-');
|
||||
},
|
||||
|
||||
@computed("setting.setting")
|
||||
settingName(setting) {
|
||||
return setting.replace(/\_/g, ' ');
|
||||
},
|
||||
|
||||
@computed("setting.type")
|
||||
componentType(type) {
|
||||
return CustomTypes.indexOf(type) !== -1 ? type : 'string';
|
||||
},
|
||||
|
||||
@computed("typeClass")
|
||||
componentName(typeClass) {
|
||||
return "site-settings/" + typeClass;
|
||||
},
|
||||
|
||||
_watchEnterKey: function() {
|
||||
const self = this;
|
||||
this.$().on("keydown.site-setting-enter", ".input-setting-string", function (e) {
|
||||
if (e.keyCode === 13) { // enter key
|
||||
self._save();
|
||||
}
|
||||
});
|
||||
}.on('didInsertElement'),
|
||||
|
||||
_removeBindings: function() {
|
||||
this.$().off("keydown.site-setting-enter");
|
||||
}.on("willDestroyElement"),
|
||||
import SettingComponent from 'admin/mixins/setting-component';
|
||||
|
||||
export default Ember.Component.extend(BufferedContent, SettingComponent, {
|
||||
_save() {
|
||||
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) {
|
||||
this.set('validationMessage', e.jqXHR.responseJSON.errors[0]);
|
||||
} else {
|
||||
this.set('validationMessage', I18n.t('generic_error'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this._save();
|
||||
},
|
||||
|
||||
resetDefault() {
|
||||
this.set('buffered.value', this.get('setting.default'));
|
||||
this._save();
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.rollbackBuffer();
|
||||
}
|
||||
const setting = this.get('buffered');
|
||||
return SiteSetting.update(setting.get('setting'), setting.get('value'));
|
||||
}
|
||||
|
||||
});
|
||||
|
@ -6,7 +6,7 @@ export default Ember.Component.extend({
|
||||
enabled: {
|
||||
get(value) {
|
||||
if (Ember.isEmpty(value)) { return false; }
|
||||
return value === "true";
|
||||
return value.toString() === "true";
|
||||
},
|
||||
set(value) {
|
||||
this.set("value", value ? "true" : "false");
|
||||
|
22
app/assets/javascripts/admin/components/staff-actions.js.es6
Normal file
@ -0,0 +1,22 @@
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ['table', 'staff-actions'],
|
||||
|
||||
willDestroyElement() {
|
||||
this.$().off('click.discourse-staff-logs');
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super();
|
||||
|
||||
this.$().on('click.discourse-staff-logs', '[data-link-post-id]', e => {
|
||||
let postId = $(e.target).attr('data-link-post-id');
|
||||
|
||||
this.store.find('post', postId).then(p => {
|
||||
DiscourseURL.routeTo(p.get('url'));
|
||||
});
|
||||
return false;
|
||||
});
|
||||
}
|
||||
});
|
@ -0,0 +1,3 @@
|
||||
export default Ember.Component.extend({
|
||||
tagName: ''
|
||||
});
|
@ -0,0 +1,9 @@
|
||||
import BufferedContent from 'discourse/mixins/buffered-content';
|
||||
import SettingComponent from 'admin/mixins/setting-component';
|
||||
|
||||
export default Ember.Component.extend(BufferedContent, SettingComponent, {
|
||||
layoutName: 'admin/templates/components/site-setting',
|
||||
_save() {
|
||||
return this.get('model').saveSettings(this.get('setting.setting'), this.get('buffered.value'));
|
||||
}
|
||||
});
|
@ -5,7 +5,7 @@ export default Ember.Component.extend({
|
||||
classNames: ['watched-word-form'],
|
||||
formSubmitted: false,
|
||||
actionKey: null,
|
||||
showSuccessMessage: false,
|
||||
showMessage: false,
|
||||
|
||||
@computed('regularExpressions')
|
||||
placeholderKey(regularExpressions) {
|
||||
@ -14,28 +14,40 @@ export default Ember.Component.extend({
|
||||
},
|
||||
|
||||
@observes('word')
|
||||
removeSuccessMessage() {
|
||||
if (this.get('showSuccessMessage') && !Ember.isEmpty(this.get('word'))) {
|
||||
this.set('showSuccessMessage', false);
|
||||
removeMessage() {
|
||||
if (this.get('showMessage') && !Ember.isEmpty(this.get('word'))) {
|
||||
this.set('showMessage', false);
|
||||
}
|
||||
},
|
||||
|
||||
@computed('word')
|
||||
isUniqueWord(word) {
|
||||
const words = this.get("filteredContent") || [];
|
||||
const filtered = words.filter(content => content.action === this.get("actionKey"));
|
||||
return filtered.every(content => content.word.toLowerCase() !== word.toLowerCase());
|
||||
},
|
||||
|
||||
actions: {
|
||||
submit() {
|
||||
if (!this.get("isUniqueWord")) {
|
||||
this.setProperties({ showMessage: true, message: I18n.t('admin.watched_words.form.exists') });
|
||||
return;
|
||||
}
|
||||
|
||||
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.setProperties({ word: '', formSubmitted: false, showMessage: true, message: I18n.t('admin.watched_words.form.success') });
|
||||
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");
|
||||
const msg = (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) ?
|
||||
I18n.t("generic_error_with_reason", {error: e.jqXHR.responseJSON.errors.join('. ')}) :
|
||||
I18n.t("generic_error");
|
||||
bootbox.alert(msg, () => this.$('.watched-word-input').focus());
|
||||
});
|
||||
}
|
||||
|
@ -5,12 +5,25 @@ export default Ember.Controller.extend({
|
||||
maximized: false,
|
||||
section: null,
|
||||
|
||||
editRouteName: 'adminCustomizeThemes.edit',
|
||||
|
||||
targets: [
|
||||
{id: 0, name: I18n.t('admin.customize.theme.common')},
|
||||
{id: 1, name: I18n.t('admin.customize.theme.desktop')},
|
||||
{id: 2, name: I18n.t('admin.customize.theme.mobile')}
|
||||
{ id: 0, name: 'common' },
|
||||
{ id: 1, name: 'desktop' },
|
||||
{ id: 2, name: 'mobile' },
|
||||
{ id: 3, name: 'settings' }
|
||||
],
|
||||
|
||||
fieldsForTarget: function (target) {
|
||||
const common = ["scss", "head_tag", "header", "after_header", "body_tag", "footer"];
|
||||
switch(target) {
|
||||
case "common": return [...common, "embedded_scss"];
|
||||
case "desktop": return common;
|
||||
case "mobile": return common;
|
||||
case "settings": return ["yaml"];
|
||||
}
|
||||
},
|
||||
|
||||
@computed('onlyOverridden')
|
||||
showCommon() {
|
||||
return this.shouldShow('common');
|
||||
@ -26,6 +39,11 @@ export default Ember.Controller.extend({
|
||||
return this.shouldShow('mobile');
|
||||
},
|
||||
|
||||
@computed('onlyOverridden', 'model.remote_theme')
|
||||
showSettings() {
|
||||
return false;
|
||||
},
|
||||
|
||||
@observes('onlyOverridden')
|
||||
onlyOverriddenChanged() {
|
||||
if (this.get('onlyOverridden')) {
|
||||
@ -36,7 +54,7 @@ export default Ember.Controller.extend({
|
||||
|
||||
let fields = this.get('model.theme_fields');
|
||||
let field = fields && fields.find(f => (f.target === target));
|
||||
this.replaceRoute('adminCustomizeThemes.edit', this.get('model.id'), target, field && field.name);
|
||||
this.replaceRoute(this.get('editRouteName'), this.get('model.id'), target, field && field.name);
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -51,27 +69,19 @@ export default Ember.Controller.extend({
|
||||
currentTarget: 0,
|
||||
|
||||
setTargetName: function(name) {
|
||||
let target;
|
||||
switch(name) {
|
||||
case "common": target = 0; break;
|
||||
case "desktop": target = 1; break;
|
||||
case "mobile": target = 2; break;
|
||||
}
|
||||
|
||||
this.set("currentTarget", target);
|
||||
const target = this.get('targets').find(t => t.name === name);
|
||||
this.set("currentTarget", target && target.id);
|
||||
},
|
||||
|
||||
@computed("currentTarget")
|
||||
currentTargetName(target) {
|
||||
switch(parseInt(target)) {
|
||||
case 0: return "common";
|
||||
case 1: return "desktop";
|
||||
case 2: return "mobile";
|
||||
}
|
||||
currentTargetName(id) {
|
||||
const target = this.get('targets').find(t => t.id === parseInt(id, 10));
|
||||
return target && target.name;
|
||||
},
|
||||
|
||||
@computed("fieldName")
|
||||
activeSectionMode(fieldName) {
|
||||
if (fieldName === "yaml") return "yaml";
|
||||
return fieldName && fieldName.indexOf("scss") > -1 ? "scss" : "html";
|
||||
},
|
||||
|
||||
@ -96,15 +106,9 @@ export default Ember.Controller.extend({
|
||||
}
|
||||
},
|
||||
|
||||
@computed("currentTarget", "onlyOverridden")
|
||||
@computed("currentTargetName", "onlyOverridden")
|
||||
fields(target, onlyOverridden) {
|
||||
let fields = [
|
||||
"scss", "head_tag", "header", "after_header", "body_tag", "footer"
|
||||
];
|
||||
|
||||
if (parseInt(target) === 0) {
|
||||
fields.push("embedded_scss");
|
||||
}
|
||||
let fields = this.fieldsForTarget(target);
|
||||
|
||||
if (onlyOverridden) {
|
||||
const model = this.get("model");
|
||||
@ -155,5 +159,4 @@ export default Ember.Controller.extend({
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
@ -2,11 +2,14 @@ import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||
import { url } from 'discourse/lib/computed';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import showModal from 'discourse/lib/show-modal';
|
||||
import ThemeSettings from 'admin/models/theme-settings';
|
||||
|
||||
const THEME_UPLOAD_VAR = 2;
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
|
||||
editRouteName: 'adminCustomizeThemes.edit',
|
||||
|
||||
@computed("model", "allThemes")
|
||||
parentThemes(model, allThemes) {
|
||||
let parents = allThemes.filter(theme =>
|
||||
@ -30,7 +33,7 @@ export default Ember.Controller.extend({
|
||||
return text + ": " + localized.join(" , ");
|
||||
}
|
||||
};
|
||||
['common','desktop','mobile'].forEach(target=> {
|
||||
['common', 'desktop', 'mobile'].forEach(target => {
|
||||
descriptions.push(description(target));
|
||||
});
|
||||
return descriptions.reject(d=>Em.isBlank(d));
|
||||
@ -77,6 +80,16 @@ export default Ember.Controller.extend({
|
||||
return themes;
|
||||
},
|
||||
|
||||
@computed("model.settings")
|
||||
settings(settings) {
|
||||
return settings.map(setting => ThemeSettings.create(setting));
|
||||
},
|
||||
|
||||
@computed("settings")
|
||||
hasSettings(settings) {
|
||||
return settings.length > 0;
|
||||
},
|
||||
|
||||
downloadUrl: url('model.id', '/admin/themes/%@'),
|
||||
|
||||
actions: {
|
||||
@ -131,7 +144,7 @@ export default Ember.Controller.extend({
|
||||
},
|
||||
|
||||
editTheme() {
|
||||
let edit = ()=>this.transitionToRoute('adminCustomizeThemes.edit', this.get('model.id'), 'common', 'scss');
|
||||
let edit = ()=>this.transitionToRoute(this.get('editRouteName'), this.get('model.id'), 'common', 'scss');
|
||||
|
||||
if (this.get("model.remote_theme")) {
|
||||
bootbox.confirm(I18n.t("admin.customize.theme.edit_confirm"), result => {
|
||||
|
@ -0,0 +1,139 @@
|
||||
import { setting } from "discourse/lib/computed";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import AdminDashboardNext from "admin/models/admin-dashboard-next";
|
||||
import Report from "admin/models/report";
|
||||
import VersionCheck from "admin/models/version-check";
|
||||
|
||||
const PROBLEMS_CHECK_MINUTES = 1;
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
queryParams: ["period"],
|
||||
period: "monthly",
|
||||
isLoading: false,
|
||||
dashboardFetchedAt: null,
|
||||
exceptionController: Ember.inject.controller("exception"),
|
||||
showVersionChecks: setting("version_checks"),
|
||||
diskSpace: Ember.computed.alias("model.attributes.disk_space"),
|
||||
logSearchQueriesEnabled: setting("log_search_queries"),
|
||||
availablePeriods: ["yearly", "quarterly", "monthly", "weekly"],
|
||||
|
||||
@computed("problems.length")
|
||||
foundProblems(problemsLength) {
|
||||
return this.currentUser.get("admin") && (problemsLength || 0) > 0;
|
||||
},
|
||||
|
||||
@computed("foundProblems")
|
||||
thereWereProblems(foundProblems) {
|
||||
if (!this.currentUser.get("admin")) { return false; }
|
||||
|
||||
if (foundProblems) {
|
||||
this.set("hadProblems", true);
|
||||
return true;
|
||||
} else {
|
||||
return this.get("hadProblems") || false;
|
||||
}
|
||||
},
|
||||
|
||||
fetchDashboard() {
|
||||
if (this.get("isLoading")) return;
|
||||
|
||||
if (!this.get("dashboardFetchedAt") || moment().subtract(30, "minutes").toDate() > this.get("dashboardFetchedAt")) {
|
||||
this.set("isLoading", true);
|
||||
|
||||
const versionChecks = this.siteSettings.version_checks;
|
||||
|
||||
AdminDashboardNext.find().then(adminDashboardNextModel => {
|
||||
|
||||
if (versionChecks) {
|
||||
this.set("versionCheck", VersionCheck.create(adminDashboardNextModel.version_check));
|
||||
}
|
||||
|
||||
this.setProperties({
|
||||
dashboardFetchedAt: new Date(),
|
||||
model: adminDashboardNextModel,
|
||||
reports: adminDashboardNextModel.reports.map(x => Report.create(x))
|
||||
});
|
||||
}).catch(e => {
|
||||
this.get("exceptionController").set("thrown", e.jqXHR);
|
||||
this.replaceRoute("exception");
|
||||
}).finally(() => {
|
||||
this.set("isLoading", false);
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.get("problemsFetchedAt") || moment().subtract(PROBLEMS_CHECK_MINUTES, "minutes").toDate() > this.get("problemsFetchedAt")) {
|
||||
this.loadProblems();
|
||||
}
|
||||
},
|
||||
|
||||
loadProblems() {
|
||||
this.set("loadingProblems", true);
|
||||
this.set("problemsFetchedAt", new Date());
|
||||
AdminDashboardNext.fetchProblems().then(d => {
|
||||
this.set("problems", d.problems);
|
||||
}).finally(() => {
|
||||
this.set("loadingProblems", false);
|
||||
});
|
||||
},
|
||||
|
||||
@computed("problemsFetchedAt")
|
||||
problemsTimestamp(problemsFetchedAt) {
|
||||
return moment(problemsFetchedAt).locale("en").format("LLL");
|
||||
},
|
||||
|
||||
@computed("period")
|
||||
startDate(period) {
|
||||
let fullDay = moment().locale("en").utc().subtract(1, "day");
|
||||
|
||||
switch (period) {
|
||||
case "yearly":
|
||||
return fullDay.subtract(1, "year").startOf("day");
|
||||
break;
|
||||
case "quarterly":
|
||||
return fullDay.subtract(3, "month").startOf("day");
|
||||
break;
|
||||
case "weekly":
|
||||
return fullDay.subtract(1, "week").startOf("day");
|
||||
break;
|
||||
case "monthly":
|
||||
return fullDay.subtract(1, "month").startOf("day");
|
||||
break;
|
||||
default:
|
||||
return fullDay.subtract(1, "month").startOf("day");
|
||||
}
|
||||
},
|
||||
|
||||
@computed()
|
||||
lastWeek() {
|
||||
return moment().locale("en").utc().endOf("day").subtract(1, "week");
|
||||
},
|
||||
|
||||
@computed()
|
||||
endDate() {
|
||||
return moment().locale("en").utc().subtract(1, "day").endOf("day");
|
||||
},
|
||||
|
||||
@computed("updated_at")
|
||||
updatedTimestamp(updatedAt) {
|
||||
return moment(updatedAt).format("LLL");
|
||||
},
|
||||
|
||||
@computed("last_backup_taken_at")
|
||||
backupTimestamp(lastBackupTakenAt) {
|
||||
return moment(lastBackupTakenAt).format("LLL");
|
||||
},
|
||||
|
||||
actions: {
|
||||
changePeriod(period) {
|
||||
DiscourseURL.routeTo(this._reportsForPeriodURL(period));
|
||||
},
|
||||
refreshProblems() {
|
||||
this.loadProblems();
|
||||
},
|
||||
},
|
||||
|
||||
_reportsForPeriodURL(period) {
|
||||
return Discourse.getURL(`/admin?period=${period}`);
|
||||
}
|
||||
});
|
@ -1,11 +1,8 @@
|
||||
import { setting } from 'discourse/lib/computed';
|
||||
import AdminDashboard from 'admin/models/admin-dashboard';
|
||||
import VersionCheck from 'admin/models/version-check';
|
||||
import Report from 'admin/models/report';
|
||||
import AdminUser from 'admin/models/admin-user';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
const PROBLEMS_CHECK_MINUTES = 1;
|
||||
|
||||
const ATTRIBUTES = [ 'disk_space','admins', 'moderators', 'silenced', 'suspended', 'top_traffic_sources',
|
||||
'top_referred_topics', 'updated_at'];
|
||||
@ -18,34 +15,13 @@ export default Ember.Controller.extend({
|
||||
loading: null,
|
||||
versionCheck: null,
|
||||
dashboardFetchedAt: null,
|
||||
showVersionChecks: setting('version_checks'),
|
||||
|
||||
@computed('problems.length')
|
||||
foundProblems(problemsLength) {
|
||||
return this.currentUser.get('admin') && (problemsLength || 0) > 0;
|
||||
},
|
||||
|
||||
@computed('foundProblems')
|
||||
thereWereProblems(foundProblems) {
|
||||
if (!this.currentUser.get('admin')) { return false; }
|
||||
|
||||
if (foundProblems) {
|
||||
this.set('hadProblems', true);
|
||||
return true;
|
||||
} else {
|
||||
return this.get('hadProblems') || false;
|
||||
}
|
||||
},
|
||||
exceptionController: Ember.inject.controller('exception'),
|
||||
|
||||
fetchDashboard() {
|
||||
if (!this.get('dashboardFetchedAt') || moment().subtract(30, 'minutes').toDate() > this.get('dashboardFetchedAt')) {
|
||||
this.set('dashboardFetchedAt', new Date());
|
||||
this.set('loading', true);
|
||||
const versionChecks = this.siteSettings.version_checks;
|
||||
AdminDashboard.find().then(d => {
|
||||
if (versionChecks) {
|
||||
this.set('versionCheck', VersionCheck.create(d.version_check));
|
||||
}
|
||||
this.set('dashboardFetchedAt', new Date());
|
||||
|
||||
REPORTS.forEach(name => this.set(name, d[name].map(r => Report.create(r))));
|
||||
|
||||
@ -56,29 +32,15 @@ export default Ember.Controller.extend({
|
||||
}
|
||||
|
||||
ATTRIBUTES.forEach(a => this.set(a, d[a]));
|
||||
}).catch(e => {
|
||||
this.get('exceptionController').set('thrown', e.jqXHR);
|
||||
this.replaceRoute('exception');
|
||||
}).finally(() => {
|
||||
this.set('loading', false);
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.get('problemsFetchedAt') || moment().subtract(PROBLEMS_CHECK_MINUTES, 'minutes').toDate() > this.get('problemsFetchedAt')) {
|
||||
this.loadProblems();
|
||||
}
|
||||
},
|
||||
|
||||
loadProblems() {
|
||||
this.set('loadingProblems', true);
|
||||
this.set('problemsFetchedAt', new Date());
|
||||
AdminDashboard.fetchProblems().then(d => {
|
||||
this.set('problems', d.problems);
|
||||
}).finally(() => {
|
||||
this.set('loadingProblems', false);
|
||||
});
|
||||
},
|
||||
|
||||
@computed('problemsFetchedAt')
|
||||
problemsTimestamp(problemsFetchedAt) {
|
||||
return moment(problemsFetchedAt).format('LLL');
|
||||
},
|
||||
|
||||
@computed('updated_at')
|
||||
updatedTimestamp(updatedAt) {
|
||||
@ -86,9 +48,6 @@ export default Ember.Controller.extend({
|
||||
},
|
||||
|
||||
actions: {
|
||||
refreshProblems() {
|
||||
this.loadProblems();
|
||||
},
|
||||
showTrafficReport() {
|
||||
this.set("showTrafficReport", true);
|
||||
}
|
||||
|
@ -1,110 +0,0 @@
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
adminGroupsType: Ember.inject.controller(),
|
||||
disableSave: false,
|
||||
savingStatus: '',
|
||||
|
||||
aliasLevelOptions: function() {
|
||||
return [
|
||||
{ name: I18n.t("groups.alias_levels.nobody"), value: 0 },
|
||||
{ name: I18n.t("groups.alias_levels.mods_and_admins"), value: 2 },
|
||||
{ name: I18n.t("groups.alias_levels.members_mods_and_admins"), value: 3 },
|
||||
{ name: I18n.t("groups.alias_levels.everyone"), value: 99 }
|
||||
];
|
||||
}.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 },
|
||||
{ name: 1, value: 1 }, { name: 2, value: 2 }, { name: 3, value: 3 }, { name: 4, value: 4 }
|
||||
];
|
||||
}.property(),
|
||||
|
||||
@computed('model.visibility_level', 'model.public_admission')
|
||||
disableMembershipRequestSetting(visibility_level, publicAdmission) {
|
||||
visibility_level = parseInt(visibility_level);
|
||||
return (visibility_level !== 0) || publicAdmission;
|
||||
},
|
||||
|
||||
@computed('model.visibility_level', 'model.allow_membership_requests')
|
||||
disablePublicSetting(visibility_level, allowMembershipRequests) {
|
||||
visibility_level = parseInt(visibility_level);
|
||||
return (visibility_level !== 0) || allowMembershipRequests;
|
||||
},
|
||||
|
||||
actions: {
|
||||
removeOwner(member) {
|
||||
const self = this,
|
||||
message = I18n.t("admin.groups.delete_owner_confirm", { username: member.get("username"), group: this.get("model.name") });
|
||||
return bootbox.confirm(message, I18n.t("no_value"), I18n.t("yes_value"), function(confirm) {
|
||||
if (confirm) {
|
||||
self.get("model").removeOwner(member);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
addOwners() {
|
||||
if (Em.isEmpty(this.get("model.ownerUsernames"))) { return; }
|
||||
this.get("model").addOwners(this.get("model.ownerUsernames")).catch(popupAjaxError);
|
||||
this.set("model.ownerUsernames", null);
|
||||
},
|
||||
|
||||
save() {
|
||||
const group = this.get('model'),
|
||||
groupsController = this.get("adminGroupsType"),
|
||||
groupType = groupsController.get("type");
|
||||
|
||||
this.set('disableSave', true);
|
||||
this.set('savingStatus', I18n.t('saving'));
|
||||
|
||||
let promise = group.get("id") ? group.save() : group.create().then(() => groupsController.get('model').addObject(group));
|
||||
|
||||
promise.then(() => {
|
||||
this.transitionToRoute("adminGroup", groupType, group.get('name'));
|
||||
this.set('savingStatus', I18n.t('saved'));
|
||||
}).catch(popupAjaxError)
|
||||
.finally(() => this.set('disableSave', false));
|
||||
},
|
||||
|
||||
destroy() {
|
||||
const group = this.get('model'),
|
||||
groupsController = this.get('adminGroupsType'),
|
||||
self = this;
|
||||
|
||||
if (!group.get('id')) {
|
||||
self.transitionToRoute('adminGroupsType.index', 'custom');
|
||||
return;
|
||||
}
|
||||
|
||||
this.set('disableSave', true);
|
||||
|
||||
bootbox.confirm(
|
||||
I18n.t("admin.groups.delete_confirm"),
|
||||
I18n.t("no_value"),
|
||||
I18n.t("yes_value"),
|
||||
function(confirmed) {
|
||||
if (confirmed) {
|
||||
group.destroy().then(() => {
|
||||
groupsController.get('model').removeObject(group);
|
||||
self.transitionToRoute('adminGroups.index');
|
||||
}).catch(() => bootbox.alert(I18n.t("admin.groups.delete_failed")))
|
||||
.finally(() => self.set('disableSave', false));
|
||||
} else {
|
||||
self.set('disableSave', false);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
@ -1,4 +0,0 @@
|
||||
export default Ember.Controller.extend({
|
||||
adminGroupsBulk: Ember.inject.controller(),
|
||||
bulkAddResponse: Ember.computed.alias('adminGroupsBulk.bulkAddResponse')
|
||||
});
|
@ -1,37 +0,0 @@
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
users: null,
|
||||
groupId: null,
|
||||
saving: false,
|
||||
bulkAddResponse: null,
|
||||
|
||||
@computed('saving', 'users', 'groupId')
|
||||
buttonDisabled(saving, users, groupId) {
|
||||
return saving || !groupId || !users || !users.length;
|
||||
},
|
||||
|
||||
actions: {
|
||||
addToGroup() {
|
||||
if (this.get('saving')) { return; }
|
||||
|
||||
const users = this.get('users').split("\n")
|
||||
.uniq()
|
||||
.reject(x => x.length === 0);
|
||||
|
||||
this.set('saving', true);
|
||||
ajax('/admin/groups/bulk', {
|
||||
data: { users, group_id: this.get('groupId') },
|
||||
method: 'PUT'
|
||||
}).then(result => {
|
||||
this.set('bulkAddResponse', result);
|
||||
this.transitionToRoute('adminGroups.bulkComplete');
|
||||
}).catch(popupAjaxError).finally(() => {
|
||||
this.set('saving', false);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
});
|
@ -1,11 +0,0 @@
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
adminGroupsType: Ember.inject.controller(),
|
||||
sortedGroups: Ember.computed.alias("adminGroupsType.sortedGroups"),
|
||||
|
||||
@computed("sortedGroups")
|
||||
messageKey(sortedGroups) {
|
||||
return `admin.groups.${sortedGroups.length > 0 ? 'none_selected' : 'no_custom_groups'}`;
|
||||
}
|
||||
});
|
@ -1,20 +0,0 @@
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
export default Ember.Controller.extend({
|
||||
sortedGroups: Ember.computed.sort('model', 'groupSorting'),
|
||||
groupSorting: ['name'],
|
||||
|
||||
refreshingAutoGroups: false,
|
||||
|
||||
isAuto: Ember.computed.equal('type', 'automatic'),
|
||||
|
||||
actions: {
|
||||
refreshAutoGroups() {
|
||||
this.set('refreshingAutoGroups', true);
|
||||
ajax('/admin/groups/refresh_automatic_groups', {type: 'POST'}).then(() => {
|
||||
this.transitionToRoute("adminGroupsType", "automatic").then(() => {
|
||||
this.set('refreshingAutoGroups', false);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
@ -4,6 +4,7 @@ import Report from 'admin/models/report';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
classNames: ["admin-reports"],
|
||||
queryParams: ["mode", "start_date", "end_date", "category_id", "group_id"],
|
||||
viewMode: 'graph',
|
||||
viewingTable: Em.computed.equal('viewMode', 'table'),
|
||||
|
@ -1,7 +1,7 @@
|
||||
export default Ember.Controller.extend({
|
||||
loading: false,
|
||||
term: null,
|
||||
period: "yearly",
|
||||
period: "quarterly",
|
||||
searchType: "all",
|
||||
|
||||
searchTypeOptions: [
|
||||
|
@ -1,16 +1,16 @@
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
categoryNameKey: null,
|
||||
adminSiteSettings: Ember.inject.controller(),
|
||||
|
||||
filteredContent: function() {
|
||||
if (!this.get('categoryNameKey')) { return []; }
|
||||
|
||||
const category = (this.get('adminSiteSettings.model') || []).findBy('nameKey', this.get('categoryNameKey'));
|
||||
if (category) {
|
||||
return category.siteSettings;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}.property('adminSiteSettings.model', 'categoryNameKey')
|
||||
@computed("adminSiteSettings.model", "categoryNameKey")
|
||||
category(categories, nameKey) {
|
||||
return (categories || []).findBy("nameKey", nameKey);
|
||||
},
|
||||
|
||||
@computed("category")
|
||||
filteredContent(category) {
|
||||
return category ? category.siteSettings : [];
|
||||
}
|
||||
});
|
||||
|
@ -3,7 +3,6 @@ import debounce from 'discourse/lib/debounce';
|
||||
export default Ember.Controller.extend({
|
||||
filter: null,
|
||||
onlyOverridden: false,
|
||||
filtered: Ember.computed.notEmpty('filter'),
|
||||
|
||||
filterContentNow(category) {
|
||||
// If we have no content, don't bother filtering anything
|
||||
@ -14,9 +13,9 @@ export default Ember.Controller.extend({
|
||||
filter = this.get('filter').toLowerCase();
|
||||
}
|
||||
|
||||
if ((filter === undefined || filter.length < 1) && !this.get('onlyOverridden')) {
|
||||
if ((!filter || 0 === filter.length) && !this.get('onlyOverridden')) {
|
||||
this.set('model', this.get('allSiteSettings'));
|
||||
this.transitionToRoute("adminSiteSettings");
|
||||
this.transitionToRoute('adminSiteSettings');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -28,11 +27,11 @@ export default Ember.Controller.extend({
|
||||
const siteSettings = settingsCategory.siteSettings.filter(item => {
|
||||
if (this.get('onlyOverridden') && !item.get('overridden')) return false;
|
||||
if (filter) {
|
||||
if (item.get('setting').toLowerCase().indexOf(filter) > -1) return true;
|
||||
if (item.get('setting').toLowerCase().replace(/_/g, ' ').indexOf(filter) > -1) return true;
|
||||
if (item.get('description').toLowerCase().indexOf(filter) > -1) return true;
|
||||
if (item.get('value').toLowerCase().indexOf(filter) > -1) return true;
|
||||
return false;
|
||||
const setting = item.get('setting').toLowerCase();
|
||||
return setting.includes(filter) ||
|
||||
setting.replace(/_/g, ' ').includes(filter) ||
|
||||
item.get('description').toLowerCase().includes(filter) ||
|
||||
(item.get('value') || '').toLowerCase().includes(filter);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
@ -49,15 +48,16 @@ export default Ember.Controller.extend({
|
||||
});
|
||||
|
||||
all.siteSettings.pushObjects(matches.slice(0, 30));
|
||||
all.count = matches.length;
|
||||
all.hasMore = matches.length > 30;
|
||||
all.count = all.hasMore ? '30+' : matches.length;
|
||||
|
||||
this.set('model', matchesGroupedByCategory);
|
||||
this.transitionToRoute("adminSiteSettingsCategory", category || "all_results");
|
||||
this.transitionToRoute('adminSiteSettingsCategory', category || 'all_results');
|
||||
},
|
||||
|
||||
filterContent: debounce(function() {
|
||||
if (this.get("_skipBounce")) {
|
||||
this.set("_skipBounce", false);
|
||||
if (this.get('_skipBounce')) {
|
||||
this.set('_skipBounce', false);
|
||||
} else {
|
||||
this.filterContentNow();
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
import UserBadge from 'discourse/models/user-badge';
|
||||
import GrantBadgeController from "discourse/mixins/grant-badge-controller";
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
export default Ember.Controller.extend(GrantBadgeController, {
|
||||
adminUser: Ember.inject.controller(),
|
||||
user: Ember.computed.alias('adminUser.model'),
|
||||
userBadges: Ember.computed.alias('model'),
|
||||
allBadges: Ember.computed.alias('badges'),
|
||||
|
||||
sortedBadges: Ember.computed.sort('model', 'badgeSortOrder'),
|
||||
badgeSortOrder: ['granted_at:desc'],
|
||||
@ -41,36 +43,6 @@ export default Ember.Controller.extend({
|
||||
return _(expanded).sortBy(group => group.granted_at).reverse().value();
|
||||
}.property('model', 'model.[]', 'model.expandedBadges.[]'),
|
||||
|
||||
/**
|
||||
Array of badges that have not been granted to this user.
|
||||
|
||||
@property grantableBadges
|
||||
@type {Boolean}
|
||||
**/
|
||||
grantableBadges: function() {
|
||||
var granted = {};
|
||||
this.get('model').forEach(function(userBadge) {
|
||||
granted[userBadge.get('badge_id')] = true;
|
||||
});
|
||||
|
||||
var badges = [];
|
||||
this.get('badges').forEach(function(badge) {
|
||||
if (badge.get('enabled') && (badge.get('multiple_grant') || !granted[badge.get('id')])) {
|
||||
badges.push(badge);
|
||||
}
|
||||
});
|
||||
|
||||
return _.sortBy(badges, badge => badge.get('name'));
|
||||
}.property('badges.[]', 'model.[]'),
|
||||
|
||||
/**
|
||||
Whether there are any badges that can be granted.
|
||||
|
||||
@property noBadges
|
||||
@type {Boolean}
|
||||
**/
|
||||
noBadges: Em.computed.empty('grantableBadges'),
|
||||
|
||||
actions: {
|
||||
|
||||
expandGroup: function(userBadge){
|
||||
@ -79,21 +51,21 @@ export default Ember.Controller.extend({
|
||||
model.get('expandedBadges').pushObject(userBadge.badge.id);
|
||||
},
|
||||
|
||||
grantBadge(badgeId) {
|
||||
UserBadge.grant(badgeId, this.get('user.username'), this.get('badgeReason')).then(userBadge => {
|
||||
this.set('badgeReason', '');
|
||||
this.get('model').pushObject(userBadge);
|
||||
Ember.run.next(() => {
|
||||
// Update the selected badge ID after the combobox has re-rendered.
|
||||
const newSelectedBadge = this.get('grantableBadges')[0];
|
||||
if (newSelectedBadge) {
|
||||
this.set('selectedBadgeId', newSelectedBadge.get('id'));
|
||||
}
|
||||
grantBadge() {
|
||||
this.grantBadge(this.get('selectedBadgeId'), this.get('user.username'), this.get('badgeReason'))
|
||||
.then(() => {
|
||||
this.set('badgeReason', '');
|
||||
Ember.run.next(() => {
|
||||
// Update the selected badge ID after the combobox has re-rendered.
|
||||
const newSelectedBadge = this.get('grantableBadges')[0];
|
||||
if (newSelectedBadge) {
|
||||
this.set('selectedBadgeId', newSelectedBadge.get('id'));
|
||||
}
|
||||
});
|
||||
}, function() {
|
||||
// Failure
|
||||
bootbox.alert(I18n.t('generic_error'));
|
||||
});
|
||||
}, function() {
|
||||
// Failure
|
||||
bootbox.alert(I18n.t('generic_error'));
|
||||
});
|
||||
},
|
||||
|
||||
revokeBadge(userBadge) {
|
||||
|
@ -19,9 +19,18 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
|
||||
primaryGroupDirty: propertyNotEqual('originalPrimaryGroupId', 'model.primary_group_id'),
|
||||
|
||||
automaticGroups: function() {
|
||||
return this.get("model.automaticGroups").map((g) => g.name).join(", ");
|
||||
}.property("model.automaticGroups"),
|
||||
canDisableSecondFactor: Ember.computed.and(
|
||||
'model.second_factor_enabled',
|
||||
'model.can_disable_second_factor'
|
||||
),
|
||||
|
||||
@computed("model.automaticGroups")
|
||||
automaticGroups(automaticGroups) {
|
||||
return automaticGroups.map(group => {
|
||||
const name = Ember.String.htmlSafe(group.name);
|
||||
return `<a href="/groups/${name}">${name}</a>`;
|
||||
}).join(", ");
|
||||
},
|
||||
|
||||
userFields: function() {
|
||||
const siteUserFields = this.site.get('user_fields'),
|
||||
@ -41,6 +50,36 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
return userPath(`${username}/preferences`);
|
||||
},
|
||||
|
||||
@computed('model.can_delete_all_posts', 'model.staff', 'model.post_count')
|
||||
deleteAllPostsExplanation(canDeleteAllPosts, staff, postCount) {
|
||||
if (canDeleteAllPosts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (staff) {
|
||||
return I18n.t('admin.user.delete_posts_forbidden_because_staff');
|
||||
}
|
||||
if (postCount > this.siteSettings.delete_all_posts_max) {
|
||||
return I18n.t('admin.user.cant_delete_all_too_many_posts', {count: this.siteSettings.delete_all_posts_max});
|
||||
} else {
|
||||
return I18n.t('admin.user.cant_delete_all_posts', {count: this.siteSettings.delete_user_max_post_age});
|
||||
}
|
||||
},
|
||||
|
||||
@computed('model.canBeDeleted', 'model.staff')
|
||||
deleteExplanation(canBeDeleted, staff) {
|
||||
if (canBeDeleted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (staff) {
|
||||
return I18n.t('admin.user.delete_forbidden_because_staff');
|
||||
} else {
|
||||
return I18n.t('admin.user.delete_forbidden', {count: this.siteSettings.delete_user_max_post_age});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
actions: {
|
||||
|
||||
impersonate() { return this.get("model").impersonate(); },
|
||||
@ -62,8 +101,36 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
silence() { return this.get("model").silence(); },
|
||||
deleteAllPosts() { return this.get("model").deleteAllPosts(); },
|
||||
anonymize() { return this.get('model').anonymize(); },
|
||||
destroy() { return this.get('model').destroy(); },
|
||||
disableSecondFactor() { return this.get('model').disableSecondFactor(); },
|
||||
|
||||
|
||||
clearPenaltyHistory() {
|
||||
let user = this.get('model');
|
||||
return ajax(`/admin/users/${user.get('id')}/penalty_history`, {
|
||||
type: 'DELETE'
|
||||
}).then(() => {
|
||||
user.set('tl3_requirements.penalty_counts.total', 0);
|
||||
}).catch(popupAjaxError);
|
||||
},
|
||||
|
||||
destroy() {
|
||||
const postCount = this.get('model.post_count');
|
||||
if (postCount <= 5) {
|
||||
return this.get('model').destroy({ deletePosts: true });
|
||||
} else {
|
||||
return this.get('model').destroy();
|
||||
}
|
||||
},
|
||||
|
||||
viewActionLogs() {
|
||||
this.get('adminTools').showActionLogs(this, {
|
||||
target_user: this.get('model.username'),
|
||||
});
|
||||
},
|
||||
|
||||
showFlagsReceived() {
|
||||
this.get('adminTools').showFlagsReceived(this.get('model'));
|
||||
},
|
||||
showSuspendModal() {
|
||||
this.get('adminTools').showSuspendModal(this.get('model'));
|
||||
},
|
||||
|
@ -10,11 +10,11 @@ export default Ember.Controller.extend({
|
||||
return (this.get('adminWatchedWords.model') || []).findBy('nameKey', actionName);
|
||||
},
|
||||
|
||||
@computed('adminWatchedWords.model', 'actionNameKey')
|
||||
filteredContent() {
|
||||
if (!this.get('actionNameKey')) { return []; }
|
||||
@computed('actionNameKey', 'adminWatchedWords.model')
|
||||
filteredContent(actionNameKey) {
|
||||
if (!actionNameKey) { return []; }
|
||||
|
||||
const a = this.findAction(this.get('actionNameKey'));
|
||||
const a = this.findAction(actionNameKey);
|
||||
return a ? a.words : [];
|
||||
},
|
||||
|
||||
@ -23,6 +23,12 @@ export default Ember.Controller.extend({
|
||||
return I18n.t('admin.watched_words.action_descriptions.' + actionNameKey);
|
||||
},
|
||||
|
||||
@computed('actionNameKey', 'adminWatchedWords.model')
|
||||
wordCount(actionNameKey) {
|
||||
const a = this.findAction(actionNameKey);
|
||||
return a ? a.words.length : 0;
|
||||
},
|
||||
|
||||
actions: {
|
||||
recordAdded(arg) {
|
||||
const a = this.findAction(this.get('actionNameKey'));
|
||||
|
@ -46,6 +46,10 @@ export default Ember.Controller.extend({
|
||||
actions: {
|
||||
clearFilter() {
|
||||
this.setProperties({ filter: '' });
|
||||
},
|
||||
|
||||
toggleMenu() {
|
||||
$('.admin-detail').toggleClass('mobile-closed mobile-open');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,9 +3,31 @@ import { ajax } from 'discourse/lib/ajax';
|
||||
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
|
||||
const THEME_FIELD_VARIABLE_TYPE_IDS = [2, 3, 4];
|
||||
|
||||
const SCSS_VARIABLE_NAMES = [
|
||||
// common/foundation/colors.scss
|
||||
"primary", "secondary", "tertiary", "quaternary", "header_background",
|
||||
"header_primary", "highlight", "danger", "success", "love",
|
||||
// common/foundation/math.scss
|
||||
"E", "PI", "LN2", "SQRT2",
|
||||
// common/foundation/variables.scss
|
||||
"small-width", "medium-width", "large-width",
|
||||
"google", "instagram", "facebook", "cas", "twitter", "yahoo", "github",
|
||||
"base-font-size", "base-line-height", "base-font-family",
|
||||
"primary-low", "primary-medium",
|
||||
"secondary-low", "secondary-medium",
|
||||
"tertiary-low", "quaternary-low",
|
||||
"highlight-low", "highlight-medium",
|
||||
"danger-low", "danger-medium",
|
||||
"success-low", "love-low",
|
||||
];
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
adminCustomizeThemesShow: Ember.inject.controller(),
|
||||
|
||||
uploadUrl: '/admin/themes/upload_asset',
|
||||
|
||||
onShow() {
|
||||
this.set('name', null);
|
||||
this.set('fileSelected', false);
|
||||
@ -14,9 +36,24 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
enabled: Em.computed.and('nameValid', 'fileSelected'),
|
||||
disabled: Em.computed.not('enabled'),
|
||||
|
||||
@computed('name')
|
||||
nameValid(name) {
|
||||
return name && name.match(/^[a-z_][a-z0-9_-]*$/i);
|
||||
@computed('name', 'adminCustomizeThemesShow.model.theme_fields')
|
||||
errorMessage(name, themeFields) {
|
||||
if (name) {
|
||||
if (!name.match(/^[a-z_][a-z0-9_-]*$/i)) {
|
||||
return I18n.t("admin.customize.theme.variable_name_error.invalid_syntax");
|
||||
} else if (SCSS_VARIABLE_NAMES.includes(name.toLowerCase())) {
|
||||
return I18n.t("admin.customize.theme.variable_name_error.no_overwrite");
|
||||
} else if (themeFields.some(tf => THEME_FIELD_VARIABLE_TYPE_IDS.includes(tf.type_id) && name === tf.name)) {
|
||||
return I18n.t("admin.customize.theme.variable_name_error.must_be_unique");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
@computed('errorMessage')
|
||||
nameValid(errorMessage) {
|
||||
return null === errorMessage;
|
||||
},
|
||||
|
||||
@observes('name')
|
||||
@ -48,7 +85,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
|
||||
options.data.append('file', file);
|
||||
|
||||
ajax('/admin/themes/upload_asset', options).then(result => {
|
||||
ajax(this.get('uploadUrl'), options).then(result => {
|
||||
const upload = {
|
||||
upload_id: result.upload_id,
|
||||
name: this.get('name'),
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
export default Ember.Controller.extend({
|
||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
|
||||
modelChanged: function(){
|
||||
const model = this.get('model');
|
||||
|
@ -0,0 +1,17 @@
|
||||
export default Ember.Controller.extend({
|
||||
loadingFlags: null,
|
||||
user: null,
|
||||
|
||||
onShow() {
|
||||
this.set('loadingFlags', true);
|
||||
this.store.findAll('flagged-post', {
|
||||
filter: 'without_custom',
|
||||
user_id: this.get('model.id')
|
||||
}).then(result => {
|
||||
this.set('loadingFlags', false);
|
||||
console.log(result);
|
||||
this.set('flaggedPosts', result);
|
||||
});
|
||||
}
|
||||
|
||||
});
|
@ -1,12 +1,35 @@
|
||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
// import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import { observes } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
local: Ember.computed.equal('selection', 'local'),
|
||||
remote: Ember.computed.equal('selection', 'remote'),
|
||||
selection: 'local',
|
||||
adminCustomizeThemes: Ember.inject.controller(),
|
||||
loading: false,
|
||||
keyGenUrl: '/admin/themes/generate_key_pair',
|
||||
importUrl: '/admin/themes/import',
|
||||
|
||||
checkPrivate: Ember.computed.match('uploadUrl', /^git/),
|
||||
|
||||
@observes('privateChecked')
|
||||
privateWasChecked() {
|
||||
const checked = this.get('privateChecked');
|
||||
if (checked && !this._keyLoading) {
|
||||
this._keyLoading = true;
|
||||
ajax(this.get('keyGenUrl'), {method: 'POST'})
|
||||
.then(pair => {
|
||||
this.set('privateKey', pair.private_key);
|
||||
this.set('publicKey', pair.public_key);
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
.finally(()=>{
|
||||
this._keyLoading = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
importTheme() {
|
||||
@ -21,14 +44,27 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
options.data = new FormData();
|
||||
options.data.append('theme', $('#file-input')[0].files[0]);
|
||||
} else {
|
||||
options.data = {remote: this.get('uploadUrl')};
|
||||
options.data = {
|
||||
remote: this.get('uploadUrl')
|
||||
};
|
||||
|
||||
if (this.get('privateChecked')){
|
||||
options.data.private_key = this.get('privateKey');
|
||||
}
|
||||
}
|
||||
|
||||
ajax('/admin/themes/import', options).then(result=>{
|
||||
this.set('loading', true);
|
||||
ajax(this.get('importUrl'), options).then(result=>{
|
||||
const theme = this.store.createRecord('theme',result.theme);
|
||||
this.get('adminCustomizeThemes').send('addTheme', theme);
|
||||
this.send('closeModal');
|
||||
});
|
||||
})
|
||||
.then(()=>{
|
||||
this.set('privateKey', null);
|
||||
this.set('publicKey', null);
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
.finally(() => this.set('loading', false));
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,26 +1,13 @@
|
||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import PenaltyController from 'admin/mixins/penalty-controller';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
export default Ember.Controller.extend(PenaltyController, {
|
||||
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,
|
||||
});
|
||||
this.resetModal();
|
||||
this.setProperties({ silenceUntil: null, silencing: false });
|
||||
},
|
||||
|
||||
@computed('silenceUntil', 'reason', 'silencing')
|
||||
@ -33,18 +20,16 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
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));
|
||||
this.penalize(() => {
|
||||
return this.get('user').silence({
|
||||
silenced_till: this.get('silenceUntil'),
|
||||
reason: this.get('reason'),
|
||||
message: this.get('message'),
|
||||
post_id: this.get('post.id'),
|
||||
post_action: this.get('postAction'),
|
||||
post_edit: this.get('postEdit')
|
||||
});
|
||||
}).finally(() => this.set('silencing', false));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1,26 +1,13 @@
|
||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import PenaltyController from 'admin/mixins/penalty-controller';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
export default Ember.Controller.extend(PenaltyController, {
|
||||
suspendUntil: null,
|
||||
reason: null,
|
||||
message: null,
|
||||
suspending: false,
|
||||
user: null,
|
||||
post: null,
|
||||
successCallback: null,
|
||||
|
||||
onShow() {
|
||||
this.setProperties({
|
||||
suspendUntil: null,
|
||||
reason: null,
|
||||
message: null,
|
||||
suspending: false,
|
||||
loadingUser: true,
|
||||
post: null,
|
||||
successCallback: null,
|
||||
});
|
||||
this.resetModal();
|
||||
this.setProperties({ suspendUntil: null, suspending: false });
|
||||
},
|
||||
|
||||
@computed('suspendUntil', 'reason', 'suspending')
|
||||
@ -33,19 +20,17 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
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));
|
||||
|
||||
this.penalize(() => {
|
||||
return this.get('user').suspend({
|
||||
suspend_until: this.get('suspendUntil'),
|
||||
reason: this.get('reason'),
|
||||
message: this.get('message'),
|
||||
post_id: this.get('post.id'),
|
||||
post_action: this.get('postAction'),
|
||||
post_edit: this.get('postEdit')
|
||||
});
|
||||
}).finally(() => this.set('suspending', false));
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
99
app/assets/javascripts/admin/mixins/async-report.js.es6
Normal file
@ -0,0 +1,99 @@
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import Report from "admin/models/report";
|
||||
|
||||
export default Ember.Mixin.create({
|
||||
classNameBindings: ["isLoading", "dataSourceNames"],
|
||||
reports: null,
|
||||
isLoading: false,
|
||||
dataSourceNames: "",
|
||||
title: null,
|
||||
|
||||
init() {
|
||||
this._super();
|
||||
this.set("reports", []);
|
||||
},
|
||||
|
||||
@computed("dataSourceNames")
|
||||
dataSources(dataSourceNames) {
|
||||
return dataSourceNames.split(",").map(source => `/admin/reports/${source}`);
|
||||
},
|
||||
|
||||
buildPayload(facets) {
|
||||
let payload = { data: { cache: true, facets } };
|
||||
|
||||
if (this.get("startDate")) {
|
||||
payload.data.start_date = this.get("startDate").format("YYYY-MM-DD[T]HH:mm:ss.SSSZZ");
|
||||
}
|
||||
|
||||
if (this.get("endDate")) {
|
||||
payload.data.end_date = this.get("endDate").format("YYYY-MM-DD[T]HH:mm:ss.SSSZZ");
|
||||
}
|
||||
|
||||
if (this.get("limit")) {
|
||||
payload.data.limit = this.get("limit");
|
||||
}
|
||||
|
||||
return payload;
|
||||
},
|
||||
|
||||
@computed("reports.[]", "startDate", "endDate", "dataSourceNames")
|
||||
reportsForPeriod(reports, startDate, endDate, dataSourceNames) {
|
||||
// on a slow network fetchReport could be called multiple times between
|
||||
// T and T+x, and all the ajax responses would occur after T+(x+y)
|
||||
// to avoid any inconsistencies we filter by period and make sure
|
||||
// the array contains only unique values
|
||||
reports = reports.uniqBy("report_key");
|
||||
|
||||
const sort = (r) => {
|
||||
if (r.length > 1) {
|
||||
return dataSourceNames
|
||||
.split(",")
|
||||
.map(name => r.findBy("type", name));
|
||||
} else {
|
||||
return r;
|
||||
}
|
||||
};
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
return sort(reports);
|
||||
}
|
||||
|
||||
return sort(reports.filter(report => {
|
||||
return report.report_key.includes(startDate.format("YYYYMMDD")) &&
|
||||
report.report_key.includes(endDate.format("YYYYMMDD"));
|
||||
}));
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super();
|
||||
|
||||
this.fetchReport()
|
||||
.finally(() => {
|
||||
this.renderReport();
|
||||
});
|
||||
},
|
||||
|
||||
didUpdateAttrs() {
|
||||
this._super();
|
||||
|
||||
this.fetchReport()
|
||||
.finally(() => {
|
||||
this.renderReport();
|
||||
});
|
||||
},
|
||||
|
||||
renderReport() {
|
||||
if (!this.element || this.isDestroying || this.isDestroyed) return;
|
||||
this.set("title", this.get("reportsForPeriod").map(r => r.title).join(", "));
|
||||
this.set("isLoading", false);
|
||||
},
|
||||
|
||||
loadReport(jsonReport) {
|
||||
return Report.create(jsonReport);
|
||||
},
|
||||
|
||||
fetchReport() {
|
||||
this.set("reports", []);
|
||||
this.set("isLoading", true);
|
||||
},
|
||||
});
|
@ -0,0 +1,41 @@
|
||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
|
||||
export default Ember.Mixin.create(ModalFunctionality, {
|
||||
reason: null,
|
||||
message: null,
|
||||
postEdit: null,
|
||||
postAction: null,
|
||||
user: null,
|
||||
post: null,
|
||||
successCallback: null,
|
||||
|
||||
resetModal() {
|
||||
this.setProperties({
|
||||
reason: null,
|
||||
message: null,
|
||||
loadingUser: true,
|
||||
post: null,
|
||||
postEdit: null,
|
||||
postAction: 'delete',
|
||||
before: null,
|
||||
successCallback: null
|
||||
});
|
||||
},
|
||||
|
||||
penalize(cb) {
|
||||
let before = this.get('before');
|
||||
let promise = before ? before() : Ember.RSVP.resolve();
|
||||
|
||||
return promise
|
||||
.then(() => cb())
|
||||
.then(result => {
|
||||
this.send('closeModal');
|
||||
let callback = this.get('successCallback');
|
||||
if (callback) {
|
||||
callback(result);
|
||||
}
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
}
|
||||
});
|
107
app/assets/javascripts/admin/mixins/setting-component.js.es6
Normal file
@ -0,0 +1,107 @@
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { categoryLinkHTML } from 'discourse/helpers/category-link';
|
||||
|
||||
const CUSTOM_TYPES = [
|
||||
'bool',
|
||||
'enum',
|
||||
'list',
|
||||
'url_list',
|
||||
'host_list',
|
||||
'category_list',
|
||||
'value_list',
|
||||
'category'
|
||||
];
|
||||
|
||||
export default Ember.Mixin.create({
|
||||
classNameBindings: [':row', ':setting', 'setting.overridden', 'typeClass'],
|
||||
content: Ember.computed.alias('setting'),
|
||||
validationMessage: null,
|
||||
|
||||
@computed("buffered.value", "setting.value")
|
||||
dirty(bufferVal, settingVal) {
|
||||
if (bufferVal === null || bufferVal === undefined) bufferVal = '';
|
||||
if (settingVal === null || settingVal === undefined) settingVal = '';
|
||||
|
||||
return bufferVal.toString() !== settingVal.toString();
|
||||
},
|
||||
|
||||
@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>");
|
||||
}
|
||||
},
|
||||
|
||||
@computed('componentType')
|
||||
typeClass(componentType) {
|
||||
return componentType.replace(/\_/g, '-');
|
||||
},
|
||||
|
||||
@computed("setting.setting")
|
||||
settingName(setting) {
|
||||
return setting.replace(/\_/g, ' ');
|
||||
},
|
||||
|
||||
@computed("setting.type")
|
||||
componentType(type) {
|
||||
return CUSTOM_TYPES.indexOf(type) !== -1 ? type : 'string';
|
||||
},
|
||||
|
||||
@computed("typeClass")
|
||||
componentName(typeClass) {
|
||||
return "site-settings/" + typeClass;
|
||||
},
|
||||
|
||||
_watchEnterKey: function() {
|
||||
const self = this;
|
||||
this.$().on("keydown.setting-enter", ".input-setting-string", function (e) {
|
||||
if (e.keyCode === 13) { // enter key
|
||||
self._save();
|
||||
}
|
||||
});
|
||||
}.on('didInsertElement'),
|
||||
|
||||
_removeBindings: function() {
|
||||
this.$().off("keydown.setting-enter");
|
||||
}.on("willDestroyElement"),
|
||||
|
||||
_save() {
|
||||
Em.warn("You should define a `_save` method", { id: "admin.mixins.setting-component" });
|
||||
return Ember.RSVP.resolve();
|
||||
},
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this._save().then(() => {
|
||||
this.set('validationMessage', null);
|
||||
this.commitBuffer();
|
||||
}).catch(e => {
|
||||
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
|
||||
this.set('validationMessage', e.jqXHR.responseJSON.errors[0]);
|
||||
} else {
|
||||
this.set('validationMessage', I18n.t('generic_error'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
resetDefault() {
|
||||
this.set('buffered.value', this.get('setting.default'));
|
||||
this._save();
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.rollbackBuffer();
|
||||
}
|
||||
}
|
||||
});
|
29
app/assets/javascripts/admin/mixins/setting-object.js.es6
Normal file
@ -0,0 +1,29 @@
|
||||
export default Ember.Mixin.create({
|
||||
overridden: function() {
|
||||
let val = this.get('value'),
|
||||
defaultVal = this.get('default');
|
||||
|
||||
if (val === null) val = '';
|
||||
if (defaultVal === null) defaultVal = '';
|
||||
|
||||
return val.toString() !== defaultVal.toString();
|
||||
}.property('value', 'default'),
|
||||
|
||||
validValues: function() {
|
||||
const vals = [],
|
||||
translateNames = this.get('translate_names');
|
||||
|
||||
this.get('valid_values').forEach(v => {
|
||||
if (v.name && v.name.length > 0 && translateNames) {
|
||||
vals.addObject({ name: I18n.t(v.name), value: v.value });
|
||||
} else {
|
||||
vals.addObject(v);
|
||||
}
|
||||
});
|
||||
return vals;
|
||||
}.property('valid_values'),
|
||||
|
||||
allowsNone: function() {
|
||||
if ( _.indexOf(this.get('valid_values'), '') >= 0 ) return 'admin.settings.none';
|
||||
}.property('valid_values')
|
||||
});
|
@ -0,0 +1,54 @@
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
|
||||
const ATTRIBUTES = [ "disk_space", "updated_at", "last_backup_taken_at" ];
|
||||
|
||||
const AdminDashboardNext = Discourse.Model.extend({});
|
||||
|
||||
AdminDashboardNext.reopenClass({
|
||||
/**
|
||||
Fetch all dashboard data. This can be an expensive request when the cached data
|
||||
has expired and the server must collect the data again.
|
||||
|
||||
@method find
|
||||
@return {jqXHR} a jQuery Promise object
|
||||
**/
|
||||
find() {
|
||||
|
||||
return ajax("/admin/dashboard-next.json").then(function(json) {
|
||||
|
||||
var model = AdminDashboardNext.create();
|
||||
|
||||
model.set("reports", json.reports);
|
||||
model.set("version_check", json.version_check);
|
||||
|
||||
const attributes = {};
|
||||
ATTRIBUTES.forEach(a => attributes[a] = json[a]);
|
||||
model.set("attributes", attributes);
|
||||
|
||||
model.set("loaded", true);
|
||||
|
||||
return model;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
Only fetch the list of problems that should be rendered on the dashboard.
|
||||
The model will only have its "problems" attribute set.
|
||||
|
||||
@method fetchProblems
|
||||
@return {jqXHR} a jQuery Promise object
|
||||
**/
|
||||
fetchProblems: function() {
|
||||
return ajax("/admin/dashboard/problems.json", {
|
||||
type: 'GET',
|
||||
dataType: 'json'
|
||||
}).then(function(json) {
|
||||
var model = AdminDashboardNext.create(json);
|
||||
model.set('loaded', true);
|
||||
return model;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default AdminDashboardNext;
|
@ -19,23 +19,6 @@ AdminDashboard.reopenClass({
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
Only fetch the list of problems that should be rendered on the dashboard.
|
||||
The model will only have its "problems" attribute set.
|
||||
|
||||
@method fetchProblems
|
||||
@return {jqXHR} a jQuery Promise object
|
||||
**/
|
||||
fetchProblems: function() {
|
||||
return ajax("/admin/dashboard/problems.json", {
|
||||
type: 'GET',
|
||||
dataType: 'json'
|
||||
}).then(function(json) {
|
||||
var model = AdminDashboard.create(json);
|
||||
model.set('loaded', true);
|
||||
return model;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default AdminDashboard;
|
||||
|
@ -5,7 +5,6 @@ import { propertyNotEqual } from 'discourse/lib/computed';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import ApiKey from 'admin/models/api-key';
|
||||
import Group from 'discourse/models/group';
|
||||
import TL3Requirements from 'admin/models/tl3-requirements';
|
||||
import { userPath } from 'discourse/lib/url';
|
||||
|
||||
const wrapAdmin = user => user ? AdminUser.create(user) : null;
|
||||
@ -88,21 +87,6 @@ const AdminUser = Discourse.User.extend({
|
||||
}).then(() => this.set('api_key', null));
|
||||
},
|
||||
|
||||
deleteAllPostsExplanation: function() {
|
||||
if (!this.get('can_delete_all_posts')) {
|
||||
if (this.get('deleteForbidden') && this.get('staff')) {
|
||||
return I18n.t('admin.user.delete_posts_forbidden_because_staff');
|
||||
}
|
||||
if (this.get('post_count') > Discourse.SiteSettings.delete_all_posts_max) {
|
||||
return I18n.t('admin.user.cant_delete_all_too_many_posts', {count: Discourse.SiteSettings.delete_all_posts_max});
|
||||
} else {
|
||||
return I18n.t('admin.user.cant_delete_all_posts', {count: Discourse.SiteSettings.delete_user_max_post_age});
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}.property('can_delete_all_posts', 'deleteForbidden'),
|
||||
|
||||
deleteAllPosts() {
|
||||
const user = this,
|
||||
message = I18n.messageFormat('admin.user.delete_all_posts_confirm_MF', { "POSTS": user.get('post_count'), "TOPICS": user.get('topic_count') }),
|
||||
@ -168,6 +152,14 @@ const AdminUser = Discourse.User.extend({
|
||||
}).catch(popupAjaxError);
|
||||
},
|
||||
|
||||
disableSecondFactor() {
|
||||
return ajax(`/admin/users/${this.get('id')}/disable_second_factor`, {
|
||||
type: 'PUT'
|
||||
}).then(() => {
|
||||
this.set('second_factor_enabled', false);
|
||||
}).catch(popupAjaxError);
|
||||
},
|
||||
|
||||
refreshBrowsers() {
|
||||
return ajax("/admin/users/" + this.get('id') + "/refresh_browsers", {
|
||||
type: 'POST'
|
||||
@ -233,8 +225,6 @@ const AdminUser = Discourse.User.extend({
|
||||
return this.get('trust_level') < 4;
|
||||
}.property('trust_level'),
|
||||
|
||||
isSuspended: Em.computed.equal('suspended', true),
|
||||
isSilenced: Ember.computed.equal('silenced', true),
|
||||
canSuspend: Em.computed.not('staff'),
|
||||
|
||||
suspendDuration: function() {
|
||||
@ -293,7 +283,8 @@ const AdminUser = Discourse.User.extend({
|
||||
|
||||
deactivate() {
|
||||
return ajax('/admin/users/' + this.id + '/deactivate', {
|
||||
type: 'PUT'
|
||||
type: 'PUT',
|
||||
data: { context: document.location.pathname }
|
||||
}).then(function() {
|
||||
window.location.reload();
|
||||
}).catch(function(e) {
|
||||
@ -345,8 +336,6 @@ const AdminUser = Discourse.User.extend({
|
||||
}).catch(popupAjaxError);
|
||||
},
|
||||
|
||||
anonymizeForbidden: Em.computed.not("can_be_anonymized"),
|
||||
|
||||
anonymize() {
|
||||
const user = this,
|
||||
message = I18n.t("admin.user.anonymize_confirm");
|
||||
@ -385,26 +374,13 @@ const AdminUser = Discourse.User.extend({
|
||||
bootbox.dialog(message, buttons, { "classes": "delete-user-modal" });
|
||||
},
|
||||
|
||||
deleteForbidden: Em.computed.not("canBeDeleted"),
|
||||
|
||||
deleteExplanation: function() {
|
||||
if (this.get('deleteForbidden')) {
|
||||
if (this.get('staff')) {
|
||||
return I18n.t('admin.user.delete_forbidden_because_staff');
|
||||
} else {
|
||||
return I18n.t('admin.user.delete_forbidden', {count: Discourse.SiteSettings.delete_user_max_post_age});
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}.property('deleteForbidden'),
|
||||
|
||||
destroy(opts) {
|
||||
const user = this,
|
||||
message = I18n.t("admin.user.delete_confirm"),
|
||||
location = document.location.pathname;
|
||||
|
||||
const performDestroy = function(block) {
|
||||
bootbox.dialog(I18n.t('admin.user.deleting_user'));
|
||||
let formData = { context: location };
|
||||
if (block) {
|
||||
formData["block_email"] = true;
|
||||
@ -464,11 +440,12 @@ const AdminUser = Discourse.User.extend({
|
||||
});
|
||||
},
|
||||
|
||||
tl3Requirements: function() {
|
||||
if (this.get('tl3_requirements')) {
|
||||
return TL3Requirements.create(this.get('tl3_requirements'));
|
||||
@computed('tl3_requirements')
|
||||
tl3Requirements(requirements) {
|
||||
if (requirements) {
|
||||
return this.store.createRecord('tl3Requirements', requirements);
|
||||
}
|
||||
}.property('tl3_requirements'),
|
||||
},
|
||||
|
||||
@computed('suspended_by')
|
||||
suspendedBy: wrapAdmin,
|
||||
|
@ -22,6 +22,11 @@ export default Post.extend({
|
||||
});
|
||||
},
|
||||
|
||||
@computed('post_actions')
|
||||
hasDisposedBy() {
|
||||
return this.get('post_actions').some(action => action.disposed_by);
|
||||
},
|
||||
|
||||
@computed('post_actions.@each.name_key')
|
||||
flaggedForSpam() {
|
||||
return this.get('post_actions').every(action => action.name_key === 'spam');
|
||||
@ -57,11 +62,74 @@ export default Post.extend({
|
||||
},
|
||||
|
||||
deferFlags(deletePost) {
|
||||
return ajax('/admin/flags/defer/' + this.id, { type: 'POST', cache: false, data: { delete_post: deletePost } }).catch(popupAjaxError);
|
||||
const action = () => {
|
||||
return ajax('/admin/flags/defer/' + this.id, {
|
||||
type: 'POST', cache: false, data: { delete_post: deletePost }
|
||||
});
|
||||
};
|
||||
|
||||
if (deletePost && this._hasDeletableReplies()) {
|
||||
return this._actOnFlagAndDeleteReplies(action);
|
||||
} else {
|
||||
return action().catch(popupAjaxError);
|
||||
}
|
||||
},
|
||||
|
||||
agreeFlags(actionOnPost) {
|
||||
return ajax('/admin/flags/agree/' + this.id, { type: 'POST', cache: false, data: { action_on_post: actionOnPost } }).catch(popupAjaxError);
|
||||
const action = () => {
|
||||
return ajax('/admin/flags/agree/' + this.id, {
|
||||
type: 'POST', cache: false, data: { action_on_post: actionOnPost }
|
||||
});
|
||||
};
|
||||
|
||||
if (actionOnPost === 'delete' && this._hasDeletableReplies()) {
|
||||
return this._actOnFlagAndDeleteReplies(action);
|
||||
} else {
|
||||
return action().catch(popupAjaxError);
|
||||
}
|
||||
},
|
||||
|
||||
_hasDeletableReplies() {
|
||||
return this.get('post_number') > 1 && this.get('reply_count') > 0;
|
||||
},
|
||||
|
||||
_actOnFlagAndDeleteReplies(action) {
|
||||
return new Ember.RSVP.Promise((resolve, reject) => {
|
||||
return ajax(`/posts/${this.id}/reply-ids/all.json`).then(replies => {
|
||||
const buttons = [];
|
||||
|
||||
buttons.push({
|
||||
label: I18n.t('no_value'),
|
||||
callback() {
|
||||
action()
|
||||
.then(resolve)
|
||||
.catch(error => {
|
||||
popupAjaxError(error);
|
||||
reject();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
buttons.push({
|
||||
label: I18n.t('yes_value'),
|
||||
class: "btn-danger",
|
||||
callback() {
|
||||
Post.deleteMany(replies.map(r => r.id))
|
||||
.then(action)
|
||||
.then(resolve)
|
||||
.catch(error => {
|
||||
popupAjaxError(error);
|
||||
reject();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
bootbox.dialog(I18n.t("admin.flags.delete_replies", { count: replies.length }), buttons);
|
||||
}).catch(error => {
|
||||
popupAjaxError(error);
|
||||
reject();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
postHidden: Ember.computed.alias('hidden'),
|
||||
|
@ -1,13 +1,24 @@
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import round from "discourse/lib/round";
|
||||
import { fmt } from 'discourse/lib/computed';
|
||||
import { fillMissingDates } from "discourse/lib/utilities";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import { number } from 'discourse/lib/formatter';
|
||||
|
||||
const Report = Discourse.Model.extend({
|
||||
reportUrl: fmt("type", "/admin/reports/%@"),
|
||||
average: false,
|
||||
percent: false,
|
||||
higher_is_better: true,
|
||||
|
||||
@computed("type", "start_date", "end_date")
|
||||
reportUrl(type, start_date, end_date) {
|
||||
start_date = moment(start_date).locale("en").format("YYYY-MM-DD");
|
||||
end_date = moment(end_date).locale("en").format("YYYY-MM-DD");
|
||||
return Discourse.getURL(`/admin/reports/${type}?start_date=${start_date}&end_date=${end_date}`);
|
||||
},
|
||||
|
||||
valueAt(numDaysAgo) {
|
||||
if (this.data) {
|
||||
const wantedDate = moment().subtract(numDaysAgo, "days").format("YYYY-MM-DD");
|
||||
const wantedDate = moment().subtract(numDaysAgo, "days").locale("en").format("YYYY-MM-DD");
|
||||
const item = this.data.find(d => d.x === wantedDate);
|
||||
if (item) {
|
||||
return item.y;
|
||||
@ -20,7 +31,7 @@ const Report = Discourse.Model.extend({
|
||||
if (this.data) {
|
||||
const earliestDate = moment().subtract(endDaysAgo, "days").startOf("day");
|
||||
const latestDate = moment().subtract(startDaysAgo, "days").startOf("day");
|
||||
var d, sum = 0, count = 0;
|
||||
let d, sum = 0, count = 0;
|
||||
_.each(this.data, datum => {
|
||||
d = moment(datum.x);
|
||||
if (d >= earliestDate && d <= latestDate) {
|
||||
@ -33,105 +44,208 @@ const Report = Discourse.Model.extend({
|
||||
}
|
||||
},
|
||||
|
||||
todayCount: function() { return this.valueAt(0); }.property("data"),
|
||||
yesterdayCount: function() { return this.valueAt(1); }.property("data"),
|
||||
sevenDaysAgoCount: function() { return this.valueAt(7); }.property("data"),
|
||||
thirtyDaysAgoCount: function() { return this.valueAt(30); }.property("data"),
|
||||
todayCount: function() { return this.valueAt(0); }.property("data", "average"),
|
||||
yesterdayCount: function() { return this.valueAt(1); }.property("data", "average"),
|
||||
sevenDaysAgoCount: function() { return this.valueAt(7); }.property("data", "average"),
|
||||
thirtyDaysAgoCount: function() { return this.valueAt(30); }.property("data", "average"),
|
||||
|
||||
lastSevenDaysCount: function() { return this.valueFor(1, 7); }.property("data"),
|
||||
lastThirtyDaysCount: function() { return this.valueFor(1, 30); }.property("data"),
|
||||
lastSevenDaysCount: function() {
|
||||
return this.averageCount(7, this.valueFor(1, 7));
|
||||
}.property("data", "average"),
|
||||
lastThirtyDaysCount: function() {
|
||||
return this.averageCount(30, this.valueFor(1, 30));
|
||||
}.property("data", "average"),
|
||||
|
||||
yesterdayTrend: function() {
|
||||
const yesterdayVal = this.valueAt(1);
|
||||
const twoDaysAgoVal = this.valueAt(2);
|
||||
if (yesterdayVal > twoDaysAgoVal) {
|
||||
return "trending-up";
|
||||
} else if (yesterdayVal < twoDaysAgoVal) {
|
||||
return "trending-down";
|
||||
} else {
|
||||
return "no-change";
|
||||
averageCount(count, value) {
|
||||
return this.get("average") ? value / count : value;
|
||||
},
|
||||
|
||||
@computed("yesterdayCount", "higher_is_better")
|
||||
yesterdayTrend(yesterdayCount, higherIsBetter) {
|
||||
return this._computeTrend(this.valueAt(2), yesterdayCount, higherIsBetter);
|
||||
},
|
||||
|
||||
@computed("lastSevenDaysCount", "higher_is_better")
|
||||
sevenDaysTrend(lastSevenDaysCount, higherIsBetter) {
|
||||
return this._computeTrend(this.valueFor(8, 14), lastSevenDaysCount, higherIsBetter);
|
||||
},
|
||||
|
||||
@computed("data")
|
||||
currentTotal(data){
|
||||
return _.reduce(data, (cur, pair) => cur + pair.y, 0);
|
||||
},
|
||||
|
||||
@computed("data", "currentTotal")
|
||||
currentAverage(data, total) {
|
||||
return Ember.makeArray(data).length === 0 ? 0 : parseFloat((total / parseFloat(data.length)).toFixed(1));
|
||||
},
|
||||
|
||||
@computed("trend", "higher_is_better")
|
||||
trendIcon(trend, higherIsBetter) {
|
||||
return this._iconForTrend(trend, higherIsBetter);
|
||||
},
|
||||
|
||||
@computed("sevenDaysTrend", "higher_is_better")
|
||||
sevenDaysTrendIcon(sevenDaysTrend, higherIsBetter) {
|
||||
return this._iconForTrend(sevenDaysTrend, higherIsBetter);
|
||||
},
|
||||
|
||||
@computed("thirtyDaysTrend", "higher_is_better")
|
||||
thirtyDaysTrendIcon(thirtyDaysTrend, higherIsBetter) {
|
||||
return this._iconForTrend(thirtyDaysTrend, higherIsBetter);
|
||||
},
|
||||
|
||||
@computed("yesterdayTrend", "higher_is_better")
|
||||
yesterdayTrendIcon(yesterdayTrend, higherIsBetter) {
|
||||
return this._iconForTrend(yesterdayTrend, higherIsBetter);
|
||||
},
|
||||
|
||||
@computed("prev_period", "currentTotal", "currentAverage", "higher_is_better")
|
||||
trend(prev, currentTotal, currentAverage, higherIsBetter) {
|
||||
const total = this.get("average") ? currentAverage : currentTotal;
|
||||
return this._computeTrend(prev, total, higherIsBetter);
|
||||
},
|
||||
|
||||
@computed("prev30Days", "lastThirtyDaysCount", "higher_is_better")
|
||||
thirtyDaysTrend(prev30Days, lastThirtyDaysCount, higherIsBetter) {
|
||||
return this._computeTrend(prev30Days, lastThirtyDaysCount, higherIsBetter);
|
||||
},
|
||||
|
||||
@computed("type")
|
||||
icon(type) {
|
||||
if (type.indexOf("message") > -1) {
|
||||
return "envelope";
|
||||
}
|
||||
}.property("data"),
|
||||
|
||||
sevenDayTrend: function() {
|
||||
const currentPeriod = this.valueFor(1, 7);
|
||||
const prevPeriod = this.valueFor(8, 14);
|
||||
if (currentPeriod > prevPeriod) {
|
||||
return "trending-up";
|
||||
} else if (currentPeriod < prevPeriod) {
|
||||
return "trending-down";
|
||||
} else {
|
||||
return "no-change";
|
||||
}
|
||||
}.property("data"),
|
||||
|
||||
thirtyDayTrend: function() {
|
||||
if (this.get("prev30Days")) {
|
||||
const currentPeriod = this.valueFor(1, 30);
|
||||
if (currentPeriod > this.get("prev30Days")) {
|
||||
return "trending-up";
|
||||
} else if (currentPeriod < this.get("prev30Days")) {
|
||||
return "trending-down";
|
||||
}
|
||||
}
|
||||
return "no-change";
|
||||
}.property("data", "prev30Days"),
|
||||
|
||||
icon: function() {
|
||||
switch (this.get("type")) {
|
||||
switch (type) {
|
||||
case "page_view_total_reqs": return "file";
|
||||
case "visits": return "user";
|
||||
case "time_to_first_response": return "reply";
|
||||
case "flags": return "flag";
|
||||
case "likes": return "heart";
|
||||
case "bookmarks": return "bookmark";
|
||||
default: return null;
|
||||
}
|
||||
}.property("type"),
|
||||
},
|
||||
|
||||
method: function() {
|
||||
if (this.get("type") === "time_to_first_response") {
|
||||
@computed("type")
|
||||
method(type) {
|
||||
if (type === "time_to_first_response") {
|
||||
return "average";
|
||||
} else {
|
||||
return "sum";
|
||||
}
|
||||
}.property("type"),
|
||||
},
|
||||
|
||||
percentChangeString(val1, val2) {
|
||||
const val = ((val1 - val2) / val2) * 100;
|
||||
if (isNaN(val) || !isFinite(val)) {
|
||||
const change = this._computeChange(val1, val2);
|
||||
|
||||
if (isNaN(change) || !isFinite(change)) {
|
||||
return null;
|
||||
} else if (val > 0) {
|
||||
return "+" + val.toFixed(0) + "%";
|
||||
} else if (change > 0) {
|
||||
return "+" + change.toFixed(0) + "%";
|
||||
} else {
|
||||
return val.toFixed(0) + "%";
|
||||
return change.toFixed(0) + "%";
|
||||
}
|
||||
},
|
||||
|
||||
changeTitle(val1, val2, prevPeriodString) {
|
||||
const percentChange = this.percentChangeString(val1, val2);
|
||||
var title = "";
|
||||
if (percentChange) { title += percentChange + " change. "; }
|
||||
title += "Was " + val2 + " " + prevPeriodString + ".";
|
||||
@computed("prev_period", "currentTotal", "currentAverage")
|
||||
trendTitle(prev, currentTotal, currentAverage) {
|
||||
let current = this.get("average") ? currentAverage : currentTotal;
|
||||
let percent = this.percentChangeString(prev, current);
|
||||
|
||||
if (this.get("average")) {
|
||||
prev = prev ? prev.toFixed(1) : "0";
|
||||
if (this.get("percent")) {
|
||||
current += "%";
|
||||
prev += "%";
|
||||
}
|
||||
} else {
|
||||
prev = number(prev);
|
||||
current = number(current);
|
||||
}
|
||||
|
||||
return I18n.t("admin.dashboard.reports.trend_title", {percent, prev, current});
|
||||
},
|
||||
|
||||
changeTitle(valAtT1, valAtT2, prevPeriodString) {
|
||||
const change = this.percentChangeString(valAtT1, valAtT2);
|
||||
let title = "";
|
||||
if (change) { title += `${change} change. `; }
|
||||
title += `Was ${number(valAtT1)} ${prevPeriodString}.`;
|
||||
return title;
|
||||
},
|
||||
|
||||
yesterdayCountTitle: function() {
|
||||
return this.changeTitle(this.valueAt(1), this.valueAt(2), "two days ago");
|
||||
}.property("data"),
|
||||
@computed("yesterdayCount")
|
||||
yesterdayCountTitle(yesterdayCount) {
|
||||
return this.changeTitle(this.valueAt(2), yesterdayCount, "two days ago");
|
||||
},
|
||||
|
||||
sevenDayCountTitle: function() {
|
||||
return this.changeTitle(this.valueFor(1, 7), this.valueFor(8, 14), "two weeks ago");
|
||||
}.property("data"),
|
||||
@computed("lastSevenDaysCount")
|
||||
sevenDaysCountTitle(lastSevenDaysCount) {
|
||||
return this.changeTitle(this.valueFor(8, 14), lastSevenDaysCount, "two weeks ago");
|
||||
},
|
||||
|
||||
thirtyDayCountTitle: function() {
|
||||
return this.changeTitle(this.valueFor(1, 30), this.get("prev30Days"), "in the previous 30 day period");
|
||||
}.property("data"),
|
||||
@computed("prev30Days", "lastThirtyDaysCount")
|
||||
thirtyDaysCountTitle(prev30Days, lastThirtyDaysCount) {
|
||||
return this.changeTitle(prev30Days, lastThirtyDaysCount, "in the previous 30 day period");
|
||||
},
|
||||
|
||||
dataReversed: function() {
|
||||
return this.get("data").toArray().reverse();
|
||||
}.property("data")
|
||||
@computed("data")
|
||||
sortedData(data) {
|
||||
return this.get("xAxisIsDate") ? data.toArray().reverse() : data.toArray();
|
||||
},
|
||||
|
||||
@computed("data")
|
||||
xAxisIsDate() {
|
||||
if (!this.data[0]) return false;
|
||||
return this.data && this.data[0].x.match(/\d{4}-\d{1,2}-\d{1,2}/);
|
||||
},
|
||||
|
||||
_computeChange(valAtT1, valAtT2) {
|
||||
return ((valAtT2 - valAtT1) / valAtT1) * 100;
|
||||
},
|
||||
|
||||
_computeTrend(valAtT1, valAtT2, higherIsBetter) {
|
||||
const change = this._computeChange(valAtT1, valAtT2);
|
||||
|
||||
if (change > 50) {
|
||||
return higherIsBetter ? "high-trending-up" : "high-trending-down";
|
||||
} else if (change > 2) {
|
||||
return higherIsBetter ? "trending-up" : "trending-down";
|
||||
} else if (change <= 2 && change >= -2) {
|
||||
return "no-change";
|
||||
} else if (change < -50) {
|
||||
return higherIsBetter ? "high-trending-down" : "high-trending-up";
|
||||
} else if (change < -2) {
|
||||
return higherIsBetter ? "trending-down" : "trending-up";
|
||||
}
|
||||
},
|
||||
|
||||
_iconForTrend(trend, higherIsBetter) {
|
||||
switch (trend) {
|
||||
case "trending-up":
|
||||
return higherIsBetter ? "angle-up" : "angle-down";
|
||||
case "trending-down":
|
||||
return higherIsBetter ? "angle-down" : "angle-up";
|
||||
case "high-trending-up":
|
||||
return higherIsBetter ? "angle-double-up" : "angle-double-down";
|
||||
case "high-trending-down":
|
||||
return higherIsBetter ? "angle-double-down" : "angle-double-up";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Report.reopenClass({
|
||||
fillMissingDates(report) {
|
||||
if (_.isArray(report.data)) {
|
||||
|
||||
const startDateFormatted = moment.utc(report.start_date).locale("en").format("YYYY-MM-DD");
|
||||
const endDateFormatted = moment.utc(report.end_date).locale("en").format("YYYY-MM-DD");
|
||||
report.data = fillMissingDates(report.data, startDateFormatted, endDateFormatted);
|
||||
}
|
||||
},
|
||||
|
||||
find(type, startDate, endDate, categoryId, groupId) {
|
||||
return ajax("/admin/reports/" + type, {
|
||||
@ -142,16 +256,19 @@ Report.reopenClass({
|
||||
group_id: groupId
|
||||
}
|
||||
}).then(json => {
|
||||
// Add a percent field to each tuple
|
||||
let maxY = 0;
|
||||
json.report.data.forEach(row => {
|
||||
if (row.y > maxY) maxY = row.y;
|
||||
});
|
||||
if (maxY > 0) {
|
||||
json.report.data.forEach(row => row.percentage = Math.round((row.y / maxY) * 100));
|
||||
}
|
||||
// Add zero values for missing dates
|
||||
Report.fillMissingDates(json.report);
|
||||
|
||||
const model = Report.create({ type: type });
|
||||
model.setProperties(json.report);
|
||||
|
||||
if (json.report.related_report) {
|
||||
// TODO: fillMissingDates if xaxis is date
|
||||
const related = Report.create({ type: json.report.related_report.type });
|
||||
related.setProperties(json.report.related_report);
|
||||
model.set("relatedReport", related);
|
||||
}
|
||||
|
||||
return model;
|
||||
});
|
||||
}
|
||||
|
@ -1,31 +1,7 @@
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
const SiteSetting = Discourse.Model.extend({
|
||||
overridden: function() {
|
||||
let val = this.get('value'),
|
||||
defaultVal = this.get('default');
|
||||
import Setting from 'admin/mixins/setting-object';
|
||||
|
||||
if (val === null) val = '';
|
||||
if (defaultVal === null) defaultVal = '';
|
||||
|
||||
return val.toString() !== defaultVal.toString();
|
||||
}.property('value', 'default'),
|
||||
|
||||
validValues: function() {
|
||||
const vals = [],
|
||||
translateNames = this.get('translate_names');
|
||||
|
||||
this.get('valid_values').forEach(function(v) {
|
||||
if (v.name && v.name.length > 0) {
|
||||
vals.addObject(translateNames ? {name: I18n.t(v.name), value: v.value} : v);
|
||||
}
|
||||
});
|
||||
return vals;
|
||||
}.property('valid_values'),
|
||||
|
||||
allowsNone: function() {
|
||||
if ( _.indexOf(this.get('valid_values'), '') >= 0 ) return 'admin.site_settings.none';
|
||||
}.property('valid_values')
|
||||
});
|
||||
const SiteSetting = Discourse.Model.extend(Setting, {});
|
||||
|
||||
SiteSetting.reopenClass({
|
||||
findAll() {
|
||||
|
@ -1,50 +1,58 @@
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
import AdminUser from 'admin/models/admin-user';
|
||||
import { escapeExpression } from 'discourse/lib/utilities';
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import AdminUser from "admin/models/admin-user";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
|
||||
function format(label, value, escape = true) {
|
||||
return value ? `<b>${I18n.t(label)}</b>: ${escape ? escapeExpression(value) : value}` : "";
|
||||
};
|
||||
|
||||
const StaffActionLog = Discourse.Model.extend({
|
||||
showFullDetails: false,
|
||||
|
||||
actionName: function() {
|
||||
return I18n.t("admin.logs.staff_actions.actions." + this.get('action_name'));
|
||||
}.property('action_name'),
|
||||
|
||||
formattedDetails: function() {
|
||||
var formatted = "";
|
||||
formatted += this.format('email', 'email');
|
||||
formatted += this.format('admin.logs.ip_address', 'ip_address');
|
||||
formatted += this.format('admin.logs.topic_id', 'topic_id');
|
||||
formatted += this.format('admin.logs.post_id', 'post_id');
|
||||
formatted += this.format('admin.logs.category_id', 'category_id');
|
||||
if (!this.get('useCustomModalForDetails')) {
|
||||
formatted += this.format('admin.logs.staff_actions.new_value', 'new_value');
|
||||
formatted += this.format('admin.logs.staff_actions.previous_value', 'previous_value');
|
||||
}
|
||||
if (!this.get('useModalForDetails')) {
|
||||
if (this.get('details')) formatted += escapeExpression(this.get('details')) + '<br/>';
|
||||
}
|
||||
return formatted;
|
||||
}.property('ip_address', 'email', 'topic_id', 'post_id', 'category_id'),
|
||||
|
||||
format: function(label, propertyName) {
|
||||
if (this.get(propertyName)) {
|
||||
return ('<b>' + I18n.t(label) + ':</b> ' + escapeExpression(this.get(propertyName)) + '<br/>');
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
@computed("action_name")
|
||||
actionName(actionName) {
|
||||
return I18n.t(`admin.logs.staff_actions.actions.${actionName}`);
|
||||
},
|
||||
|
||||
useModalForDetails: function() {
|
||||
return (this.get('details') && this.get('details').length > 100);
|
||||
}.property('action_name'),
|
||||
@computed("email", "ip_address", "topic_id", "post_id", "category_id", "new_value", "previous_value", "details", "useCustomModalForDetails", "useModalForDetails")
|
||||
formattedDetails(email, ipAddress, topicId, postId, categoryId, newValue, previousValue, details, useCustomModalForDetails, useModalForDetails) {
|
||||
const postLink = postId ? `<a href data-link-post-id="${postId}">${postId}</a>` : null;
|
||||
|
||||
useCustomModalForDetails: function() {
|
||||
return _.contains(['change_theme', 'delete_theme'], this.get('action_name'));
|
||||
}.property('action_name')
|
||||
let lines = [
|
||||
format("email", email),
|
||||
format("admin.logs.ip_address", ipAddress),
|
||||
format("admin.logs.topic_id", topicId),
|
||||
format("admin.logs.post_id", postLink, false),
|
||||
format("admin.logs.category_id", categoryId),
|
||||
];
|
||||
|
||||
if (!useCustomModalForDetails) {
|
||||
lines.push(format("admin.logs.staff_actions.new_value", newValue));
|
||||
lines.push(format("admin.logs.staff_actions.previous_value", previousValue));
|
||||
}
|
||||
|
||||
if (!useModalForDetails && details) {
|
||||
lines = [...lines, ...escapeExpression(details).split("\n")];
|
||||
}
|
||||
|
||||
const formatted = lines.filter(l => l.length > 0).join("<br/>");
|
||||
return formatted.length > 0 ? formatted + "<br/>" : "";
|
||||
},
|
||||
|
||||
@computed("details")
|
||||
useModalForDetails(details) {
|
||||
return details && details.length > 100;
|
||||
},
|
||||
|
||||
@computed("action_name")
|
||||
useCustomModalForDetails(actionName) {
|
||||
return ["change_theme", "delete_theme"].includes(actionName);
|
||||
}
|
||||
});
|
||||
|
||||
StaffActionLog.reopenClass({
|
||||
create: function(attrs) {
|
||||
create(attrs) {
|
||||
attrs = attrs || {};
|
||||
|
||||
if (attrs.acting_user) {
|
||||
@ -56,13 +64,11 @@ StaffActionLog.reopenClass({
|
||||
return this._super(attrs);
|
||||
},
|
||||
|
||||
findAll: function(filters) {
|
||||
return ajax("/admin/logs/staff_action_logs.json", { data: filters }).then((data) => {
|
||||
findAll(data) {
|
||||
return ajax("/admin/logs/staff_action_logs.json", { data }).then(result => {
|
||||
return {
|
||||
staff_action_logs: data.staff_action_logs.map(function(s) {
|
||||
return StaffActionLog.create(s);
|
||||
}),
|
||||
user_history_actions: data.user_history_actions
|
||||
staff_action_logs: result.staff_action_logs.map(s => StaffActionLog.create(s)),
|
||||
user_history_actions: result.user_history_actions
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -0,0 +1,3 @@
|
||||
import Setting from 'admin/mixins/setting-object';
|
||||
|
||||
export default Discourse.Model.extend(Setting, {});
|
@ -5,6 +5,8 @@ const THEME_UPLOAD_VAR = 2;
|
||||
|
||||
const Theme = RestModel.extend({
|
||||
|
||||
FIELDS_IDS: [0, 1],
|
||||
|
||||
@computed('theme_fields')
|
||||
themeFields(fields) {
|
||||
|
||||
@ -14,13 +16,11 @@ const Theme = RestModel.extend({
|
||||
}
|
||||
|
||||
let hash = {};
|
||||
if (fields) {
|
||||
fields.forEach(field=>{
|
||||
if (!field.type_id || field.type_id < THEME_UPLOAD_VAR) {
|
||||
hash[this.getKey(field)] = field;
|
||||
}
|
||||
});
|
||||
}
|
||||
fields.forEach(field => {
|
||||
if (!field.type_id || this.get('FIELDS_IDS').includes(field.type_id)) {
|
||||
hash[this.getKey(field)] = field;
|
||||
}
|
||||
});
|
||||
return hash;
|
||||
},
|
||||
|
||||
@ -29,11 +29,11 @@ const Theme = RestModel.extend({
|
||||
if (!fields) {
|
||||
return [];
|
||||
}
|
||||
return fields.filter((f)=> f.target === 'common' && f.type_id === THEME_UPLOAD_VAR);
|
||||
return fields.filter(f => f.target === 'common' && f.type_id === THEME_UPLOAD_VAR);
|
||||
},
|
||||
|
||||
getKey(field){
|
||||
return field.target + " " + field.name;
|
||||
return `${field.target} ${field.name}`;
|
||||
},
|
||||
|
||||
hasEdited(target, name){
|
||||
@ -151,6 +151,11 @@ const Theme = RestModel.extend({
|
||||
.then(() => this.set("changed", false));
|
||||
},
|
||||
|
||||
saveSettings(name, value) {
|
||||
const settings = {};
|
||||
settings[name] = value;
|
||||
return this.save({ settings });
|
||||
}
|
||||
});
|
||||
|
||||
export default Theme;
|
||||
|
@ -1,11 +1,15 @@
|
||||
const TL3Requirements = Discourse.Model.extend({
|
||||
days_visited_percent: function() {
|
||||
return Math.round((this.get('days_visited') * 100) / this.get('time_period'));
|
||||
}.property('days_visited', 'time_period'),
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
min_days_visited_percent: function() {
|
||||
return Math.round((this.get('min_days_visited') * 100) / this.get('time_period'));
|
||||
}.property('min_days_visited', 'time_period'),
|
||||
export default Discourse.Model.extend({
|
||||
@computed('days_visited', 'time_period')
|
||||
days_visited_percent(daysVisited, timePeriod) {
|
||||
return Math.round((daysVisited * 100) / timePeriod);
|
||||
},
|
||||
|
||||
@computed('min_days_visited', 'time_period')
|
||||
min_days_visited_percent(minDaysVisited, timePeriod) {
|
||||
return Math.round((minDaysVisited * 100) / timePeriod);
|
||||
},
|
||||
|
||||
met: function() {
|
||||
return {
|
||||
@ -21,22 +25,26 @@ const TL3Requirements = Discourse.Model.extend({
|
||||
likes_received: this.get('num_likes_received') >= this.get('min_likes_received'),
|
||||
likes_received_days: this.get('num_likes_received_days') >= this.get('min_likes_received_days'),
|
||||
likes_received_users: this.get('num_likes_received_users') >= this.get('min_likes_received_users'),
|
||||
level_locked: this.get('trust_level_locked')
|
||||
level_locked: this.get('trust_level_locked'),
|
||||
silenced: this.get('penalty_counts.silenced') === 0,
|
||||
suspended: this.get('penalty_counts.suspended') === 0
|
||||
};
|
||||
}.property('days_visited', 'min_days_visited',
|
||||
'num_topics_replied_to', 'min_topics_replied_to',
|
||||
'topics_viewed', 'min_topics_viewed',
|
||||
'posts_read', 'min_posts_read',
|
||||
'num_flagged_posts', 'max_flagged_posts',
|
||||
'topics_viewed_all_time', 'min_topics_viewed_all_time',
|
||||
'posts_read_all_time', 'min_posts_read_all_time',
|
||||
'num_flagged_by_users', 'max_flagged_by_users',
|
||||
'num_likes_given', 'min_likes_given',
|
||||
'num_likes_received', 'min_likes_received',
|
||||
'num_likes_received', 'min_likes_received',
|
||||
'num_likes_received_days', 'min_likes_received_days',
|
||||
'num_likes_received_users', 'min_likes_received_users',
|
||||
'trust_level_locked')
|
||||
}.property(
|
||||
'days_visited', 'min_days_visited',
|
||||
'num_topics_replied_to', 'min_topics_replied_to',
|
||||
'topics_viewed', 'min_topics_viewed',
|
||||
'posts_read', 'min_posts_read',
|
||||
'num_flagged_posts', 'max_flagged_posts',
|
||||
'topics_viewed_all_time', 'min_topics_viewed_all_time',
|
||||
'posts_read_all_time', 'min_posts_read_all_time',
|
||||
'num_flagged_by_users', 'max_flagged_by_users',
|
||||
'num_likes_given', 'min_likes_given',
|
||||
'num_likes_received', 'min_likes_received',
|
||||
'num_likes_received', 'min_likes_received',
|
||||
'num_likes_received_days', 'min_likes_received_days',
|
||||
'num_likes_received_users', 'min_likes_received_users',
|
||||
'trust_level_locked',
|
||||
'penalty_counts.silenced',
|
||||
'penalty_counts.suspended'
|
||||
)
|
||||
});
|
||||
|
||||
export default TL3Requirements;
|
||||
|
@ -1,7 +1,15 @@
|
||||
import Backup from 'admin/models/backup';
|
||||
|
||||
export default Ember.Route.extend({
|
||||
activate() {
|
||||
this.messageBus.subscribe("/admin/backups", backups => this.controller.set("model", backups));
|
||||
},
|
||||
|
||||
model() {
|
||||
return Backup.find();
|
||||
},
|
||||
|
||||
deactivate() {
|
||||
this.messageBus.unsubscribe("/admin/backups");
|
||||
}
|
||||
});
|
||||
|
@ -9,26 +9,24 @@ const LOG_CHANNEL = "/admin/backups/logs";
|
||||
export default Discourse.Route.extend({
|
||||
|
||||
activate() {
|
||||
this.messageBus.subscribe(LOG_CHANNEL, this._processLogMessage.bind(this));
|
||||
},
|
||||
|
||||
_processLogMessage(log) {
|
||||
if (log.message === "[STARTED]") {
|
||||
this.controllerFor("adminBackups").set("model.isOperationRunning", true);
|
||||
this.controllerFor("adminBackupsLogs").get('logs').clear();
|
||||
} else if (log.message === "[FAILED]") {
|
||||
this.controllerFor("adminBackups").set("model.isOperationRunning", false);
|
||||
bootbox.alert(I18n.t("admin.backups.operations.failed", { operation: log.operation }));
|
||||
} else if (log.message === "[SUCCESS]") {
|
||||
Discourse.User.currentProp("hideReadOnlyAlert", false);
|
||||
this.controllerFor("adminBackups").set("model.isOperationRunning", false);
|
||||
if (log.operation === "restore") {
|
||||
// redirect to homepage when the restore is done (session might be lost)
|
||||
window.location.pathname = Discourse.getURL("/");
|
||||
this.messageBus.subscribe(LOG_CHANNEL, (log) => {
|
||||
if (log.message === "[STARTED]") {
|
||||
this.controllerFor("adminBackups").set("model.isOperationRunning", true);
|
||||
this.controllerFor("adminBackupsLogs").get('logs').clear();
|
||||
} else if (log.message === "[FAILED]") {
|
||||
this.controllerFor("adminBackups").set("model.isOperationRunning", false);
|
||||
bootbox.alert(I18n.t("admin.backups.operations.failed", { operation: log.operation }));
|
||||
} else if (log.message === "[SUCCESS]") {
|
||||
Discourse.User.currentProp("hideReadOnlyAlert", false);
|
||||
this.controllerFor("adminBackups").set("model.isOperationRunning", false);
|
||||
if (log.operation === "restore") {
|
||||
// redirect to homepage when the restore is done (session might be lost)
|
||||
window.location.pathname = Discourse.getURL("/");
|
||||
}
|
||||
} else {
|
||||
this.controllerFor("adminBackupsLogs").get('logs').pushObject(Em.Object.create(log));
|
||||
}
|
||||
} else {
|
||||
this.controllerFor("adminBackupsLogs").get('logs').pushObject(Em.Object.create(log));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
model() {
|
||||
@ -122,12 +120,7 @@ export default Discourse.Route.extend({
|
||||
},
|
||||
|
||||
uploadSuccess(filename) {
|
||||
const self = this;
|
||||
bootbox.alert(I18n.t("admin.backups.upload.success", { filename: filename }), function() {
|
||||
Backup.find().then(function (backups) {
|
||||
self.controllerFor("adminBackupsIndex").set("model", backups);
|
||||
});
|
||||
});
|
||||
bootbox.alert(I18n.t("admin.backups.upload.success", { filename: filename }));
|
||||
},
|
||||
|
||||
uploadError(filename, message) {
|
||||
|
@ -18,6 +18,11 @@ export default Ember.Route.extend({
|
||||
},
|
||||
|
||||
setupController(controller, wrapper) {
|
||||
const fields = controller.fieldsForTarget(wrapper.target);
|
||||
if (!fields.includes(wrapper.field_name)) {
|
||||
this.transitionTo('adminCustomizeThemes.edit', wrapper.model.id, wrapper.target, fields[0]);
|
||||
return;
|
||||
}
|
||||
controller.set("model", wrapper.model);
|
||||
controller.setTargetName(wrapper.target || "common");
|
||||
controller.set("fieldName", wrapper.field_name || "scss");
|
||||
|
@ -0,0 +1,5 @@
|
||||
export default Discourse.Route.extend({
|
||||
activate() {
|
||||
this.controllerFor('admin-dashboard-next').fetchDashboard();
|
||||
}
|
||||
});
|
@ -1,24 +0,0 @@
|
||||
import Group from 'discourse/models/group';
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
|
||||
model(params) {
|
||||
if (params.name === 'new') {
|
||||
return Group.create({ automatic: false, visibility_level: 0 });
|
||||
}
|
||||
|
||||
const group = this.modelFor('adminGroupsType').findBy('name', params.name);
|
||||
|
||||
if (!group) { return this.transitionTo('adminGroups.index'); }
|
||||
|
||||
return group;
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.set("model", model);
|
||||
controller.set("model.usernames", null);
|
||||
controller.set("savingStatus", "");
|
||||
model.findMembers();
|
||||
}
|
||||
|
||||
});
|
@ -1,13 +0,0 @@
|
||||
import Group from 'discourse/models/group';
|
||||
|
||||
export default Ember.Route.extend({
|
||||
model() {
|
||||
return Group.findAll().then(groups => {
|
||||
return groups.filter(g => !g.get('automatic'));
|
||||
});
|
||||
},
|
||||
|
||||
setupController(controller, groups) {
|
||||
controller.setProperties({ groups, groupId: null, users: null });
|
||||
}
|
||||
});
|
@ -1,5 +0,0 @@
|
||||
export default Discourse.Route.extend({
|
||||
redirect: function() {
|
||||
this.transitionTo("adminGroupsType", "custom");
|
||||
}
|
||||
});
|
@ -1,15 +0,0 @@
|
||||
import Group from 'discourse/models/group';
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
model(params) {
|
||||
this.set("type", params.type);
|
||||
return Group.findAll().then(function(groups) {
|
||||
return groups.filterBy("type", params.type);
|
||||
});
|
||||
},
|
||||
|
||||
setupController(controller, model){
|
||||
controller.set("type", this.get("type"));
|
||||
controller.set("model", model);
|
||||
}
|
||||
});
|
@ -8,12 +8,15 @@ export default Ember.Route.extend({
|
||||
const controller = this.controllerFor('adminSiteSettings');
|
||||
this.transitionTo('adminSiteSettingsCategory', 'plugins').then(() => {
|
||||
if (plugin) {
|
||||
const siteSettingFilter = plugin.get('enabled_setting_filter');
|
||||
const match = /^(.*)_enabled/.exec(plugin.get('enabled_setting'));
|
||||
if (match[1]) {
|
||||
const filter = siteSettingFilter || match[1];
|
||||
|
||||
if (filter) {
|
||||
// filterContent() is normally on a debounce from typing.
|
||||
// Because we don't want the default of "All Results", we tell it
|
||||
// to skip the next debounce.
|
||||
controller.set('filter', match[1]);
|
||||
controller.set('filter', filter);
|
||||
controller.set('_skipBounce', true);
|
||||
controller.filterContentNow('plugins');
|
||||
}
|
||||
@ -22,4 +25,3 @@ export default Ember.Route.extend({
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -12,8 +12,8 @@ export default Discourse.Route.extend({
|
||||
model: model,
|
||||
categoryId: (model.get('category_id') || 'all'),
|
||||
groupId: model.get('group_id'),
|
||||
startDate: moment(model.get('start_date')).format('YYYY-MM-DD'),
|
||||
endDate: moment(model.get('end_date')).format('YYYY-MM-DD')
|
||||
startDate: moment(model.get('start_date')).utc().format('YYYY-MM-DD'),
|
||||
endDate: moment(model.get('end_date')).utc().format('YYYY-MM-DD')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -1,6 +1,7 @@
|
||||
export default function() {
|
||||
this.route('admin', { resetNamespace: true }, function() {
|
||||
this.route('dashboard', { path: '/' });
|
||||
this.route('dashboard', { path: '/dashboard-old' });
|
||||
this.route('dashboardNext', { path: '/' });
|
||||
this.route('adminSiteSettings', { path: '/site_settings', resetNamespace: true }, function() {
|
||||
this.route('adminSiteSettingsCategory', { path: 'category/:category_id', resetNamespace: true} );
|
||||
});
|
||||
@ -76,14 +77,6 @@ export default function() {
|
||||
});
|
||||
});
|
||||
|
||||
this.route('adminGroups', { path: '/groups', resetNamespace: true }, function() {
|
||||
this.route('bulk');
|
||||
this.route('bulkComplete', { path: 'bulk-complete' });
|
||||
this.route('adminGroupsType', { path: '/:type', resetNamespace: true }, function() {
|
||||
this.route('adminGroup', { path: '/:name', resetNamespace: true });
|
||||
});
|
||||
});
|
||||
|
||||
this.route('adminUsers', { path: '/users', resetNamespace: true }, function() {
|
||||
this.route('adminUser', { path: '/:user_id/:username', resetNamespace: true }, function() {
|
||||
this.route('badges');
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
import { fillMissingDates } from 'discourse/lib/utilities';
|
||||
import { translateResults } from "discourse/lib/search";
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
queryParams: {
|
||||
@ -15,6 +17,16 @@ export default Discourse.Route.extend({
|
||||
search_type: params.searchType
|
||||
}
|
||||
}).then(json => {
|
||||
// Add zero values for missing dates
|
||||
if (json.term.data.length > 0) {
|
||||
const startDate = (json.term.period === "all") ? moment(json.term.data[0].x).format('YYYY-MM-DD') : moment(json.term.start_date).format('YYYY-MM-DD');
|
||||
const endDate = moment(json.term.end_date).format('YYYY-MM-DD');
|
||||
json.term.data = fillMissingDates(json.term.data, startDate, endDate);
|
||||
}
|
||||
if (json.term.search_result) {
|
||||
json.term.search_result = translateResults(json.term.search_result);
|
||||
}
|
||||
|
||||
const model = Ember.Object.create({ type: "search_log_term" });
|
||||
model.setProperties(json.term);
|
||||
return model;
|
||||
|
@ -21,15 +21,5 @@ export default Discourse.Route.extend({
|
||||
availableGroups: this._availableGroups,
|
||||
model
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
viewActionLogs(username) {
|
||||
const controller = this.controllerFor('adminLogs.staffActionLogs');
|
||||
this.transitionTo('adminLogs.staffActionLogs').then(() => {
|
||||
controller.set('filters', Ember.Object.create());
|
||||
controller._changeFilters({ target_user: username });
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
export default Discourse.Route.extend({
|
||||
model: function() {
|
||||
model() {
|
||||
return this.modelFor('adminUser');
|
||||
}
|
||||
});
|
||||
|
@ -17,10 +17,26 @@ export default Ember.Service.extend({
|
||||
this.siteSettings = getOwner(this).lookup('site-settings:main');
|
||||
},
|
||||
|
||||
showActionLogs(target, filters) {
|
||||
const controller = getOwner(target).lookup('controller:adminLogs.staffActionLogs');
|
||||
target.transitionToRoute('adminLogs.staffActionLogs').then(() => {
|
||||
controller.set('filters', Ember.Object.create());
|
||||
controller._changeFilters(filters);
|
||||
});
|
||||
},
|
||||
|
||||
showFlagsReceived(user) {
|
||||
showModal(`admin-flags-received`, { admin: true, model: user });
|
||||
},
|
||||
|
||||
checkSpammer(userId) {
|
||||
return AdminUser.find(userId).then(au => this.spammerDetails(au));
|
||||
},
|
||||
|
||||
deleteUser(id) {
|
||||
AdminUser.find(id).then(user => user.destroy({ deletePosts: true }));
|
||||
},
|
||||
|
||||
spammerDetails(adminUser) {
|
||||
return {
|
||||
deleteUser: () => this._deleteSpammer(adminUser),
|
||||
@ -36,17 +52,20 @@ export default Ember.Service.extend({
|
||||
modalClass: `${type}-user-modal`
|
||||
});
|
||||
if (opts.post) {
|
||||
controller.set('post', opts.post);
|
||||
controller.setProperties({
|
||||
post: opts.post,
|
||||
postEdit: opts.post.get('raw')
|
||||
});
|
||||
}
|
||||
|
||||
let promise = user.adminUserView ?
|
||||
return (user.adminUserView ?
|
||||
Ember.RSVP.resolve(user) :
|
||||
AdminUser.find(user.get('id'));
|
||||
|
||||
promise.then(loadedUser => {
|
||||
AdminUser.find(user.get('id'))
|
||||
).then(loadedUser => {
|
||||
controller.setProperties({
|
||||
user: loadedUser,
|
||||
loadingUser: false,
|
||||
before: opts.before,
|
||||
successCallback: opts.successCallback
|
||||
});
|
||||
});
|
||||
|
BIN
app/assets/javascripts/admin/templates/.dashboard_next.hbs.swl
Normal file
@ -3,7 +3,7 @@
|
||||
<div class="full-width">
|
||||
|
||||
<ul class="nav nav-pills">
|
||||
{{nav-item route='admin.dashboard' label='admin.dashboard.title'}}
|
||||
{{nav-item route='admin.dashboardNext' label='admin.dashboard.title'}}
|
||||
{{#if currentUser.admin}}
|
||||
{{nav-item route='adminSiteSettings' label='admin.site_settings.title'}}
|
||||
{{/if}}
|
||||
@ -12,7 +12,6 @@
|
||||
{{nav-item route='adminBadges' label='admin.badges.title'}}
|
||||
{{/if}}
|
||||
{{#if currentUser.admin}}
|
||||
{{nav-item route='adminGroups' label='admin.groups.title'}}
|
||||
{{nav-item route='adminEmail' label='admin.email.title'}}
|
||||
{{/if}}
|
||||
{{nav-item route='adminFlags' label='admin.flags.title'}}
|
||||
@ -25,7 +24,7 @@
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{nav-item route='adminPlugins' label='admin.plugins.title'}}
|
||||
{{plugin-outlet name="admin-menu" connectorTagName="li"}}
|
||||
{{plugin-outlet name="admin-menu" connectorTagName="" tagName=""}}
|
||||
</ul>
|
||||
|
||||
<div class='boxed white admin-content'>
|
||||
|
@ -1,12 +1,12 @@
|
||||
<div class='admin-backups'>
|
||||
<div class="admin-controls">
|
||||
<div class="span15">
|
||||
<nav>
|
||||
<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>
|
||||
</nav>
|
||||
<div class="pull-right">
|
||||
{{#if model.canRollback}}
|
||||
{{d-button action="rollback"
|
||||
|
@ -1,4 +1,4 @@
|
||||
{{#d-section class="current-badge span13"}}
|
||||
{{#d-section class="current-badge content-body"}}
|
||||
<p>{{i18n 'admin.badges.none_selected'}}</p>
|
||||
|
||||
<div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
{{#d-section class="current-badge span13"}}
|
||||
{{#d-section class="current-badge content-body"}}
|
||||
<form class="form-horizontal">
|
||||
<div>
|
||||
<label for="name">{{i18n 'admin.badges.name'}}</label>
|
||||
@ -18,7 +18,7 @@
|
||||
<div>
|
||||
<label for="image">{{i18n 'admin.badges.image'}}</label>
|
||||
{{input type="text" name="image" value=buffered.image}}
|
||||
<p class='help'>{{i18n 'admin.badges.icon_help'}}</p>
|
||||
<p class='help'>{{i18n 'admin.badges.image_help'}}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -144,7 +144,7 @@
|
||||
{{/d-section}}
|
||||
|
||||
{{#if grant_count}}
|
||||
<div class="span13 current-badge-actions">
|
||||
<div class="content-body current-badge-actions">
|
||||
<div>
|
||||
{{#link-to 'badges.show' this}}{{i18n 'badges.granted' count=grant_count}}{{/link-to}}
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div class="badges">
|
||||
|
||||
<div class='content-list span6'>
|
||||
<div class='content-list'>
|
||||
<h3>{{i18n 'admin.badges.title'}}</h3>
|
||||
<ul>
|
||||
{{#each model as |badge|}}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class='admin-controls'>
|
||||
<div class='span15'>
|
||||
<nav>
|
||||
<ul class="nav nav-pills">
|
||||
{{yield}}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
@ -5,18 +5,20 @@
|
||||
<a href="{{report.reportUrl}}">{{report.title}}</a>
|
||||
</td>
|
||||
|
||||
<td class="value">{{number report.todayCount}}</td>
|
||||
<td class="value">{{number report.todayCount ceil=true}}</td>
|
||||
|
||||
<td class="value {{report.yesterdayTrend}}" title={{report.yesterdayCountTitle}}>
|
||||
{{number report.yesterdayCount}} {{d-icon "caret-up" class="up"}} {{d-icon "caret-down" class="down"}}
|
||||
{{number report.yesterdayCount ceil=true}} {{d-icon report.yesterdayTrendIcon}}
|
||||
</td>
|
||||
|
||||
<td class="value {{report.sevenDayTrend}}" title={{report.sevenDayCountTitle}}>
|
||||
{{number report.lastSevenDaysCount}} {{d-icon "caret-up" class="up"}} {{d-icon "caret-down" class="down"}}
|
||||
<td class="value {{report.sevenDaysTrend}}" title={{report.sevenDaysCountTitle}}>
|
||||
{{number report.lastSevenDaysCount ceil=true}} {{d-icon report.sevenDaysTrendIcon}}
|
||||
</td>
|
||||
|
||||
<td class="value {{report.thirtyDayTrend}}" title={{report.thirtyDayCountTitle}}>
|
||||
{{number report.lastThirtyDaysCount}} {{d-icon "caret-up" class="up"}} {{d-icon "caret-down" class="down"}}
|
||||
<td class="value {{report.thirtyDaysTrend}}" title={{report.thirtyDaysCountTitle}}>
|
||||
{{number report.lastThirtyDaysCount ceil=true}} {{d-icon report.thirtyDaysTrendIcon}}
|
||||
</td>
|
||||
|
||||
<td class="value">{{number report.total}}</td>
|
||||
{{#if allTime}}
|
||||
<td class="value">{{number report.total}}</td>
|
||||
{{/if}}
|
||||
|
@ -0,0 +1,17 @@
|
||||
{{#if model.sortedData}}
|
||||
<table class="table report {{model.type}}">
|
||||
<tr>
|
||||
<th>{{model.xaxis}}</th>
|
||||
<th>{{model.yaxis}}</th>
|
||||
</tr>
|
||||
|
||||
{{#each model.sortedData as |row|}}
|
||||
<tr>
|
||||
<td class="x-value">{{row.x}}</td>
|
||||
<td>
|
||||
{{row.y}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
{{/if}}
|