Merge master

This commit is contained in:
Neil Lalonde 2018-05-31 18:19:36 -04:00
commit b675f5fa6b
11049 changed files with 111860 additions and 63867 deletions

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -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
[![Build Status](https://api.travis-ci.org/discourse/discourse.svg?branch=master)](https://travis-ci.org/discourse/discourse)
[![Code Climate](https://codeclimate.com/github/discourse/discourse.svg)](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 &ndash; 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
View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1000 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

View File

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

View File

@ -1,3 +1,3 @@
export default Ember.Component.extend({
classNames: ['latest-topic-list']
tagName: ''
});

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
export default Ember.Component.extend({
classNames: ['flag-user-lists']
});

View File

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

View File

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

View File

@ -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();
});
}
}
}
});

View File

@ -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());
});

View File

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

View File

@ -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'));
}
});

View File

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

View 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;
});
}
});

View File

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

View File

@ -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'));
}
});

View File

@ -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());
});
}

View File

@ -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({
});
}
}
});

View File

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

View File

@ -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}`);
}
});

View File

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

View File

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

View File

@ -1,4 +0,0 @@
export default Ember.Controller.extend({
adminGroupsBulk: Ember.inject.controller(),
bulkAddResponse: Ember.computed.alias('adminGroupsBulk.bulkAddResponse')
});

View File

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

View File

@ -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'}`;
}
});

View File

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

View File

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

View File

@ -1,7 +1,7 @@
export default Ember.Controller.extend({
loading: false,
term: null,
period: "yearly",
period: "quarterly",
searchType: "all",
searchTypeOptions: [

View File

@ -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 : [];
}
});

View File

@ -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();
}

View File

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

View File

@ -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'));
},

View File

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

View File

@ -46,6 +46,10 @@ export default Ember.Controller.extend({
actions: {
clearFilter() {
this.setProperties({ filter: '' });
},
toggleMenu() {
$('.admin-detail').toggleClass('mobile-closed mobile-open');
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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);
},
});

View File

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

View 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();
}
}
});

View 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')
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {

View File

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

View File

@ -0,0 +1,3 @@
import Setting from 'admin/mixins/setting-object';
export default Discourse.Model.extend(Setting, {});

View File

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

View File

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

View File

@ -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");
}
});

View File

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

View File

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

View File

@ -0,0 +1,5 @@
export default Discourse.Route.extend({
activate() {
this.controllerFor('admin-dashboard-next').fetchDashboard();
}
});

View File

@ -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();
}
});

View File

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

View File

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

View File

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

View File

@ -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({
}
}
});

View File

@ -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')
});
}
});

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
export default Discourse.Route.extend({
model: function() {
model() {
return this.modelFor('adminUser');
}
});

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
<div class='admin-controls'>
<div class='span15'>
<nav>
<ul class="nav nav-pills">
{{yield}}
</ul>
</div>
</nav>
</div>

View File

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

View File

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

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