Version bump
This commit is contained in:
commit
a33dc7403f
@ -6,7 +6,6 @@ env:
|
||||
- RUBY_GC_MALLOC_LIMIT=50000000
|
||||
matrix:
|
||||
- "RAILS_MASTER=0"
|
||||
- "RAILS42=1"
|
||||
- "RAILS_MASTER=1"
|
||||
|
||||
addons:
|
||||
@ -21,7 +20,6 @@ addons:
|
||||
matrix:
|
||||
allow_failures:
|
||||
- env: "RAILS_MASTER=1"
|
||||
- env: "RAILS42=1"
|
||||
- rvm: rbx-2
|
||||
fast_finish: true
|
||||
|
||||
@ -51,7 +49,6 @@ before_script:
|
||||
- bundle exec rake db:create db:migrate
|
||||
|
||||
install:
|
||||
- bash -c "if [ '$RAILS42' == '1' ]; then bundle update --retry=3 --jobs=3 rails rails-observers; fi"
|
||||
- bash -c "if [ '$RAILS_MASTER' == '1' ]; then bundle update --retry=3 --jobs=3 arel rails rails-observers seed-fu; fi"
|
||||
- bash -c "if [ '$RAILS_MASTER' == '0' ]; then bundle install --without development --deployment --retry=3 --jobs=3; fi"
|
||||
|
||||
|
||||
20
Gemfile
20
Gemfile
@ -6,30 +6,19 @@ def rails_master?
|
||||
ENV["RAILS_MASTER"] == '1'
|
||||
end
|
||||
|
||||
def rails_42?
|
||||
ENV["RAILS42"] == '1'
|
||||
end
|
||||
|
||||
if rails_master?
|
||||
gem 'arel', git: 'https://github.com/rails/arel.git'
|
||||
gem 'rails', git: 'https://github.com/rails/rails.git'
|
||||
gem 'rails-observers', git: 'https://github.com/rails/rails-observers.git'
|
||||
gem 'seed-fu', git: 'https://github.com/SamSaffron/seed-fu.git', branch: 'discourse'
|
||||
elsif rails_42?
|
||||
gem 'rails', '~> 4.2.1'
|
||||
gem 'rails-observers', git: 'https://github.com/rails/rails-observers.git'
|
||||
gem 'seed-fu', '~> 2.3.5'
|
||||
else
|
||||
gem 'rails', '~> 4.1.10'
|
||||
gem 'rails', '~> 4.2'
|
||||
gem 'rails-observers'
|
||||
gem 'seed-fu', '~> 2.3.3'
|
||||
gem 'seed-fu', '~> 2.3.5'
|
||||
end
|
||||
|
||||
# Rails 4.1.6+ will relax the mail gem version requirement to `~> 2.5, >= 2.5.4`.
|
||||
# However, mail gem 2.6.x currently does not work with discourse because of the
|
||||
# reference to `Mail::RFC2822Parser` in `lib/email.rb`. This ensure discourse
|
||||
# would continue to work with Rails 4.1.6+ when it is released.
|
||||
gem 'mail', '~> 2.5.4'
|
||||
gem 'mail'
|
||||
gem 'mime-types', require: 'mime/types/columnar'
|
||||
|
||||
#gem 'redis-rails'
|
||||
gem 'hiredis'
|
||||
@ -48,7 +37,6 @@ gem 'babel-transpiler'
|
||||
gem 'message_bus'
|
||||
gem 'rails_multisite', path: 'vendor/gems/rails_multisite'
|
||||
|
||||
gem 'redcarpet', require: false
|
||||
gem 'fast_xs'
|
||||
|
||||
gem 'fast_xor'
|
||||
|
||||
290
Gemfile.lock
290
Gemfile.lock
@ -6,46 +6,53 @@ PATH
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actionmailer (4.1.10)
|
||||
actionpack (= 4.1.10)
|
||||
actionview (= 4.1.10)
|
||||
actionmailer (4.2.4)
|
||||
actionpack (= 4.2.4)
|
||||
actionview (= 4.2.4)
|
||||
activejob (= 4.2.4)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
actionpack (4.1.10)
|
||||
actionview (= 4.1.10)
|
||||
activesupport (= 4.1.10)
|
||||
rack (~> 1.5.2)
|
||||
rails-dom-testing (~> 1.0, >= 1.0.5)
|
||||
actionpack (4.2.4)
|
||||
actionview (= 4.2.4)
|
||||
activesupport (= 4.2.4)
|
||||
rack (~> 1.6)
|
||||
rack-test (~> 0.6.2)
|
||||
actionview (4.1.10)
|
||||
activesupport (= 4.1.10)
|
||||
rails-dom-testing (~> 1.0, >= 1.0.5)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||
actionview (4.2.4)
|
||||
activesupport (= 4.2.4)
|
||||
builder (~> 3.1)
|
||||
erubis (~> 2.7.0)
|
||||
rails-dom-testing (~> 1.0, >= 1.0.5)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||
active_model_serializers (0.8.3)
|
||||
activemodel (>= 3.0)
|
||||
activemodel (4.1.10)
|
||||
activesupport (= 4.1.10)
|
||||
activejob (4.2.4)
|
||||
activesupport (= 4.2.4)
|
||||
globalid (>= 0.3.0)
|
||||
activemodel (4.2.4)
|
||||
activesupport (= 4.2.4)
|
||||
builder (~> 3.1)
|
||||
activerecord (4.1.10)
|
||||
activemodel (= 4.1.10)
|
||||
activesupport (= 4.1.10)
|
||||
arel (~> 5.0.0)
|
||||
activesupport (4.1.10)
|
||||
i18n (~> 0.6, >= 0.6.9)
|
||||
activerecord (4.2.4)
|
||||
activemodel (= 4.2.4)
|
||||
activesupport (= 4.2.4)
|
||||
arel (~> 6.0)
|
||||
activesupport (4.2.4)
|
||||
i18n (~> 0.7)
|
||||
json (~> 1.7, >= 1.7.7)
|
||||
minitest (~> 5.1)
|
||||
thread_safe (~> 0.1)
|
||||
thread_safe (~> 0.3, >= 0.3.4)
|
||||
tzinfo (~> 1.1)
|
||||
annotate (2.6.6)
|
||||
activerecord (>= 2.3.0)
|
||||
rake (~> 10.4.2, >= 10.4.2)
|
||||
arel (5.0.1.20140414130214)
|
||||
aws-sdk (2.0.45)
|
||||
aws-sdk-resources (= 2.0.45)
|
||||
aws-sdk-core (2.0.45)
|
||||
builder (~> 3.0)
|
||||
annotate (2.6.10)
|
||||
activerecord (>= 3.2, <= 4.3)
|
||||
rake (~> 10.4)
|
||||
arel (6.0.3)
|
||||
aws-sdk (2.1.23)
|
||||
aws-sdk-resources (= 2.1.23)
|
||||
aws-sdk-core (2.1.23)
|
||||
jmespath (~> 1.0)
|
||||
multi_json (~> 1.0)
|
||||
aws-sdk-resources (2.0.45)
|
||||
aws-sdk-core (= 2.0.45)
|
||||
aws-sdk-resources (2.1.23)
|
||||
aws-sdk-core (= 2.1.23)
|
||||
babel-source (5.8.19)
|
||||
babel-transpiler (0.7.0)
|
||||
babel-source (>= 4.0, < 6)
|
||||
@ -60,22 +67,59 @@ GEM
|
||||
binding_of_caller (0.7.2)
|
||||
debug_inspector (>= 0.0.1)
|
||||
builder (3.2.2)
|
||||
byebug (5.0.0)
|
||||
columnize (= 0.9.0)
|
||||
celluloid (0.16.0)
|
||||
timers (~> 4.0.0)
|
||||
byebug (6.0.2)
|
||||
celluloid (0.17.1.2)
|
||||
bundler
|
||||
celluloid-essentials
|
||||
celluloid-extras
|
||||
celluloid-fsm
|
||||
celluloid-pool
|
||||
celluloid-supervision
|
||||
dotenv
|
||||
nenv
|
||||
rspec-logsplit (>= 0.1.2)
|
||||
timers (>= 4.1.1)
|
||||
celluloid-essentials (0.20.2.1)
|
||||
bundler
|
||||
dotenv
|
||||
nenv
|
||||
rspec-logsplit (>= 0.1.2)
|
||||
timers (>= 4.1.1)
|
||||
celluloid-extras (0.20.1)
|
||||
bundler
|
||||
dotenv
|
||||
nenv
|
||||
rspec-logsplit (>= 0.1.2)
|
||||
timers (>= 4.1.1)
|
||||
celluloid-fsm (0.20.1)
|
||||
bundler
|
||||
dotenv
|
||||
nenv
|
||||
rspec-logsplit (>= 0.1.2)
|
||||
timers (>= 4.1.1)
|
||||
celluloid-pool (0.20.1)
|
||||
bundler
|
||||
dotenv
|
||||
nenv
|
||||
rspec-logsplit (>= 0.1.2)
|
||||
timers (>= 4.1.1)
|
||||
celluloid-supervision (0.20.1.1)
|
||||
bundler
|
||||
dotenv
|
||||
nenv
|
||||
rspec-logsplit (>= 0.1.2)
|
||||
timers (>= 4.1.1)
|
||||
certified (1.0.0)
|
||||
coderay (1.1.0)
|
||||
columnize (0.9.0)
|
||||
connection_pool (2.2.0)
|
||||
crass (1.0.1)
|
||||
daemons (1.2.2)
|
||||
crass (1.0.2)
|
||||
daemons (1.2.3)
|
||||
debug_inspector (0.0.2)
|
||||
diff-lcs (1.2.5)
|
||||
discourse-qunit-rails (0.0.8)
|
||||
railties
|
||||
docile (1.1.5)
|
||||
dotenv (1.0.2)
|
||||
dotenv (2.0.2)
|
||||
email_reply_parser (0.5.8)
|
||||
ember-data-source (1.0.0.beta.16.1)
|
||||
ember-source (~> 1.8)
|
||||
@ -91,15 +135,15 @@ GEM
|
||||
railties (>= 3.1)
|
||||
ember-source (1.12.1)
|
||||
erubis (2.7.0)
|
||||
eventmachine (1.0.7)
|
||||
excon (0.45.3)
|
||||
execjs (2.5.2)
|
||||
exifr (1.2.2)
|
||||
eventmachine (1.0.8)
|
||||
excon (0.45.4)
|
||||
execjs (2.6.0)
|
||||
exifr (1.2.3.1)
|
||||
fabrication (2.9.8)
|
||||
fakeweb (1.3.0)
|
||||
faraday (0.9.1)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
fast_blank (0.0.2)
|
||||
fast_blank (1.0.0)
|
||||
fast_stack (0.1.0)
|
||||
rake
|
||||
rake-compiler
|
||||
@ -108,24 +152,25 @@ GEM
|
||||
rake-compiler
|
||||
fast_xs (0.8.0)
|
||||
fastimage_discourse (1.6.6)
|
||||
ffi (1.9.6)
|
||||
ffi (1.9.10)
|
||||
flamegraph (0.1.0)
|
||||
fast_stack
|
||||
foreman (0.77.0)
|
||||
dotenv (~> 1.0.2)
|
||||
foreman (0.78.0)
|
||||
thor (~> 0.19.1)
|
||||
fspath (2.1.1)
|
||||
gctools (0.2.3)
|
||||
given_core (3.5.4)
|
||||
sorcerer (>= 0.3.7)
|
||||
globalid (0.3.6)
|
||||
activesupport (>= 4.1.0)
|
||||
guess_html_encoding (0.0.11)
|
||||
handlebars-source (2.0.0)
|
||||
hashie (3.4.0)
|
||||
highline (1.7.1)
|
||||
hashie (3.4.2)
|
||||
highline (1.7.7)
|
||||
hike (1.2.3)
|
||||
hiredis (0.6.0)
|
||||
hitimes (1.2.2)
|
||||
htmlentities (4.3.3)
|
||||
hitimes (1.2.3)
|
||||
htmlentities (4.3.4)
|
||||
i18n (0.7.0)
|
||||
image_optim (0.20.2)
|
||||
exifr (~> 1.1, >= 1.1.3)
|
||||
@ -135,46 +180,47 @@ GEM
|
||||
progress (~> 3.0, >= 3.0.1)
|
||||
image_size (1.4.1)
|
||||
in_threads (1.3.1)
|
||||
jmespath (1.0.2)
|
||||
multi_json (~> 1.0)
|
||||
jmespath (1.1.3)
|
||||
jquery-rails (3.1.2)
|
||||
railties (>= 3.0, < 5.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (1.8.3)
|
||||
jwt (1.3.0)
|
||||
kgio (2.9.3)
|
||||
jwt (1.5.1)
|
||||
kgio (2.10.0)
|
||||
librarian (0.1.2)
|
||||
highline
|
||||
thor (~> 0.15)
|
||||
libv8 (3.16.14.7)
|
||||
libv8 (3.16.14.11)
|
||||
listen (0.7.3)
|
||||
logster (1.0.0.3.pre)
|
||||
loofah (2.0.3)
|
||||
nokogiri (>= 1.5.9)
|
||||
lru_redux (1.1.0)
|
||||
mail (2.5.4)
|
||||
mime-types (~> 1.16)
|
||||
treetop (~> 1.4.8)
|
||||
memory_profiler (0.9.3)
|
||||
mail (2.6.3)
|
||||
mime-types (>= 1.16, < 3)
|
||||
memory_profiler (0.9.4)
|
||||
message_bus (1.0.16)
|
||||
rack (>= 1.1.3)
|
||||
redis
|
||||
metaclass (0.0.4)
|
||||
method_source (0.8.2)
|
||||
mime-types (1.25.1)
|
||||
mime-types (2.6.2)
|
||||
mini_portile (0.6.2)
|
||||
minitest (5.6.1)
|
||||
minitest (5.8.0)
|
||||
mocha (1.1.0)
|
||||
metaclass (~> 0.0.1)
|
||||
mock_redis (0.14.0)
|
||||
mock_redis (0.15.2)
|
||||
moneta (0.8.0)
|
||||
msgpack (0.5.11)
|
||||
msgpack (0.6.2)
|
||||
multi_json (1.11.2)
|
||||
multi_xml (0.5.5)
|
||||
multipart-post (2.0.0)
|
||||
mustache (1.0.2)
|
||||
nenv (0.2.0)
|
||||
netrc (0.10.3)
|
||||
nokogiri (1.6.6.2)
|
||||
mini_portile (~> 0.6.0)
|
||||
nokogumbo (1.2.0)
|
||||
nokogumbo (1.4.1)
|
||||
nokogiri
|
||||
oauth (0.4.7)
|
||||
oauth2 (1.0.0)
|
||||
@ -183,11 +229,11 @@ GEM
|
||||
multi_json (~> 1.3)
|
||||
multi_xml (~> 0.5)
|
||||
rack (~> 1.2)
|
||||
oj (2.12.9)
|
||||
oj (2.12.14)
|
||||
omniauth (1.2.2)
|
||||
hashie (>= 1.2, < 4)
|
||||
rack (~> 1.0)
|
||||
omniauth-facebook (2.0.0)
|
||||
omniauth-facebook (2.0.1)
|
||||
omniauth-oauth2 (~> 1.2)
|
||||
omniauth-github-discourse (1.1.2)
|
||||
omniauth (~> 1.0)
|
||||
@ -195,20 +241,18 @@ GEM
|
||||
omniauth-google-oauth2 (0.2.5)
|
||||
omniauth (> 1.0)
|
||||
omniauth-oauth2 (~> 1.1)
|
||||
omniauth-oauth (1.0.1)
|
||||
omniauth-oauth (1.1.0)
|
||||
oauth
|
||||
omniauth (~> 1.0)
|
||||
omniauth-oauth2 (1.2.0)
|
||||
faraday (>= 0.8, < 0.10)
|
||||
multi_json (~> 1.3)
|
||||
omniauth-oauth2 (1.3.1)
|
||||
oauth2 (~> 1.0)
|
||||
omniauth (~> 1.2)
|
||||
omniauth-openid (1.0.1)
|
||||
omniauth (~> 1.0)
|
||||
rack-openid (~> 1.3.1)
|
||||
omniauth-twitter (1.0.1)
|
||||
multi_json (~> 1.3)
|
||||
omniauth-oauth (~> 1.0)
|
||||
omniauth-twitter (1.2.1)
|
||||
json (~> 1.3)
|
||||
omniauth-oauth (~> 1.1)
|
||||
onebox (1.5.26)
|
||||
moneta (~> 0.8)
|
||||
multi_json (~> 1.11)
|
||||
@ -217,8 +261,7 @@ GEM
|
||||
openid-redis-store (0.0.2)
|
||||
redis
|
||||
ruby-openid
|
||||
pg (0.18.1)
|
||||
polyglot (0.3.5)
|
||||
pg (0.18.3)
|
||||
progress (3.1.0)
|
||||
pry (0.10.1)
|
||||
coderay (~> 1.1.0)
|
||||
@ -226,13 +269,12 @@ GEM
|
||||
slop (~> 3.4)
|
||||
pry-nav (0.2.4)
|
||||
pry (>= 0.9.10, < 0.11.0)
|
||||
pry-rails (0.3.3)
|
||||
pry-rails (0.3.4)
|
||||
pry (>= 0.9.10)
|
||||
puma (2.11.1)
|
||||
rack (>= 1.1, < 2.0)
|
||||
puma (2.14.0)
|
||||
r2 (0.2.5)
|
||||
rack (1.5.5)
|
||||
rack-mini-profiler (0.9.6)
|
||||
rack (1.6.4)
|
||||
rack-mini-profiler (0.9.7)
|
||||
rack (>= 1.1.3)
|
||||
rack-openid (1.3.1)
|
||||
rack (>= 1.1.0)
|
||||
@ -241,39 +283,47 @@ GEM
|
||||
rack
|
||||
rack-test (0.6.3)
|
||||
rack (>= 1.0)
|
||||
rails (4.1.10)
|
||||
actionmailer (= 4.1.10)
|
||||
actionpack (= 4.1.10)
|
||||
actionview (= 4.1.10)
|
||||
activemodel (= 4.1.10)
|
||||
activerecord (= 4.1.10)
|
||||
activesupport (= 4.1.10)
|
||||
rails (4.2.4)
|
||||
actionmailer (= 4.2.4)
|
||||
actionpack (= 4.2.4)
|
||||
actionview (= 4.2.4)
|
||||
activejob (= 4.2.4)
|
||||
activemodel (= 4.2.4)
|
||||
activerecord (= 4.2.4)
|
||||
activesupport (= 4.2.4)
|
||||
bundler (>= 1.3.0, < 2.0)
|
||||
railties (= 4.1.10)
|
||||
sprockets-rails (~> 2.0)
|
||||
railties (= 4.2.4)
|
||||
sprockets-rails
|
||||
rails-deprecated_sanitizer (1.0.3)
|
||||
activesupport (>= 4.2.0.alpha)
|
||||
rails-dom-testing (1.0.7)
|
||||
activesupport (>= 4.2.0.beta, < 5.0)
|
||||
nokogiri (~> 1.6.0)
|
||||
rails-deprecated_sanitizer (>= 1.0.1)
|
||||
rails-html-sanitizer (1.0.2)
|
||||
loofah (~> 2.0)
|
||||
rails-observers (0.1.2)
|
||||
activemodel (~> 4.0)
|
||||
railties (4.1.10)
|
||||
actionpack (= 4.1.10)
|
||||
activesupport (= 4.1.10)
|
||||
railties (4.2.4)
|
||||
actionpack (= 4.2.4)
|
||||
activesupport (= 4.2.4)
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.18.1, < 2.0)
|
||||
raindrops (0.13.0)
|
||||
raindrops (0.15.0)
|
||||
rake (10.4.2)
|
||||
rake-compiler (0.9.4)
|
||||
rake-compiler (0.9.5)
|
||||
rake
|
||||
rb-fsevent (0.9.4)
|
||||
rb-fsevent (0.9.6)
|
||||
rb-inotify (0.9.5)
|
||||
ffi (>= 0.5.0)
|
||||
rbtrace (0.4.7)
|
||||
ffi (>= 1.0.6)
|
||||
msgpack (>= 0.4.3)
|
||||
trollop (>= 1.16.2)
|
||||
redcarpet (3.2.2)
|
||||
redis (3.2.1)
|
||||
redis-namespace (1.5.2)
|
||||
redis (~> 3.0, >= 3.0.4)
|
||||
ref (1.0.5)
|
||||
ref (2.0.0)
|
||||
rest-client (1.7.2)
|
||||
mime-types (>= 1.16, < 3.0)
|
||||
netrc (~> 0.7)
|
||||
@ -291,6 +341,7 @@ GEM
|
||||
rspec-given (3.5.4)
|
||||
given_core (= 3.5.4)
|
||||
rspec (>= 2.12)
|
||||
rspec-logsplit (0.1.3)
|
||||
rspec-mocks (3.2.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.2.0)
|
||||
@ -304,14 +355,14 @@ GEM
|
||||
rspec-support (~> 3.2.0)
|
||||
rspec-support (3.2.2)
|
||||
rtlit (0.0.5)
|
||||
ruby-openid (2.5.0)
|
||||
ruby-openid (2.7.0)
|
||||
ruby-readability (0.7.0)
|
||||
guess_html_encoding (>= 0.0.4)
|
||||
nokogiri (>= 1.6.0)
|
||||
sanitize (3.1.2)
|
||||
crass (~> 1.0.1)
|
||||
sanitize (4.0.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.4.4)
|
||||
nokogumbo (= 1.2.0)
|
||||
nokogumbo (= 1.4.1)
|
||||
sass (3.2.19)
|
||||
sass-rails (4.0.5)
|
||||
railties (>= 4.0.0, < 5.0)
|
||||
@ -327,8 +378,8 @@ GEM
|
||||
shoulda-context (1.2.1)
|
||||
shoulda-matchers (2.7.0)
|
||||
activesupport (>= 3.0.0)
|
||||
sidekiq (3.4.2)
|
||||
celluloid (~> 0.16.0)
|
||||
sidekiq (3.5.0)
|
||||
celluloid (~> 0.17.0)
|
||||
connection_pool (~> 2.2, >= 2.2.0)
|
||||
json (~> 1.0)
|
||||
redis (~> 3.2, >= 3.2.1)
|
||||
@ -336,15 +387,15 @@ GEM
|
||||
sidekiq-statistic (1.1.0)
|
||||
sidekiq (~> 3.3, >= 3.3.4)
|
||||
simple-rss (1.3.1)
|
||||
simplecov (0.9.1)
|
||||
simplecov (0.10.0)
|
||||
docile (~> 1.1.0)
|
||||
multi_json (~> 1.0)
|
||||
simplecov-html (~> 0.8.0)
|
||||
simplecov-html (0.8.0)
|
||||
sinatra (1.4.5)
|
||||
json (~> 1.8)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.0)
|
||||
sinatra (1.4.6)
|
||||
rack (~> 1.4)
|
||||
rack-protection (~> 1.4)
|
||||
tilt (~> 1.3, >= 1.3.4)
|
||||
tilt (>= 1.3, < 3)
|
||||
slop (3.6.0)
|
||||
sorcerer (1.0.2)
|
||||
spork (1.0.0rc4)
|
||||
@ -364,29 +415,26 @@ GEM
|
||||
therubyracer (0.12.2)
|
||||
libv8 (~> 3.16.14.0)
|
||||
ref
|
||||
thin (1.6.3)
|
||||
thin (1.6.4)
|
||||
daemons (~> 1.0, >= 1.0.9)
|
||||
eventmachine (~> 1.0)
|
||||
eventmachine (~> 1.0, >= 1.0.4)
|
||||
rack (~> 1.0)
|
||||
thor (0.19.1)
|
||||
thread_safe (0.3.5)
|
||||
tilt (1.4.1)
|
||||
timecop (0.7.3)
|
||||
timers (4.0.1)
|
||||
timecop (0.8.0)
|
||||
timers (4.1.1)
|
||||
hitimes
|
||||
treetop (1.4.15)
|
||||
polyglot
|
||||
polyglot (>= 0.3.1)
|
||||
trollop (2.1.1)
|
||||
tzinfo (1.2.2)
|
||||
thread_safe (~> 0.1)
|
||||
uglifier (2.7.1)
|
||||
uglifier (2.7.2)
|
||||
execjs (>= 0.3.0)
|
||||
json (>= 1.8.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.6)
|
||||
unicorn (4.8.3)
|
||||
unicorn (4.9.0)
|
||||
kgio (~> 2.6)
|
||||
rack
|
||||
raindrops (~> 0.7)
|
||||
@ -427,9 +475,10 @@ DEPENDENCIES
|
||||
listen (= 0.7.3)
|
||||
logster
|
||||
lru_redux
|
||||
mail (~> 2.5.4)
|
||||
mail
|
||||
memory_profiler
|
||||
message_bus
|
||||
mime-types
|
||||
minitest
|
||||
mocha
|
||||
mock_redis
|
||||
@ -453,14 +502,13 @@ DEPENDENCIES
|
||||
r2 (~> 0.2.5)
|
||||
rack-mini-profiler
|
||||
rack-protection
|
||||
rails (~> 4.1.10)
|
||||
rails (~> 4.2)
|
||||
rails-observers
|
||||
rails_multisite!
|
||||
rake
|
||||
rb-fsevent
|
||||
rb-inotify (~> 0.9)
|
||||
rbtrace
|
||||
redcarpet
|
||||
redis
|
||||
rest-client
|
||||
rinku
|
||||
@ -473,7 +521,7 @@ DEPENDENCIES
|
||||
sanitize
|
||||
sass
|
||||
sass-rails (~> 4.0.5)
|
||||
seed-fu (~> 2.3.3)
|
||||
seed-fu (~> 2.3.5)
|
||||
shoulda
|
||||
sidekiq
|
||||
sidekiq-statistic
|
||||
|
||||
@ -6,6 +6,11 @@ export default Ember.Component.extend({
|
||||
@computed('field')
|
||||
inputId(field) { return field.dasherize(); },
|
||||
|
||||
@computed('placeholder')
|
||||
placeholderValue(placeholder) {
|
||||
return placeholder ? I18n.t(placeholder) : null;
|
||||
},
|
||||
|
||||
@computed('field')
|
||||
translationKey(field) { return `admin.embedding.${field}`; },
|
||||
|
||||
|
||||
@ -18,14 +18,25 @@ Discourse.ScreenedIpAddressFormComponent = Ember.Component.extend({
|
||||
formSubmitted: false,
|
||||
actionName: 'block',
|
||||
|
||||
actionNames: function() {
|
||||
return [
|
||||
{id: 'block', name: I18n.t('admin.logs.screened_ips.actions.block')},
|
||||
{id: 'do_nothing', name: I18n.t('admin.logs.screened_ips.actions.do_nothing')},
|
||||
{id: 'allow_admin', name: I18n.t('admin.logs.screened_ips.actions.allow_admin')}
|
||||
];
|
||||
adminWhitelistEnabled: function() {
|
||||
return Discourse.SiteSettings.use_admin_ip_whitelist;
|
||||
}.property(),
|
||||
|
||||
actionNames: function() {
|
||||
if (this.get('adminWhitelistEnabled')) {
|
||||
return [
|
||||
{id: 'block', name: I18n.t('admin.logs.screened_ips.actions.block')},
|
||||
{id: 'do_nothing', name: I18n.t('admin.logs.screened_ips.actions.do_nothing')},
|
||||
{id: 'allow_admin', name: I18n.t('admin.logs.screened_ips.actions.allow_admin')}
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
{id: 'block', name: I18n.t('admin.logs.screened_ips.actions.block')},
|
||||
{id: 'do_nothing', name: I18n.t('admin.logs.screened_ips.actions.do_nothing')}
|
||||
];
|
||||
}
|
||||
}.property('adminWhitelistEnabled'),
|
||||
|
||||
actions: {
|
||||
submit: function() {
|
||||
if (!this.get('formSubmitted')) {
|
||||
|
||||
@ -5,7 +5,7 @@ export default Ember.ArrayController.extend({
|
||||
onlyOverridden: false,
|
||||
filtered: Ember.computed.notEmpty('filter'),
|
||||
|
||||
filterContentNow: function(category) {
|
||||
filterContentNow(category) {
|
||||
// If we have no content, don't bother filtering anything
|
||||
if (!!Ember.isEmpty(this.get('allSiteSettings'))) return;
|
||||
|
||||
@ -20,12 +20,13 @@ export default Ember.ArrayController.extend({
|
||||
return;
|
||||
}
|
||||
|
||||
const self = this,
|
||||
matchesGroupedByCategory = [{nameKey: 'all_results', name: I18n.t('admin.site_settings.categories.all_results'), siteSettings: []}];
|
||||
const all = {nameKey: 'all_results', name: I18n.t('admin.site_settings.categories.all_results'), siteSettings: []};
|
||||
const matchesGroupedByCategory = [all];
|
||||
|
||||
this.get('allSiteSettings').forEach(function(settingsCategory) {
|
||||
const matches = settingsCategory.siteSettings.filter(function(item) {
|
||||
if (self.get('onlyOverridden') && !item.get('overridden')) return false;
|
||||
const matches = [];
|
||||
this.get('allSiteSettings').forEach(settingsCategory => {
|
||||
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;
|
||||
@ -36,16 +37,20 @@ export default Ember.ArrayController.extend({
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (matches.length > 0) {
|
||||
matchesGroupedByCategory[0].siteSettings.pushObjects(matches);
|
||||
if (siteSettings.length > 0) {
|
||||
matches.pushObjects(siteSettings);
|
||||
matchesGroupedByCategory.pushObject({
|
||||
nameKey: settingsCategory.nameKey,
|
||||
name: I18n.t('admin.site_settings.categories.' + settingsCategory.nameKey),
|
||||
siteSettings: matches
|
||||
siteSettings,
|
||||
count: siteSettings.length
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
all.siteSettings.pushObjects(matches.slice(0, 30));
|
||||
all.count = matches.length;
|
||||
|
||||
this.set('model', matchesGroupedByCategory);
|
||||
this.transitionToRoute("adminSiteSettingsCategory", category || "all_results");
|
||||
},
|
||||
@ -60,10 +65,7 @@ export default Ember.ArrayController.extend({
|
||||
|
||||
actions: {
|
||||
clearFilter() {
|
||||
this.setProperties({
|
||||
filter: '',
|
||||
onlyOverridden: false
|
||||
});
|
||||
this.setProperties({ filter: '', onlyOverridden: false });
|
||||
},
|
||||
|
||||
toggleMenu() {
|
||||
|
||||
@ -56,12 +56,12 @@ export default Ember.ArrayController.extend({
|
||||
|
||||
var badges = [];
|
||||
this.get('badges').forEach(function(badge) {
|
||||
if (badge.get('multiple_grant') || !granted[badge.get('id')]) {
|
||||
if (badge.get('enabled') && (badge.get('multiple_grant') || !granted[badge.get('id')])) {
|
||||
badges.push(badge);
|
||||
}
|
||||
});
|
||||
|
||||
return _.sortBy(badges, "name");
|
||||
return _.sortBy(badges, badge => badge.get('displayName'));
|
||||
}.property('badges.@each', 'model.@each'),
|
||||
|
||||
/**
|
||||
|
||||
@ -387,16 +387,16 @@ const AdminUser = Discourse.User.extend({
|
||||
|
||||
const buttons = [{
|
||||
"label": I18n.t("composer.cancel"),
|
||||
"class": "cancel",
|
||||
"link": true
|
||||
}, {
|
||||
"label": I18n.t('admin.user.delete_dont_block'),
|
||||
"class": "btn",
|
||||
"callback": function(){ performDestroy(false); }
|
||||
"link": true
|
||||
}, {
|
||||
"label": '<i class="fa fa-exclamation-triangle"></i>' + I18n.t('admin.user.delete_and_block'),
|
||||
"class": "btn btn-danger",
|
||||
"callback": function(){ performDestroy(true); }
|
||||
}, {
|
||||
"label": I18n.t('admin.user.delete_dont_block'),
|
||||
"class": "btn btn-primary",
|
||||
"callback": function(){ performDestroy(false); }
|
||||
}];
|
||||
|
||||
bootbox.dialog(message, buttons, { "classes": "delete-user-modal" });
|
||||
|
||||
@ -11,6 +11,7 @@ Discourse.StaffActionLog = Discourse.Model.extend({
|
||||
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');
|
||||
@ -19,7 +20,7 @@ Discourse.StaffActionLog = Discourse.Model.extend({
|
||||
if (this.get('details')) formatted += Handlebars.Utils.escapeExpression(this.get('details')) + '<br/>';
|
||||
}
|
||||
return formatted;
|
||||
}.property('ip_address', 'email', 'topic_id', 'post_id'),
|
||||
}.property('ip_address', 'email', 'topic_id', 'post_id', 'category_id'),
|
||||
|
||||
format: function(label, propertyName) {
|
||||
if (this.get(propertyName)) {
|
||||
|
||||
@ -13,8 +13,8 @@
|
||||
{{/if}}
|
||||
{{#if currentUser.admin}}
|
||||
{{nav-item route='adminGroups' label='admin.groups.title'}}
|
||||
{{nav-item route='adminEmail' label='admin.email.title'}}
|
||||
{{/if}}
|
||||
{{nav-item route='adminEmail' label='admin.email.title'}}
|
||||
{{nav-item route='adminFlags' label='admin.flags.title'}}
|
||||
{{nav-item route='adminLogs' label='admin.logs.title'}}
|
||||
{{#if currentUser.admin}}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
</label>
|
||||
{{else}}
|
||||
<label for={{inputId}}>{{i18n translationKey}}</label>
|
||||
{{input value=value id=inputId}}
|
||||
{{input value=value id=inputId placeholder=placeholderValue}}
|
||||
{{/if}}
|
||||
|
||||
<div class='clearfix'></div>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<h3>{{unbound settingName}}</h3>
|
||||
</div>
|
||||
<div class="setting-value">
|
||||
{{component componentName setting=setting value=buffered.value validationMessage=validationMessage}}
|
||||
{{component componentName setting=setting value=buffered.value validationMessage=validationMessage}}
|
||||
</div>
|
||||
{{#if dirty}}
|
||||
<div class='setting-controls'>
|
||||
|
||||
@ -145,7 +145,7 @@
|
||||
<tr>
|
||||
<td>{{i18n 'admin.dashboard.uploads'}}</td>
|
||||
<td>{{disk_space.uploads_used}} ({{i18n 'admin.dashboard.space_free' size=disk_space.uploads_free}})</td>
|
||||
<td><a href="/admin/backups">{{i18n 'admin.dashboard.backups'}}</a></td>
|
||||
<td>{{#if currentUser.admin}}<a href="/admin/backups">{{i18n 'admin.dashboard.backups'}}</a>{{/if}}</td>
|
||||
<td>{{disk_space.backups_used}} ({{i18n 'admin.dashboard.space_free' size=disk_space.backups_free}})</td>
|
||||
</tr>
|
||||
{{/unless}}
|
||||
|
||||
@ -46,8 +46,13 @@
|
||||
<h3>{{i18n "admin.embedding.crawling_settings"}}</h3>
|
||||
<p class="description">{{i18n "admin.embedding.crawling_description"}}</p>
|
||||
|
||||
{{embedding-setting field="embed_whitelist_selector" value=embedding.embed_whitelist_selector}}
|
||||
{{embedding-setting field="embed_blacklist_selector" value=embedding.embed_blacklist_selector}}
|
||||
{{embedding-setting field="embed_whitelist_selector"
|
||||
value=embedding.embed_whitelist_selector
|
||||
placeholder="admin.embedding.whitelist_example"}}
|
||||
|
||||
{{embedding-setting field="embed_blacklist_selector"
|
||||
value=embedding.embed_blacklist_selector
|
||||
placeholder="admin.embedding.blacklist_example"}}
|
||||
</div>
|
||||
|
||||
<div class='embedding-secondary'>
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
{{#if length}}
|
||||
|
||||
{{d-button label="admin.plugins.change_settings"
|
||||
icon="gear"
|
||||
class='settings-button pull-right'
|
||||
action="showSettings"}}
|
||||
{{#if currentUser.admin}}
|
||||
{{d-button label="admin.plugins.change_settings"
|
||||
icon="gear"
|
||||
class='settings-button pull-right'
|
||||
action="showSettings"}}
|
||||
{{/if}}
|
||||
|
||||
<h3>{{i18n "admin.plugins.installed"}}</h3>
|
||||
|
||||
@ -41,11 +43,10 @@
|
||||
{{/if}}
|
||||
</td>
|
||||
<td>
|
||||
{{#if plugin.enabled_setting}}
|
||||
<button {{action "showSettings" plugin}} class="btn">
|
||||
{{fa-icon "gear"}}
|
||||
{{i18n "admin.plugins.change_settings_short"}}
|
||||
</button>
|
||||
{{#if currentUser.admin}}
|
||||
{{#if plugin.enabled_setting}}
|
||||
{{d-button action="showSettings" actionParam=plugin icon="gear" label="admin.plugins.change_settings_short"}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{{#if filteredContent}}
|
||||
<div class='form-horizontal settings'>
|
||||
{{#each setting in filteredContent}}
|
||||
{{#each filteredContent as |setting|}}
|
||||
{{site-setting setting=setting saveAction="saveSetting"}}
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
@ -6,9 +6,9 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class='controls'>
|
||||
<button {{action "toggleMenu"}} class="menu-toggle">{{fa-icon "bars"}}</button>
|
||||
{{d-button action="toggleMenu" class="menu-toggle" icon="bars"}}
|
||||
{{text-field value=filter placeholderKey="type_to_filter" class="no-blur"}}
|
||||
<button {{action "clearFilter"}} class="btn">{{i18n 'admin.site_settings.clear_filter'}}</button>
|
||||
{{d-button action="clearFilter" label="admin.site_settings.clear_filter"}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
{{#link-to 'adminSiteSettingsCategory' category.nameKey class=category.nameKey}}
|
||||
{{category.name}}
|
||||
{{#if filtered}}
|
||||
<span class="count">({{category.siteSettings.length}})</span>
|
||||
<span class="count">({{category.count}})</span>
|
||||
{{/if}}
|
||||
{{/link-to}}
|
||||
{{/link-to}}
|
||||
|
||||
@ -24,9 +24,7 @@ export default Ember.Object.extend({
|
||||
return "/";
|
||||
},
|
||||
|
||||
pathFor(store, type, findArgs) {
|
||||
let path = this.basePath(store, type, findArgs) + Ember.String.underscore(store.pluralize(type));
|
||||
|
||||
appendQueryParams(path, findArgs) {
|
||||
if (findArgs) {
|
||||
if (typeof findArgs === "object") {
|
||||
const queryString = Object.keys(findArgs)
|
||||
@ -34,17 +32,21 @@ export default Ember.Object.extend({
|
||||
.map(k => k + "=" + encodeURIComponent(findArgs[k]));
|
||||
|
||||
if (queryString.length) {
|
||||
path += "?" + queryString.join('&');
|
||||
return path + "?" + queryString.join('&');
|
||||
}
|
||||
} else {
|
||||
// It's serializable as a string if not an object
|
||||
path += "/" + findArgs;
|
||||
return path + "/" + findArgs;
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
},
|
||||
|
||||
pathFor(store, type, findArgs) {
|
||||
let path = this.basePath(store, type, findArgs) + Ember.String.underscore(store.pluralize(type));
|
||||
return this.appendQueryParams(path, findArgs);
|
||||
},
|
||||
|
||||
findAll(store, type) {
|
||||
return ajax(this.pathFor(store, type)).catch(rethrow);
|
||||
},
|
||||
|
||||
@ -56,7 +56,7 @@ export default Ember.Component.extend(StringBuffer, {
|
||||
if (postUrl) { key = key + "_with_url"; }
|
||||
|
||||
// TODO postUrl might be uninitialized? pick a good default
|
||||
buffer.push(" " + I18n.t(key, { icons: iconsHtml, postUrl: postUrl}) + ".");
|
||||
buffer.push(" " + I18n.t(key, { icons: iconsHtml, postUrl }) + ".");
|
||||
}
|
||||
|
||||
if (users.length === 0) {
|
||||
@ -83,6 +83,8 @@ export default Ember.Component.extend(StringBuffer, {
|
||||
autoUpdatingRelativeAge(new Date(post.get('postDeletedAt'))) +
|
||||
"</div>");
|
||||
}
|
||||
|
||||
buffer.push("<div class='clearfix'></div>");
|
||||
},
|
||||
|
||||
actionTypeById(actionTypeId) {
|
||||
|
||||
@ -36,7 +36,7 @@ export default ComboboxView.extend({
|
||||
|
||||
@computed("rootNone")
|
||||
none(rootNone) {
|
||||
if (Discourse.User.currentProp('staff') || Discourse.SiteSettings.allow_uncategorized_topics) {
|
||||
if (Discourse.SiteSettings.allow_uncategorized_topics) {
|
||||
if (rootNone) {
|
||||
return "category.none";
|
||||
} else {
|
||||
|
||||
@ -28,10 +28,9 @@ export default Ember.Component.extend({
|
||||
return '';
|
||||
},
|
||||
|
||||
@computed("title", "label")
|
||||
translatedTitle(title, label) {
|
||||
const text = title || label;
|
||||
if (text) return I18n.t(text);
|
||||
@computed("title")
|
||||
translatedTitle(title) {
|
||||
if (title) return I18n.t(title);
|
||||
},
|
||||
|
||||
click(e) {
|
||||
|
||||
@ -22,8 +22,14 @@ export default Ember.Component.extend({
|
||||
const it = this.get('notification');
|
||||
const badgeId = it.get("data.badge_id");
|
||||
if (badgeId) {
|
||||
const badgeName = it.get("data.badge_name");
|
||||
return Discourse.getURL('/badges/' + badgeId + '/' + badgeName.replace(/[^A-Za-z0-9_]+/g, '-').toLowerCase());
|
||||
var badgeSlug = it.get("data.badge_slug");
|
||||
|
||||
if (!badgeSlug) {
|
||||
const badgeName = it.get("data.badge_name");
|
||||
badgeSlug = badgeName.replace(/[^A-Za-z0-9_]+/g, '-').toLowerCase();
|
||||
}
|
||||
|
||||
return Discourse.getURL('/badges/' + badgeId + '/' + badgeSlug);
|
||||
}
|
||||
|
||||
const topicId = it.get('topic_id');
|
||||
|
||||
@ -1,21 +1,23 @@
|
||||
import { observes, on } from 'ember-addons/ember-computed-decorators';
|
||||
import loadScript from 'discourse/lib/load-script';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNameBindings: [':pagedown-editor'],
|
||||
|
||||
_initializeWmd: function() {
|
||||
const self = this;
|
||||
loadScript('defer/html-sanitizer-bundle').then(function() {
|
||||
self.$('.wmd-input').data('init', true);
|
||||
self._editor = Discourse.Markdown.createEditor({ containerElement: self.element });
|
||||
self._editor.run();
|
||||
Ember.run.scheduleOnce('afterRender', self, self._refreshPreview);
|
||||
@on("didInsertElement")
|
||||
_initializeWmd() {
|
||||
loadScript('defer/html-sanitizer-bundle').then(() => {
|
||||
this.$('.wmd-input').data('init', true);
|
||||
this._editor = Discourse.Markdown.createEditor({ containerElement: this.element });
|
||||
this._editor.run();
|
||||
Ember.run.scheduleOnce('afterRender', this, this._refreshPreview);
|
||||
});
|
||||
}.on('didInsertElement'),
|
||||
},
|
||||
|
||||
observeValue: function() {
|
||||
@observes("value")
|
||||
observeValue() {
|
||||
Ember.run.scheduleOnce('afterRender', this, this._refreshPreview);
|
||||
}.observes('value'),
|
||||
},
|
||||
|
||||
_refreshPreview() {
|
||||
this._editor.refreshPreview();
|
||||
|
||||
@ -1,22 +1,25 @@
|
||||
var MAX_SHOWN = 5;
|
||||
const MAX_SHOWN = 5;
|
||||
|
||||
import StringBuffer from 'discourse/mixins/string-buffer';
|
||||
import { iconHTML } from 'discourse/helpers/fa-icon';
|
||||
import property from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Em.Component.extend(StringBuffer, {
|
||||
const { get, isEmpty, Component } = Ember;
|
||||
|
||||
export default Component.extend(StringBuffer, {
|
||||
classNameBindings: [':gutter'],
|
||||
|
||||
rerenderTriggers: ['expanded'],
|
||||
|
||||
// Roll up links to avoid duplicates
|
||||
collapsed: function() {
|
||||
var seen = {},
|
||||
result = [],
|
||||
links = this.get('links');
|
||||
@property('links')
|
||||
collapsed(links) {
|
||||
const seen = {};
|
||||
const result = [];
|
||||
|
||||
if (!Em.isEmpty(links)) {
|
||||
if (!isEmpty(links)) {
|
||||
links.forEach(function(l) {
|
||||
var title = Em.get(l, 'title');
|
||||
const title = get(l, 'title');
|
||||
if (!seen[title]) {
|
||||
result.pushObject(l);
|
||||
seen[title] = true;
|
||||
@ -24,52 +27,52 @@ export default Em.Component.extend(StringBuffer, {
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}.property('links'),
|
||||
},
|
||||
|
||||
renderString: function(buffer) {
|
||||
var links = this.get('collapsed'),
|
||||
toRender = links,
|
||||
collapsed = !this.get('expanded');
|
||||
renderString(buffer) {
|
||||
const links = this.get('collapsed');
|
||||
const collapsed = !this.get('expanded');
|
||||
|
||||
if (!Em.isEmpty(links)) {
|
||||
if (!isEmpty(links)) {
|
||||
let toRender = links;
|
||||
if (collapsed) {
|
||||
toRender = toRender.slice(0, MAX_SHOWN);
|
||||
}
|
||||
|
||||
buffer.push("<ul class='post-links'>");
|
||||
toRender.forEach(function(l) {
|
||||
var direction = Em.get(l, 'reflection') ? 'inbound' : 'outbound',
|
||||
clicks = Em.get(l, 'clicks');
|
||||
const direction = get(l, 'reflection') ? 'inbound' : 'outbound',
|
||||
clicks = get(l, 'clicks');
|
||||
|
||||
buffer.push("<li><a href='" + Em.get(l, 'url') + "' class='track-link " + direction + "'>");
|
||||
buffer.push(`<li><a href='${get(l, 'url')}' class='track-link ${direction}'>`);
|
||||
|
||||
var title = Em.get(l, 'title');
|
||||
if (!Em.isEmpty(title)) {
|
||||
let title = get(l, 'title');
|
||||
if (!isEmpty(title)) {
|
||||
title = Handlebars.Utils.escapeExpression(title);
|
||||
buffer.push(Discourse.Emoji.unescape(title));
|
||||
}
|
||||
if (clicks) {
|
||||
buffer.push("<span class='badge badge-notification clicks'>" + clicks + "</span>");
|
||||
buffer.push(`<span class='badge badge-notification clicks'>${clicks}</span>`);
|
||||
}
|
||||
buffer.push("</a></li>");
|
||||
});
|
||||
|
||||
if (collapsed) {
|
||||
var remaining = links.length - MAX_SHOWN;
|
||||
const remaining = links.length - MAX_SHOWN;
|
||||
if (remaining > 0) {
|
||||
buffer.push("<li><a href class='toggle-more'>" + I18n.t('post.more_links', {count: remaining}) + "</a></li>");
|
||||
buffer.push(`<li><a href class='toggle-more'>${I18n.t('post.more_links', {count: remaining})}</a></li>`);
|
||||
}
|
||||
}
|
||||
buffer.push('</ul>');
|
||||
}
|
||||
|
||||
if (this.get('canReplyAsNewTopic')) {
|
||||
buffer.push("<a href class='reply-new'>" + iconHTML('plus') + I18n.t('post.reply_as_new_topic') + "</a>");
|
||||
buffer.push(`<a href class='reply-new'>${iconHTML('plus')}${I18n.t('post.reply_as_new_topic')}</a>`);
|
||||
}
|
||||
},
|
||||
|
||||
click: function(e) {
|
||||
var $target = $(e.target);
|
||||
click(e) {
|
||||
const $target = $(e.target);
|
||||
if ($target.hasClass('toggle-more')) {
|
||||
this.toggleProperty('expanded');
|
||||
return false;
|
||||
|
||||
@ -5,7 +5,7 @@ export default Ember.Component.extend(StringBuffer, {
|
||||
|
||||
renderString(buffer) {
|
||||
const users = this.get('users');
|
||||
if (users && users.length > 0) {
|
||||
if (users && users.get('length') > 0) {
|
||||
buffer.push("<div class='who-liked'>");
|
||||
let iconsHtml = "";
|
||||
users.forEach(function(u) {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
import { extractError } from 'discourse/lib/ajax-error';
|
||||
|
||||
// Modal for editing / creating a category
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
@ -67,17 +68,13 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
this.set('saving', true);
|
||||
model.set('parentCategory', parentCategory);
|
||||
|
||||
self.set('saving', false);
|
||||
this.get('model').save().then(function(result) {
|
||||
self.set('saving', false);
|
||||
self.send('closeModal');
|
||||
model.setProperties({slug: result.category.slug, id: result.category.id });
|
||||
DiscourseURL.redirectTo("/c/" + Discourse.Category.slugFor(model));
|
||||
}).catch(function(error) {
|
||||
if (error && error.responseText) {
|
||||
self.flash($.parseJSON(error.responseText).errors[0], 'error');
|
||||
} else {
|
||||
self.flash(I18n.t('generic_error'), 'error');
|
||||
}
|
||||
self.flash(extractError(error), 'error');
|
||||
self.set('saving', false);
|
||||
});
|
||||
},
|
||||
@ -94,13 +91,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
self.send('closeModal');
|
||||
DiscourseURL.redirectTo("/categories");
|
||||
}, function(error){
|
||||
|
||||
if (error && error.responseText) {
|
||||
self.flash($.parseJSON(error.responseText).errors[0]);
|
||||
} else {
|
||||
self.flash(I18n.t('generic_error'));
|
||||
}
|
||||
|
||||
self.flash(extractError(error), 'error');
|
||||
self.send('reopenModal');
|
||||
self.displayErrors([I18n.t("category.delete_error")]);
|
||||
self.set('deleting', false);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import loadScript from 'discourse/lib/load-script';
|
||||
import Quote from 'discourse/lib/quote';
|
||||
import property from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
needs: ['topic', 'composer'],
|
||||
@ -8,10 +9,15 @@ export default Ember.Controller.extend({
|
||||
loadScript('defer/html-sanitizer-bundle');
|
||||
}.on('init'),
|
||||
|
||||
// If the buffer is cleared, clear out other state (post)
|
||||
bufferChanged: function() {
|
||||
if (Ember.isEmpty(this.get('buffer'))) this.set('post', null);
|
||||
}.observes('buffer'),
|
||||
@property('buffer', 'postId')
|
||||
post(buffer, postId) {
|
||||
if (!postId || Ember.isEmpty(buffer)) { return null; }
|
||||
|
||||
const postStream = this.get('controllers.topic.model.postStream');
|
||||
const post = postStream.findLoadedPost(postId);
|
||||
|
||||
return post;
|
||||
},
|
||||
|
||||
// Save the currently selected text and displays the
|
||||
// "quote reply" button
|
||||
@ -26,8 +32,12 @@ export default Ember.Controller.extend({
|
||||
}
|
||||
|
||||
const selection = window.getSelection();
|
||||
// no selections
|
||||
if (selection.isCollapsed) return;
|
||||
|
||||
// no selections
|
||||
if (selection.isCollapsed) {
|
||||
this.set('buffer', '');
|
||||
return;
|
||||
}
|
||||
|
||||
// retrieve the selected range
|
||||
const range = selection.getRangeAt(0),
|
||||
@ -85,16 +95,13 @@ export default Ember.Controller.extend({
|
||||
},
|
||||
|
||||
quoteText() {
|
||||
|
||||
const postStream = this.get('controllers.topic.model.postStream');
|
||||
const postId = this.get('postId');
|
||||
const post = postStream.findLoadedPost(postId);
|
||||
const post = this.get('post');
|
||||
|
||||
// defer load if needed, if in an expanded replies section
|
||||
if (!post) {
|
||||
postStream.loadPost(postId).then(() => {
|
||||
this.quoteText();
|
||||
});
|
||||
const postStream = this.get('controllers.topic.model.postStream');
|
||||
postStream.loadPost(postId).then(() => this.quoteText());
|
||||
return;
|
||||
}
|
||||
|
||||
@ -110,7 +117,7 @@ export default Ember.Controller.extend({
|
||||
draftKey: post.get('topic.draft_key')
|
||||
};
|
||||
|
||||
if(post.get('post_number') === 1) {
|
||||
if (post.get('post_number') === 1) {
|
||||
composerOpts.topic = post.get("topic");
|
||||
} else {
|
||||
composerOpts.post = post;
|
||||
|
||||
@ -131,15 +131,15 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
draftSequence: topic.get('draft_sequence')
|
||||
};
|
||||
|
||||
if (quotedText) { opts.quote = quotedText; }
|
||||
|
||||
if(post && post.get("post_number") !== 1){
|
||||
opts.post = post;
|
||||
} else {
|
||||
opts.topic = topic;
|
||||
}
|
||||
|
||||
composerController.open(opts).then(function() {
|
||||
composerController.appendText(quotedText);
|
||||
});
|
||||
composerController.open(opts);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
@ -410,12 +410,12 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
action: Discourse.Composer.CREATE_TOPIC,
|
||||
draftKey: Discourse.Composer.REPLY_AS_NEW_TOPIC_KEY,
|
||||
categoryId: this.get('category.id')
|
||||
}).then(function() {
|
||||
}).then(() => {
|
||||
return Em.isEmpty(quotedText) ? Discourse.Post.loadQuote(post.get('id')) : quotedText;
|
||||
}).then(function(q) {
|
||||
const postUrl = "" + location.protocol + "//" + location.host + post.get('url'),
|
||||
postLink = "[" + Handlebars.escapeExpression(self.get('model.title')) + "](" + postUrl + ")";
|
||||
composerController.appendText(I18n.t("post.continue_discussion", { postLink: postLink }) + "\n\n" + q);
|
||||
}).then(q => {
|
||||
const postUrl = `${location.protocol}//${location.host}${post.get('url')}`,
|
||||
postLink = `[${Handlebars.escapeExpression(self.get('model.title'))}](${postUrl})`;
|
||||
composerController.appendText(`${I18n.t("post.continue_discussion", { postLink })}\n\n${q}`);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@ -67,6 +67,7 @@ export default Ember.Controller.extend({
|
||||
|
||||
const args = { stats: false };
|
||||
args.include_post_count_for = this.get('controllers.topic.model.id');
|
||||
args.skip_track_visit = true;
|
||||
|
||||
return Discourse.User.findByUsername(username, args).then((user) => {
|
||||
if (user.topic_post_count) {
|
||||
|
||||
@ -23,11 +23,11 @@ Discourse.BBCode.register('quote', {noWrap: true, singlePara: true}, function(co
|
||||
}
|
||||
|
||||
var avatarImg;
|
||||
var postNumber = parseInt(params['data-post'], 10);
|
||||
var topicId = parseInt(params['data-topic'], 10);
|
||||
|
||||
if (options.lookupAvatarByPostNumber) {
|
||||
// client-side, we can retrieve the avatar from the post
|
||||
var postNumber = parseInt(params['data-post'], 10);
|
||||
var topicId = parseInt(params['data-topic'], 10);
|
||||
|
||||
avatarImg = options.lookupAvatarByPostNumber(postNumber, topicId);
|
||||
} else if (options.lookupAvatar) {
|
||||
// server-side, we need to lookup the avatar from the username
|
||||
@ -39,12 +39,23 @@ Discourse.BBCode.register('quote', {noWrap: true, singlePara: true}, function(co
|
||||
return ['p', ['aside', params, ['blockquote'].concat(contents)]];
|
||||
}
|
||||
|
||||
return ['aside', params,
|
||||
['div', {'class': 'title'},
|
||||
['div', {'class': 'quote-controls'}],
|
||||
avatarImg ? ['__RAW', avatarImg] : "",
|
||||
username ? I18n.t('user.said', {username: username}) : ""
|
||||
],
|
||||
['blockquote'].concat(contents)
|
||||
];
|
||||
var header = [ 'div', {'class': 'title'},
|
||||
['div', {'class': 'quote-controls'}],
|
||||
avatarImg ? ['__RAW', avatarImg] : "",
|
||||
username ? I18n.t('user.said', {username: username}) : ""
|
||||
];
|
||||
|
||||
if (options.topicId && postNumber && options.getTopicInfo && topicId !== options.topicId) {
|
||||
var topicInfo = options.getTopicInfo(topicId);
|
||||
if (topicInfo) {
|
||||
var href = topicInfo.href;
|
||||
if (postNumber > 0) { href += "/" + postNumber; }
|
||||
// get rid of username said stuff
|
||||
header.pop();
|
||||
header.push(['a', {'href': href}, topicInfo.title]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return ['aside', params, header, ['blockquote'].concat(contents)];
|
||||
});
|
||||
|
||||
@ -29,10 +29,6 @@ export default {
|
||||
});
|
||||
}
|
||||
|
||||
bus.subscribe("/notification-alert/" + user.get('id'), function(data){
|
||||
onNotification(data, user);
|
||||
});
|
||||
|
||||
bus.subscribe("/notification/" + user.get('id'), function(data) {
|
||||
const oldUnread = user.get('unread_notifications');
|
||||
const oldPM = user.get('unread_private_messages');
|
||||
@ -85,7 +81,13 @@ export default {
|
||||
});
|
||||
|
||||
if (!Ember.testing) {
|
||||
initDesktopNotifications(bus);
|
||||
if (!Discourse.Mobile.mobileView) {
|
||||
bus.subscribe("/notification-alert/" + user.get('id'), function(data){
|
||||
onNotification(data, user);
|
||||
});
|
||||
|
||||
initDesktopNotifications(bus);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -306,7 +306,8 @@
|
||||
// end of Chunks
|
||||
|
||||
function firstByClass(doc, containerElement, className) {
|
||||
var elements = doc.getElementsByClassName(className);
|
||||
var container = containerElement || doc;
|
||||
var elements = container.getElementsByClassName(className);
|
||||
if (elements && elements.length) {
|
||||
return elements[0];
|
||||
}
|
||||
|
||||
@ -160,13 +160,15 @@ var toolbar = function(selected){
|
||||
|
||||
var PER_ROW = 12, PER_PAGE = 60;
|
||||
|
||||
var bindEvents = function(page,offset){
|
||||
var bindEvents = function(page, offset, options) {
|
||||
var composerController = Discourse.__container__.lookup('controller:composer');
|
||||
|
||||
$('.emoji-page a').click(function(){
|
||||
var title = $(this).attr('title');
|
||||
trackEmojiUsage(title);
|
||||
composerController.appendTextAtCursor(":" + title + ":", {space: true});
|
||||
|
||||
const prefix = options.skipPrefix ? "" : ":";
|
||||
composerController.appendTextAtCursor(`${prefix}${title}:`, {space: !options.skipPrefix});
|
||||
closeSelector();
|
||||
return false;
|
||||
}).hover(function(){
|
||||
@ -178,21 +180,21 @@ var bindEvents = function(page,offset){
|
||||
});
|
||||
|
||||
$('.emoji-modal .nav .next a').click(function(){
|
||||
render(page, offset+PER_PAGE);
|
||||
render(page, offset+PER_PAGE, options);
|
||||
});
|
||||
|
||||
$('.emoji-modal .nav .prev a').click(function(){
|
||||
render(page, offset-PER_PAGE);
|
||||
render(page, offset-PER_PAGE, options);
|
||||
});
|
||||
|
||||
$('.emoji-modal .toolbar a').click(function(){
|
||||
var p = parseInt($(this).data('group-id'));
|
||||
render(p, 0);
|
||||
render(p, 0, options);
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
var render = function(page, offset){
|
||||
var render = function(page, offset, options) {
|
||||
localStorage.emojiPage = page;
|
||||
localStorage.emojiOffset = offset;
|
||||
|
||||
@ -222,10 +224,12 @@ var render = function(page, offset){
|
||||
var rendered = Ember.TEMPLATES["emoji-toolbar.raw"](model);
|
||||
$('body').append(rendered);
|
||||
|
||||
bindEvents(page, offset);
|
||||
bindEvents(page, offset, options);
|
||||
};
|
||||
|
||||
var showSelector = function(){
|
||||
var showSelector = function(options) {
|
||||
options = options || {};
|
||||
|
||||
$('body').append('<div class="emoji-modal-wrapper"></div>');
|
||||
|
||||
$('.emoji-modal-wrapper').click(function(){
|
||||
@ -234,7 +238,7 @@ var showSelector = function(){
|
||||
|
||||
var page = parseInt(localStorage.emojiPage) || 0;
|
||||
var offset = parseInt(localStorage.emojiOffset) || 0;
|
||||
render(page, offset);
|
||||
render(page, offset, options);
|
||||
|
||||
$('body, textarea').on('keydown.emoji', function(e){
|
||||
if(e.which === 27){
|
||||
|
||||
@ -3,15 +3,17 @@ export default {
|
||||
REGEXP: /\[quote=([^\]]*)\]((?:[\s\S](?!\[quote=[^\]]*\]))*?)\[\/quote\]/im,
|
||||
|
||||
// Build the BBCode quote around the selected text
|
||||
build: function(post, contents, opts) {
|
||||
build(post, contents, opts) {
|
||||
var contents_hashed, result, sansQuotes, stripped, stripped_hashed, tmp;
|
||||
var full = opts && opts["full"];
|
||||
var raw = opts && opts["raw"];
|
||||
|
||||
if (!post) { return ""; }
|
||||
|
||||
if (!contents) contents = "";
|
||||
|
||||
sansQuotes = contents.replace(this.REGEXP, '').trim();
|
||||
if (sansQuotes.length === 0) return "";
|
||||
if (sansQuotes.length === 0) { return ""; }
|
||||
|
||||
// Escape the content of the quote
|
||||
sansQuotes = sansQuotes.replace(/</g, "<")
|
||||
@ -22,7 +24,7 @@ export default {
|
||||
/* Strip the HTML from cooked */
|
||||
tmp = document.createElement('div');
|
||||
tmp.innerHTML = post.get('cooked');
|
||||
stripped = tmp.textContent || tmp.innerText;
|
||||
stripped = tmp.textContent || tmp.innerText || "";
|
||||
|
||||
/*
|
||||
Let's remove any non alphanumeric characters as a kind of hash. Yes it's
|
||||
|
||||
@ -111,18 +111,9 @@ export default RestModel.extend({
|
||||
},
|
||||
|
||||
loadUsers(post) {
|
||||
return Discourse.ajax("/post_actions/users", {
|
||||
data: { id: post.get('id'), post_action_type_id: this.get('id') }
|
||||
}).then(function (result) {
|
||||
const users = [];
|
||||
result.forEach(function(user) {
|
||||
if (user.id === Discourse.User.currentProp('id')) {
|
||||
users.pushObject(Discourse.User.current());
|
||||
} else {
|
||||
users.pushObject(Discourse.User.create(user));
|
||||
}
|
||||
});
|
||||
return users;
|
||||
return this.store.find('post-action-user', {
|
||||
id: post.get('id'),
|
||||
post_action_type_id: this.get('id')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,9 +1,15 @@
|
||||
Discourse.LoginMethod = Ember.Object.extend({
|
||||
title: function(){
|
||||
title: function() {
|
||||
var titleSetting = this.get('titleSetting');
|
||||
if (!Ember.isEmpty(titleSetting)) {
|
||||
var result = Discourse.SiteSettings[titleSetting];
|
||||
if (!Ember.isEmpty(result)) { return result; }
|
||||
}
|
||||
|
||||
return this.get("titleOverride") || I18n.t("login." + this.get("name") + ".title");
|
||||
}.property(),
|
||||
|
||||
message: function(){
|
||||
message: function() {
|
||||
return this.get("messageOverride") || I18n.t("login." + this.get("name") + ".message");
|
||||
}.property()
|
||||
});
|
||||
@ -12,8 +18,8 @@ Discourse.LoginMethod = Ember.Object.extend({
|
||||
// just Em.get("Discourse.LoginMethod.all") and then
|
||||
// pushObject for any new methods
|
||||
Discourse.LoginMethod.reopenClass({
|
||||
register: function(method){
|
||||
if(this.methods){
|
||||
register: function(method) {
|
||||
if (this.methods){
|
||||
this.methods.pushObject(method);
|
||||
} else {
|
||||
this.preRegister = this.preRegister || [];
|
||||
@ -50,7 +56,14 @@ Discourse.LoginMethod.reopenClass({
|
||||
|
||||
if (this.preRegister){
|
||||
this.preRegister.forEach(function(method){
|
||||
methods.pushObject(method);
|
||||
var enabledSetting = method.get('enabledSetting');
|
||||
if (enabledSetting) {
|
||||
if (Discourse.SiteSettings[enabledSetting]) {
|
||||
methods.pushObject(method);
|
||||
}
|
||||
} else {
|
||||
methods.pushObject(method);
|
||||
}
|
||||
});
|
||||
delete this.preRegister;
|
||||
}
|
||||
|
||||
@ -405,9 +405,8 @@ Post.reopenClass({
|
||||
},
|
||||
|
||||
loadRevision(postId, version) {
|
||||
return Discourse.ajax("/posts/" + postId + "/revisions/" + version + ".json").then(function (result) {
|
||||
return Ember.Object.create(result);
|
||||
});
|
||||
return Discourse.ajax("/posts/" + postId + "/revisions/" + version + ".json")
|
||||
.then(result => Ember.Object.create(result));
|
||||
},
|
||||
|
||||
hideRevision(postId, version) {
|
||||
@ -419,16 +418,15 @@ Post.reopenClass({
|
||||
},
|
||||
|
||||
loadQuote(postId) {
|
||||
return Discourse.ajax("/posts/" + postId + ".json").then(function (result) {
|
||||
return Discourse.ajax("/posts/" + postId + ".json").then(result => {
|
||||
const post = Discourse.Post.create(result);
|
||||
return Quote.build(post, post.get('raw'), {raw: true, full: true});
|
||||
});
|
||||
},
|
||||
|
||||
loadRawEmail(postId) {
|
||||
return Discourse.ajax("/posts/" + postId + "/raw-email").then(function (result) {
|
||||
return result.raw_email;
|
||||
});
|
||||
return Discourse.ajax("/posts/" + postId + "/raw-email")
|
||||
.then(result => result.raw_email);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@ -1,23 +1,24 @@
|
||||
/*global Modernizr:true*/
|
||||
|
||||
/**
|
||||
Initializes an object that lets us know about our capabilities.
|
||||
**/
|
||||
// Initializes an object that lets us know about our capabilities.
|
||||
export default {
|
||||
name: "sniff-capabilities",
|
||||
initialize: function(container, application) {
|
||||
var $html = $('html'),
|
||||
touch = $html.hasClass('touch') || (Modernizr.prefixed("MaxTouchPoints", navigator) > 1),
|
||||
caps = Ember.Object.create();
|
||||
initialize(container, application) {
|
||||
const $html = $('html'),
|
||||
touch = $html.hasClass('touch') || (Modernizr.prefixed("MaxTouchPoints", navigator) > 1),
|
||||
caps = Ember.Object.create();
|
||||
|
||||
// Store the touch ability in our capabilities object
|
||||
caps.set('touch', touch);
|
||||
$html.addClass(touch ? 'discourse-touch' : 'discourse-no-touch');
|
||||
|
||||
// Detect Android
|
||||
// Detect Devices
|
||||
if (navigator) {
|
||||
var ua = navigator.userAgent;
|
||||
caps.set('android', ua && ua.indexOf('Android') !== -1);
|
||||
const ua = navigator.userAgent;
|
||||
if (ua) {
|
||||
caps.set('android', ua.indexOf('Android') !== -1);
|
||||
caps.set('winphone', ua.indexOf('Windows Phone') !== -1);
|
||||
}
|
||||
}
|
||||
|
||||
// We consider high res a device with 1280 horizontal pixels. High DPI tablets like
|
||||
|
||||
@ -12,7 +12,7 @@ export default Discourse.Route.extend({
|
||||
serialize(model) {
|
||||
return {
|
||||
id: model.get("id"),
|
||||
slug: model.get("name").replace(/[^A-Za-z0-9_]+/g, "-").toLowerCase()
|
||||
slug: model.get("slug")
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@ -63,9 +63,15 @@ export default (filter, params) => {
|
||||
|
||||
setupController(controller, model) {
|
||||
const topics = this.get('topics'),
|
||||
periodId = topics.get('for_period') || (filter.indexOf('/') > 0 ? filter.split('/')[1] : '');
|
||||
periodId = topics.get('for_period') || (filter.indexOf('/') > 0 ? filter.split('/')[1] : ''),
|
||||
canCreateTopic = topics.get('can_create_topic'),
|
||||
canCreateTopicOnCategory = model.get('permission') === Discourse.PermissionType.FULL;
|
||||
|
||||
this.controllerFor('navigation/category').set('canCreateTopic', topics.get('can_create_topic'));
|
||||
this.controllerFor('navigation/category').setProperties({
|
||||
canCreateTopicOnCategory: canCreateTopicOnCategory,
|
||||
cannotCreateTopicOnCategory: !canCreateTopicOnCategory,
|
||||
canCreateTopic: canCreateTopic
|
||||
});
|
||||
this.controllerFor('discovery/topics').setProperties({
|
||||
model: topics,
|
||||
category: model,
|
||||
@ -74,7 +80,9 @@ export default (filter, params) => {
|
||||
noSubcategories: params && !!params.no_subcategories,
|
||||
order: topics.get('params.order'),
|
||||
ascending: topics.get('params.ascending'),
|
||||
expandAllPinned: true
|
||||
expandAllPinned: true,
|
||||
canCreateTopic: canCreateTopic,
|
||||
canCreateTopicOnCategory: canCreateTopicOnCategory
|
||||
});
|
||||
|
||||
this.searchService.set('searchContext', model.get('searchContext'));
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
<div class='wmd-button-bar'></div>
|
||||
{{textarea value=value class="wmd-input"}}
|
||||
<div class="wmd-preview preview {{unless value 'hidden'}}">
|
||||
</div>
|
||||
<div class="wmd-preview preview {{unless value 'hidden'}}"></div>
|
||||
|
||||
@ -37,10 +37,10 @@
|
||||
{{/conditional-loading-spinner}}
|
||||
</div>
|
||||
{{plugin-outlet "user-menu-bottom"}}
|
||||
{{#if siteSettings.show_logout_in_header}}
|
||||
<div class='logout-link'>
|
||||
<hr>
|
||||
<ul class='menu-links'>
|
||||
<li>{{d-link action="logout" class="logout" icon="sign-out" label="user.log_out"}}</li>
|
||||
</ul>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/menu-panel}}
|
||||
|
||||
@ -21,14 +21,15 @@
|
||||
<div class='top-lists'>
|
||||
{{period-chooser period=period action="changePeriod"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if topicTrackingState.hasIncoming}}
|
||||
<div class="show-more {{if hasTopics 'has-topics'}}">
|
||||
<div class='alert alert-info clickable' {{action "showInserted"}}>
|
||||
{{count-i18n key="topic_count_" suffix=topicTrackingState.filter count=topicTrackingState.incomingCount}}
|
||||
{{i18n 'click_to_show'}}
|
||||
{{else}}
|
||||
{{#if topicTrackingState.hasIncoming}}
|
||||
<div class="show-more {{if hasTopics 'has-topics'}}">
|
||||
<div class='alert alert-info clickable' {{action "showInserted"}}>
|
||||
{{count-i18n key="topic_count_" suffix=topicTrackingState.filter count=topicTrackingState.incomingCount}}
|
||||
{{i18n 'click_to_show'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if hasTopics}}
|
||||
@ -67,7 +68,7 @@
|
||||
</div>
|
||||
<h3>
|
||||
{{footerMessage}}
|
||||
{{#if model.can_create_topic}}<a href {{action "createTopic"}}>{{i18n 'topic.suggest_create_topic'}}</a>{{/if}}
|
||||
{{#if canCreateTopicOnCategory}}<a href {{action "createTopic"}}>{{i18n 'topic.suggest_create_topic'}}</a>{{/if}}
|
||||
</h3>
|
||||
{{else}}
|
||||
{{#if top}}
|
||||
|
||||
@ -1,8 +1,14 @@
|
||||
<div class='autocomplete'>
|
||||
<ul>
|
||||
{{#each option in options}}
|
||||
{{#each option in options}}
|
||||
<li>
|
||||
<a href><img src='{{option.src}}' class='emoji'> {{option.code}}</a>
|
||||
<a href>
|
||||
{{#if option.src}}
|
||||
<img src={{option.src}} class='emoji'> {{option.code}}
|
||||
{{else}}
|
||||
{{option.label}}
|
||||
{{/if}}
|
||||
</a>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
|
||||
@ -3,13 +3,13 @@
|
||||
<div class='top-lists'>
|
||||
{{period-chooser period=period action="changePeriod"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if topicTrackingState.hasIncoming}}
|
||||
<div class='alert alert-info' {{action "showInserted"}}>
|
||||
{{count-i18n key="topic_count_" suffix=topicTrackingState.filter count=topicTrackingState.incomingCount}}
|
||||
{{i18n 'click_to_show'}}
|
||||
</div>
|
||||
{{else}}
|
||||
{{#if topicTrackingState.hasIncoming}}
|
||||
<div class='alert alert-info' {{action "showInserted"}}>
|
||||
{{count-i18n key="topic_count_" suffix=topicTrackingState.filter count=topicTrackingState.incomingCount}}
|
||||
{{i18n 'click_to_show'}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if hasTopics}}
|
||||
|
||||
@ -11,7 +11,12 @@
|
||||
{{/if}}
|
||||
|
||||
{{#if canCreateTopic}}
|
||||
{{d-button id="create-topic" class="btn-default" action="createTopic" icon="plus" label="topic.create"}}
|
||||
{{d-button id="create-topic"
|
||||
class="btn-default"
|
||||
action="createTopic"
|
||||
icon="plus"
|
||||
label="topic.create"
|
||||
disabled=cannotCreateTopicOnCategory}}
|
||||
{{/if}}
|
||||
|
||||
{{#if canEditCategory}}
|
||||
|
||||
@ -12,12 +12,12 @@
|
||||
<div class='row'>
|
||||
|
||||
<div class="topic-avatar">
|
||||
{{#if userDeleted}}
|
||||
<i class="fa fa-trash-o deleted-user-avatar"></i>
|
||||
{{else}}
|
||||
{{raw "post/poster-avatar" post=this classNames="main-avatar"}}
|
||||
{{/if}}
|
||||
{{plugin-outlet "poster-avatar-bottom"}}
|
||||
{{#if userDeleted}}
|
||||
<i class="fa fa-trash-o deleted-user-avatar"></i>
|
||||
{{else}}
|
||||
{{raw "post/poster-avatar" post=this classNames="main-avatar"}}
|
||||
{{/if}}
|
||||
{{plugin-outlet "poster-avatar-bottom"}}
|
||||
</div>
|
||||
|
||||
<div class='topic-body'>
|
||||
|
||||
@ -120,6 +120,7 @@
|
||||
{{#if model.last_seen_at}}
|
||||
<dt>{{i18n 'user.last_seen'}}</dt><dd>{{bound-date model.last_seen_at}}</dd>
|
||||
{{/if}}
|
||||
<dt>{{i18n 'views'}}</dt><dd>{{model.profile_view_count}}</dd>
|
||||
{{#if model.invited_by}}
|
||||
<dt>{{i18n 'user.invited_by'}}</dt><dd>{{#link-to 'user' model.invited_by}}{{model.invited_by.username}}{{/link-to}}</dd>
|
||||
{{/if}}
|
||||
|
||||
@ -5,6 +5,7 @@ import positioningWorkaround from 'discourse/lib/safari-hacks';
|
||||
import debounce from 'discourse/lib/debounce';
|
||||
import { linkSeenMentions, fetchUnseenMentions } from 'discourse/lib/link-mentions';
|
||||
import { headerHeight } from 'discourse/views/header';
|
||||
import { showSelector } from 'discourse/lib/emoji/emoji-toolbar';
|
||||
|
||||
const ComposerView = Ember.View.extend(Ember.Evented, {
|
||||
_lastKeyTimeout: null,
|
||||
@ -185,13 +186,23 @@ const ComposerView = Ember.View.extend(Ember.Evented, {
|
||||
if (!this.siteSettings.enable_emoji) { return; }
|
||||
|
||||
const template = this.container.lookup('template:emoji-selector-autocomplete.raw');
|
||||
|
||||
this.$('.wmd-input').autocomplete({
|
||||
template: template,
|
||||
key: ":",
|
||||
transformComplete(v) { return v.code + ":"; },
|
||||
dataSource(term){
|
||||
return new Ember.RSVP.Promise(function(resolve) {
|
||||
const full = ":" + term;
|
||||
|
||||
transformComplete(v) {
|
||||
if (v.code) {
|
||||
return `${v.code}:`;
|
||||
} else {
|
||||
showSelector({ skipPrefix: true });
|
||||
return "";
|
||||
}
|
||||
},
|
||||
|
||||
dataSource(term) {
|
||||
return new Ember.RSVP.Promise(resolve => {
|
||||
const full = `:${term}`;
|
||||
term = term.toLowerCase();
|
||||
|
||||
if (term === "") {
|
||||
@ -205,10 +216,13 @@ const ComposerView = Ember.View.extend(Ember.Evented, {
|
||||
const options = Discourse.Emoji.search(term, {maxResults: 5});
|
||||
|
||||
return resolve(options);
|
||||
}).then(function(list) {
|
||||
return list.map(function(i) {
|
||||
return {code: i, src: Discourse.Emoji.urlFor(i)};
|
||||
});
|
||||
}).then(list => list.map(code => {
|
||||
return {code, src: Discourse.Emoji.urlFor(code)};
|
||||
})).then(list => {
|
||||
if (list.length) {
|
||||
list.push({ label: I18n.t("composer.more_emoji") });
|
||||
}
|
||||
return list;
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -246,7 +260,7 @@ const ComposerView = Ember.View.extend(Ember.Evented, {
|
||||
});
|
||||
|
||||
|
||||
const options ={
|
||||
const options = {
|
||||
containerElement: this.element,
|
||||
lookupAvatarByPostNumber(postNumber, topicId) {
|
||||
const posts = controller.get('controllers.topic.model.postStream.posts');
|
||||
|
||||
@ -208,10 +208,15 @@ const PostView = Discourse.GroupedView.extend(Ember.Evented, {
|
||||
const likeAction = post.get('likeAction');
|
||||
if (likeAction && likeAction.get('canToggle')) {
|
||||
const users = this.get('likedUsers');
|
||||
if (likeAction.toggle(post) && users.length) {
|
||||
users.addObject(currentUser);
|
||||
const store = this.get('controller.store');
|
||||
const action = store.createRecord('post-action-user',
|
||||
currentUser.getProperties('id', 'username', 'avatar_template')
|
||||
);
|
||||
|
||||
if (likeAction.toggle(post) && users.get('length')) {
|
||||
users.addObject(action);
|
||||
} else {
|
||||
users.removeObject(currentUser);
|
||||
users.removeObject(action);
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -221,7 +226,7 @@ const PostView = Discourse.GroupedView.extend(Ember.Evented, {
|
||||
const likeAction = post.get('likeAction');
|
||||
if (likeAction) {
|
||||
const users = this.get('likedUsers');
|
||||
if (users.length) {
|
||||
if (users.get('length')) {
|
||||
users.clear();
|
||||
} else {
|
||||
likeAction.loadUsers(post).then(newUsers => this.set('likedUsers', newUsers));
|
||||
|
||||
@ -34,7 +34,11 @@ export default Ember.View.extend({
|
||||
// best we can do is debounce this so we dont keep locking up
|
||||
// the selection when we add the caret to measure where we place
|
||||
// the quote reply widget
|
||||
if (navigator.userAgent.match(/Windows Phone/)) {
|
||||
//
|
||||
// Same hack applied to Android cause it has unreliable touchend
|
||||
const caps = this.capabilities;
|
||||
const android = caps.get('android');
|
||||
if (caps.get('winphone') || android) {
|
||||
onSelectionChanged = _.debounce(onSelectionChanged, 500);
|
||||
}
|
||||
|
||||
@ -58,12 +62,6 @@ export default Ember.View.extend({
|
||||
view.selectText(e.target, controller);
|
||||
view.set('isMouseDown', false);
|
||||
})
|
||||
.on('touchstart.quote-button', function(){
|
||||
view.set('isTouchInProgress', true);
|
||||
})
|
||||
.on('touchend.quote-button', function(){
|
||||
view.set('isTouchInProgress', false);
|
||||
})
|
||||
.on('selectionchange', function() {
|
||||
// there is no need to handle this event when the mouse is down
|
||||
// or if there a touch in progress
|
||||
@ -71,6 +69,18 @@ export default Ember.View.extend({
|
||||
// `selection.anchorNode` is used as a target
|
||||
onSelectionChanged();
|
||||
});
|
||||
|
||||
// Android is dodgy, touchend often will not fire
|
||||
// https://code.google.com/p/android/issues/detail?id=19827
|
||||
if (!android) {
|
||||
$(document)
|
||||
.on('touchstart.quote-button', function(){
|
||||
view.set('isTouchInProgress', true);
|
||||
})
|
||||
.on('touchend.quote-button', function(){
|
||||
view.set('isTouchInProgress', false);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
selectText(target, controller) {
|
||||
|
||||
@ -73,6 +73,9 @@
|
||||
//= require ./discourse/components/topic-notifications-button
|
||||
//= require ./discourse/lib/link-mentions
|
||||
//= require ./discourse/views/header
|
||||
//= require ./discourse/dialects/dialect
|
||||
//= require ./discourse/lib/emoji/emoji
|
||||
//= require ./discourse/lib/emoji/emoji-toolbar
|
||||
//= require ./discourse/views/composer
|
||||
//= require ./discourse/lib/show-modal
|
||||
//= require ./discourse/lib/screen-track
|
||||
@ -90,8 +93,6 @@
|
||||
//= require ./discourse/helpers/loading-spinner
|
||||
//= require ./discourse/helpers/category-link
|
||||
//= require ./discourse/lib/export-result
|
||||
//= require ./discourse/dialects/dialect
|
||||
//= require ./discourse/lib/emoji/emoji
|
||||
//= require_tree ./discourse/lib
|
||||
//= require ./discourse/router
|
||||
|
||||
|
||||
@ -180,7 +180,6 @@ td.flaggers td {
|
||||
.result-message {
|
||||
display: inline-block;
|
||||
padding-left: 10px;
|
||||
padding-top: 5px;
|
||||
}
|
||||
.username {
|
||||
input[type=text] {
|
||||
|
||||
@ -248,7 +248,7 @@ aside.onebox.twitterstatus .onebox-body {
|
||||
.thumbnail {
|
||||
float: left;
|
||||
}
|
||||
.tweet {
|
||||
p, .tweet {
|
||||
float: left;
|
||||
display: inline-block;
|
||||
white-space: pre-wrap;
|
||||
|
||||
@ -9,9 +9,6 @@
|
||||
|
||||
.badge-wrapper {
|
||||
float: left;
|
||||
&.bullet {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -189,13 +189,15 @@
|
||||
}
|
||||
.badge-wrapper {
|
||||
&.bar {
|
||||
margin: 5px 0;
|
||||
padding: 5px 0;
|
||||
width: 100%;
|
||||
.badge-category {
|
||||
max-width: 100px;
|
||||
}
|
||||
}
|
||||
&.bullet {
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
width: 100%;
|
||||
.badge-category {
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
@ -85,6 +85,9 @@
|
||||
color: dark-light-choose(scale-color($tertiary, $lightness: -40%), scale-color($tertiary, $lightness: 40%));
|
||||
}
|
||||
}
|
||||
.badge-wrapper {
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.composer-popup:nth-of-type(2) {
|
||||
@ -221,7 +224,8 @@
|
||||
}
|
||||
}
|
||||
.contents {
|
||||
padding: 10px;
|
||||
padding-left: 10px;
|
||||
padding-top: 5px;
|
||||
min-width: 1280px;
|
||||
.form-element {
|
||||
position: relative;
|
||||
|
||||
@ -507,7 +507,7 @@ video {
|
||||
.extra-info-wrapper {
|
||||
overflow: hidden;
|
||||
|
||||
.star, .badge-wrapper, i, .topic-link:not(.loading) {
|
||||
.badge-wrapper, i, .topic-link {
|
||||
-webkit-animation: fadein .7s;
|
||||
animation: fadein .7s;
|
||||
}
|
||||
@ -651,13 +651,13 @@ blockquote {
|
||||
|
||||
.reply-new {
|
||||
padding-left: 27px;
|
||||
display:block;
|
||||
display: inline-block;
|
||||
overflow:hidden;
|
||||
}
|
||||
|
||||
.track-link {
|
||||
padding-left: 10px;
|
||||
display: block;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@ -707,7 +707,7 @@ $topic-avatar-width: 45px;
|
||||
}
|
||||
|
||||
.small-action {
|
||||
width: 755px;
|
||||
max-width: 755px;
|
||||
border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -75%);
|
||||
}
|
||||
|
||||
|
||||
@ -123,11 +123,10 @@ a:hover.reply-new {
|
||||
border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
|
||||
padding: 5px;
|
||||
background: $secondary;
|
||||
@include box-shadow(0 0px 2px rgba(0,0,0, .2));
|
||||
|
||||
position: relative;
|
||||
left: 345px;
|
||||
width: 133px;
|
||||
left: 340px;
|
||||
width: 135px;
|
||||
padding: 5px;
|
||||
|
||||
button.full {
|
||||
|
||||
@ -46,7 +46,7 @@ button {
|
||||
padding: 8px 10px;
|
||||
vertical-align: top;
|
||||
background: transparent;
|
||||
color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%));
|
||||
color: dark-light-choose(scale-color($primary, $lightness: 75%), scale-color($secondary, $lightness: 25%));
|
||||
float: left;
|
||||
&.hidden {
|
||||
display: none;
|
||||
@ -80,7 +80,7 @@ button {
|
||||
/* shift post reply button to the right and make it black */
|
||||
.post-controls button.create {
|
||||
float: right;
|
||||
color: $primary;
|
||||
color: dark-light-choose(scale-color($primary, $lightness: 20%), scale-color($secondary, $lightness: 80%));
|
||||
}
|
||||
|
||||
|
||||
@ -461,14 +461,10 @@ span.highlighted {
|
||||
}
|
||||
|
||||
.topic-meta-data {
|
||||
white-space: nowrap;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: 0px;
|
||||
margin-left: 50px;
|
||||
.names {
|
||||
margin: 5px 0 0 5px;
|
||||
line-height: 17px;
|
||||
padding-left: 50px;
|
||||
span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@ -64,11 +64,10 @@
|
||||
border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
|
||||
padding: 5px;
|
||||
background: $secondary;
|
||||
box-shadow: 0 0px 2px rgba(0,0,0, .2);
|
||||
|
||||
position: absolute;
|
||||
bottom: 34px;
|
||||
width: 133px;
|
||||
width: 135px;
|
||||
|
||||
button.full {
|
||||
width: 100%;
|
||||
|
||||
@ -76,17 +76,7 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
# If they hit the rate limiter
|
||||
rescue_from RateLimiter::LimitExceeded do |e|
|
||||
|
||||
time_left = ""
|
||||
if e.available_in < 1.minute.to_i
|
||||
time_left = I18n.t("rate_limiter.seconds", count: e.available_in)
|
||||
elsif e.available_in < 1.hour.to_i
|
||||
time_left = I18n.t("rate_limiter.minutes", count: (e.available_in / 1.minute.to_i))
|
||||
else
|
||||
time_left = I18n.t("rate_limiter.hours", count: (e.available_in / 1.hour.to_i))
|
||||
end
|
||||
|
||||
render_json_error I18n.t("rate_limiter.too_many_requests", time_left: time_left), type: :rate_limit, status: 429
|
||||
render_json_error e.description, type: :rate_limit, status: 429
|
||||
end
|
||||
|
||||
rescue_from PG::ReadOnlySqlTransaction do |e|
|
||||
@ -310,9 +300,6 @@ class ApplicationController < ActionController::Base
|
||||
def preload_current_user_data
|
||||
store_preloaded("currentUser", MultiJson.dump(CurrentUserSerializer.new(current_user, scope: guardian, root: false)))
|
||||
report = TopicTrackingState.report(current_user.id)
|
||||
if report.length >= SiteSetting.max_tracked_new_unread.to_i
|
||||
TopicUser.cap_unread_later(current_user.id)
|
||||
end
|
||||
serializer = ActiveModel::ArraySerializer.new(report, each_serializer: TopicTrackingStateSerializer)
|
||||
store_preloaded("topicTrackingStates", MultiJson.dump(serializer))
|
||||
end
|
||||
|
||||
@ -4,6 +4,7 @@ class CategoriesController < ApplicationController
|
||||
|
||||
before_filter :ensure_logged_in, except: [:index, :show, :redirect]
|
||||
before_filter :fetch_category, only: [:show, :update, :destroy]
|
||||
before_filter :initialize_staff_action_logger, only: [:create, :update, :destroy]
|
||||
skip_before_filter :check_xhr, only: [:index, :redirect]
|
||||
|
||||
def redirect
|
||||
@ -81,10 +82,18 @@ class CategoriesController < ApplicationController
|
||||
position = category_params.delete(:position)
|
||||
|
||||
@category = Category.create(category_params.merge(user: current_user))
|
||||
return render_json_error(@category) unless @category.save
|
||||
|
||||
@category.move_to(position.to_i) if position
|
||||
render_serialized(@category, CategorySerializer)
|
||||
if @category.save
|
||||
@category.move_to(position.to_i) if position
|
||||
|
||||
Scheduler::Defer.later "Log staff action create category" do
|
||||
@staff_action_logger.log_category_creation(@category)
|
||||
end
|
||||
|
||||
render_serialized(@category, CategorySerializer)
|
||||
else
|
||||
return render_json_error(@category) unless @category.save
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@ -103,8 +112,15 @@ class CategoriesController < ApplicationController
|
||||
end
|
||||
|
||||
category_params.delete(:position)
|
||||
old_permissions = Category.find(@category.id).permissions_params
|
||||
|
||||
cat.update_attributes(category_params)
|
||||
if result = cat.update_attributes(category_params)
|
||||
Scheduler::Defer.later "Log staff action change category settings" do
|
||||
@staff_action_logger.log_category_settings_change(@category, category_params, old_permissions)
|
||||
end
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
@ -133,6 +149,10 @@ class CategoriesController < ApplicationController
|
||||
guardian.ensure_can_delete!(@category)
|
||||
@category.destroy
|
||||
|
||||
Scheduler::Defer.later "Log staff action delete category" do
|
||||
@staff_action_logger.log_category_deletion(@category)
|
||||
end
|
||||
|
||||
render json: success_json
|
||||
end
|
||||
|
||||
@ -175,4 +195,8 @@ class CategoriesController < ApplicationController
|
||||
def fetch_category
|
||||
@category = Category.find_by(slug: params[:id]) || Category.find_by(id: params[:id].to_i)
|
||||
end
|
||||
|
||||
def initialize_staff_action_logger
|
||||
@staff_action_logger = StaffActionLogger.new(current_user)
|
||||
end
|
||||
end
|
||||
|
||||
@ -84,7 +84,7 @@ class ListController < ApplicationController
|
||||
end
|
||||
|
||||
define_method("category_#{filter}") do
|
||||
canonical_url "#{Discourse.base_url}#{@category.url}"
|
||||
canonical_url "#{Discourse.base_url_no_prefix}#{@category.url}"
|
||||
self.send(filter, category: @category.id)
|
||||
end
|
||||
|
||||
@ -93,7 +93,7 @@ class ListController < ApplicationController
|
||||
end
|
||||
|
||||
define_method("parent_category_category_#{filter}") do
|
||||
canonical_url "#{Discourse.base_url}#{@category.url}"
|
||||
canonical_url "#{Discourse.base_url_no_prefix}#{@category.url}"
|
||||
self.send(filter, category: @category.id)
|
||||
end
|
||||
|
||||
@ -228,11 +228,11 @@ class ListController < ApplicationController
|
||||
parent_category_id = nil
|
||||
if parent_slug_or_id.present?
|
||||
parent_category_id = Category.query_parent_category(parent_slug_or_id)
|
||||
raise Discourse::NotFound if parent_category_id.blank?
|
||||
redirect_or_not_found and return if parent_category_id.blank?
|
||||
end
|
||||
|
||||
@category = Category.query_category(slug_or_id, parent_category_id)
|
||||
raise Discourse::NotFound if !@category
|
||||
redirect_or_not_found and return if !@category
|
||||
|
||||
@description_meta = @category.description_text
|
||||
guardian.ensure_can_see!(@category)
|
||||
@ -308,4 +308,23 @@ class ListController < ApplicationController
|
||||
periods
|
||||
end
|
||||
|
||||
def redirect_or_not_found
|
||||
url = request.fullpath
|
||||
permalink = Permalink.find_by_url(url)
|
||||
|
||||
if permalink.present?
|
||||
# permalink present, redirect to that URL
|
||||
if permalink.external_url
|
||||
redirect_to permalink.external_url, status: :moved_permanently
|
||||
elsif permalink.target_url
|
||||
redirect_to "#{Discourse::base_uri}#{permalink.target_url}", status: :moved_permanently
|
||||
else
|
||||
raise Discourse::NotFound
|
||||
end
|
||||
else
|
||||
# redirect to 404
|
||||
raise Discourse::NotFound
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
15
app/controllers/manifest_json_controller.rb
Normal file
15
app/controllers/manifest_json_controller.rb
Normal file
@ -0,0 +1,15 @@
|
||||
class ManifestJsonController < ApplicationController
|
||||
layout false
|
||||
skip_before_filter :preload_json, :check_xhr
|
||||
|
||||
def index
|
||||
manifest = {
|
||||
short_name: SiteSetting.title,
|
||||
display: 'browser',
|
||||
orientation: 'portrait',
|
||||
start_url: "#{Discourse.base_uri}/"
|
||||
}
|
||||
|
||||
render json: manifest.to_json
|
||||
end
|
||||
end
|
||||
22
app/controllers/post_action_users_controller.rb
Normal file
22
app/controllers/post_action_users_controller.rb
Normal file
@ -0,0 +1,22 @@
|
||||
require_dependency 'discourse'
|
||||
|
||||
class PostActionUsersController < ApplicationController
|
||||
def index
|
||||
params.require(:post_action_type_id)
|
||||
params.require(:id)
|
||||
post_action_type_id = params[:post_action_type_id].to_i
|
||||
|
||||
finder = Post.where(id: params[:id].to_i)
|
||||
finder = finder.with_deleted if guardian.is_staff?
|
||||
|
||||
post = finder.first
|
||||
guardian.ensure_can_see!(post)
|
||||
guardian.ensure_can_see_post_actors!(post.topic, post_action_type_id)
|
||||
|
||||
post_actions = post.post_actions.where(post_action_type_id: post_action_type_id)
|
||||
.includes(:user)
|
||||
.order('post_actions.created_at asc')
|
||||
|
||||
render_serialized(post_actions.to_a, PostActionUserSerializer, root: 'post_action_users')
|
||||
end
|
||||
end
|
||||
@ -1,8 +1,7 @@
|
||||
require_dependency 'discourse'
|
||||
|
||||
class PostActionsController < ApplicationController
|
||||
|
||||
before_filter :ensure_logged_in, except: :users
|
||||
before_filter :ensure_logged_in
|
||||
before_filter :fetch_post_from_params
|
||||
before_filter :fetch_post_action_type_id_from_params
|
||||
|
||||
@ -26,16 +25,6 @@ class PostActionsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def users
|
||||
guardian.ensure_can_see_post_actors!(@post.topic, @post_action_type_id)
|
||||
|
||||
post_actions = @post.post_actions.where(post_action_type_id: @post_action_type_id)
|
||||
.includes(:user)
|
||||
.order('post_actions.created_at asc')
|
||||
|
||||
render_serialized(post_actions.to_a, PostActionUserSerializer)
|
||||
end
|
||||
|
||||
def destroy
|
||||
post_action = current_user.post_actions.find_by(post_id: params[:id].to_i, post_action_type_id: @post_action_type_id, deleted_at: nil)
|
||||
raise Discourse::NotFound if post_action.blank?
|
||||
|
||||
@ -83,7 +83,7 @@ class TopicsController < ApplicationController
|
||||
response.headers['X-Robots-Tag'] = 'noindex'
|
||||
end
|
||||
|
||||
canonical_url UrlHelper.absolute_without_cdn("#{Discourse.base_uri}#{@topic_view.canonical_path}")
|
||||
canonical_url UrlHelper.absolute_without_cdn(@topic_view.canonical_path)
|
||||
|
||||
perform_show_response
|
||||
|
||||
|
||||
@ -39,6 +39,10 @@ class UsersController < ApplicationController
|
||||
user_serializer.topic_post_count = {topic_id => Post.where(topic_id: topic_id, user_id: @user.id).count }
|
||||
end
|
||||
|
||||
if !params[:skip_track_visit] && (@user != current_user)
|
||||
track_visit_to_user_profile
|
||||
end
|
||||
|
||||
# This is a hack to get around a Rails issue where values with periods aren't handled correctly
|
||||
# when used as part of a route.
|
||||
if params[:external_id] and params[:external_id].ends_with? '.json'
|
||||
@ -650,4 +654,14 @@ class UsersController < ApplicationController
|
||||
render json: { success: false, message: I18n.t(key) }
|
||||
end
|
||||
|
||||
def track_visit_to_user_profile
|
||||
user_profile_id = @user.user_profile.id
|
||||
ip = request.remote_ip
|
||||
user_id = (current_user.id if current_user)
|
||||
|
||||
Scheduler::Defer.later 'Track profile view visit' do
|
||||
UserProfileView.add(user_profile_id, ip, user_id)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@ -121,7 +121,7 @@ module ApplicationHelper
|
||||
|
||||
opts ||= {}
|
||||
opts[:image] ||= "#{Discourse.base_url}#{SiteSetting.logo_small_url}"
|
||||
opts[:url] ||= "#{Discourse.base_url}#{request.fullpath}"
|
||||
opts[:url] ||= "#{Discourse.base_url_no_prefix}#{request.fullpath}"
|
||||
|
||||
# Use the correct scheme for open graph
|
||||
if opts[:image].present? && opts[:image].start_with?("//")
|
||||
|
||||
@ -14,7 +14,9 @@ module Jobs
|
||||
recooked = nil
|
||||
|
||||
if args[:cook].present?
|
||||
recooked = post.cook(post.raw, topic_id: post.topic_id)
|
||||
cooking_options = args[:cooking_options] || {}
|
||||
cooking_options[:topic_id] = post.topic_id
|
||||
recooked = post.cook(post.raw, cooking_options.symbolize_keys)
|
||||
post.update_column(:cooked, recooked)
|
||||
end
|
||||
|
||||
|
||||
@ -33,9 +33,7 @@ module Jobs
|
||||
Discourse.handle_job_exception(hash[:ex], error_context(args, "Rebaking user id #{user_id}", user_id: user_id))
|
||||
end
|
||||
|
||||
TopicUser.cap_unread_backlog!
|
||||
|
||||
offset = (SiteSetting.max_tracked_new_unread * (2/5.0)).to_i
|
||||
offset = (SiteSetting.max_new_topics).to_i
|
||||
last_new_topic = Topic.order('created_at desc').offset(offset).select(:created_at).first
|
||||
if last_new_topic
|
||||
SiteSetting.min_new_topics_time = last_new_topic.created_at.to_i
|
||||
|
||||
@ -175,6 +175,7 @@ class UserNotifications < ActionMailer::Base
|
||||
.where("post_number < ?", post.post_number)
|
||||
.where(user_deleted: false)
|
||||
.where(hidden: false)
|
||||
.where(post_type: Topic.visible_post_types)
|
||||
.order('created_at desc')
|
||||
.limit(SiteSetting.email_posts_context)
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ class AdminDashboardData
|
||||
GLOBAL_REPORTS ||= [
|
||||
'visits',
|
||||
'signups',
|
||||
'profile_views',
|
||||
'topics',
|
||||
'posts',
|
||||
'time_to_first_response',
|
||||
|
||||
12
app/models/anon_site_json_cache_observer.rb
Normal file
12
app/models/anon_site_json_cache_observer.rb
Normal file
@ -0,0 +1,12 @@
|
||||
class AnonSiteJsonCacheObserver < ActiveRecord::Observer
|
||||
observe :category, :post_action_type, :user_field, :group
|
||||
|
||||
def after_destroy(object)
|
||||
Site.clear_anon_cache!
|
||||
end
|
||||
|
||||
def after_save(object)
|
||||
Site.clear_anon_cache!
|
||||
end
|
||||
|
||||
end
|
||||
@ -329,12 +329,38 @@ SQL
|
||||
Badge.find_each(&:reset_grant_count!)
|
||||
end
|
||||
|
||||
def display_name
|
||||
if self.system?
|
||||
key = "admin_js.badges.badge.#{i18n_name}.name"
|
||||
I18n.t(key, default: self.name)
|
||||
else
|
||||
self.name
|
||||
end
|
||||
end
|
||||
|
||||
def long_description
|
||||
if self[:long_description].present?
|
||||
self[:long_description]
|
||||
else
|
||||
key = "badges.long_descriptions.#{i18n_name}"
|
||||
I18n.t(key, default: '')
|
||||
end
|
||||
end
|
||||
|
||||
def slug
|
||||
Slug.for(self.display_name, '-')
|
||||
end
|
||||
|
||||
protected
|
||||
def ensure_not_system
|
||||
unless id
|
||||
self.id = [Badge.maximum(:id) + 1, 100].max
|
||||
end
|
||||
end
|
||||
|
||||
def i18n_name
|
||||
self.name.downcase.gsub(' ', '_')
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
|
||||
@ -193,13 +193,17 @@ SQL
|
||||
end
|
||||
|
||||
def topic_url
|
||||
topic_only_relative_url.try(:relative_url)
|
||||
if has_attribute?("topic_slug")
|
||||
Topic.relative_url(topic_id, read_attribute(:topic_slug))
|
||||
else
|
||||
topic_only_relative_url.try(:relative_url)
|
||||
end
|
||||
end
|
||||
|
||||
def description_text
|
||||
return nil unless description
|
||||
|
||||
@@cache ||= LruRedux::ThreadSafeCache.new(100)
|
||||
@@cache ||= LruRedux::ThreadSafeCache.new(1000)
|
||||
@@cache.getset(self.description) do
|
||||
Nokogiri::HTML(self.description).text
|
||||
end
|
||||
@ -283,6 +287,14 @@ SQL
|
||||
set_permissions(permissions)
|
||||
end
|
||||
|
||||
def permissions_params
|
||||
hash = {}
|
||||
category_groups.includes(:group).each do |category_group|
|
||||
hash[category_group.group_name] = category_group.permission_type
|
||||
end
|
||||
hash
|
||||
end
|
||||
|
||||
def apply_permissions
|
||||
if @permissions
|
||||
category_groups.destroy_all
|
||||
@ -370,7 +382,8 @@ SQL
|
||||
end
|
||||
|
||||
def has_children?
|
||||
id && Category.where(parent_category_id: id).exists?
|
||||
@has_children ||= (id && Category.where(parent_category_id: id).exists?) ? :true : :false
|
||||
@has_children == :true
|
||||
end
|
||||
|
||||
def uncategorized?
|
||||
|
||||
@ -2,6 +2,8 @@ class CategoryGroup < ActiveRecord::Base
|
||||
belongs_to :category
|
||||
belongs_to :group
|
||||
|
||||
delegate :name, to: :group, prefix: true
|
||||
|
||||
def self.permission_types
|
||||
@permission_types ||= Enum.new(:full, :create_post, :readonly)
|
||||
end
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
require_dependency 'sass/discourse_stylesheets'
|
||||
require_dependency 'distributed_cache'
|
||||
|
||||
class ColorScheme < ActiveRecord::Base
|
||||
|
||||
def self.hex_cache
|
||||
@hex_cache ||= DistributedCache.new("scheme_hex_for_name")
|
||||
end
|
||||
|
||||
attr_accessor :is_base
|
||||
|
||||
has_many :color_scheme_colors, -> { order('id ASC') }, dependent: :destroy
|
||||
@ -12,6 +17,8 @@ class ColorScheme < ActiveRecord::Base
|
||||
|
||||
after_destroy :destroy_versions
|
||||
after_save :publish_discourse_stylesheet
|
||||
after_save :dump_hex_cache
|
||||
after_destroy :dump_hex_cache
|
||||
|
||||
validates_associated :color_scheme_colors
|
||||
|
||||
@ -64,8 +71,14 @@ class ColorScheme < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def self.hex_for_name(name)
|
||||
# Can't use `where` here because base doesn't allow it
|
||||
(enabled || base).colors.find {|c| c.name == name }.try(:hex)
|
||||
val = begin
|
||||
hex_cache[name] ||= begin
|
||||
# Can't use `where` here because base doesn't allow it
|
||||
(enabled || base).colors.find {|c| c.name == name }.try(:hex) || :nil
|
||||
end
|
||||
end
|
||||
|
||||
val == :nil ? nil : val
|
||||
end
|
||||
|
||||
def colors=(arr)
|
||||
@ -101,6 +114,11 @@ class ColorScheme < ActiveRecord::Base
|
||||
DiscourseStylesheets.cache.clear
|
||||
end
|
||||
|
||||
|
||||
def dump_hex_cache
|
||||
self.class.hex_cache.clear
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
|
||||
@ -15,6 +15,13 @@ class Group < ActiveRecord::Base
|
||||
after_save :update_primary_group
|
||||
after_save :update_title
|
||||
|
||||
after_save :expire_cache
|
||||
after_destroy :expire_cache
|
||||
|
||||
def expire_cache
|
||||
ApplicationSerializer.expire_cache_fragment!("group_names")
|
||||
end
|
||||
|
||||
validate :name_format_validator
|
||||
validates_uniqueness_of :name, case_sensitive: false
|
||||
|
||||
|
||||
@ -91,7 +91,7 @@ class Post < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def limit_posts_per_day
|
||||
if user.first_day_user? && post_number && post_number > 1
|
||||
if user && user.first_day_user? && post_number && post_number > 1
|
||||
RateLimiter.new(user, "first-day-replies-per-day", SiteSetting.max_replies_in_first_day, 1.day.to_i)
|
||||
end
|
||||
end
|
||||
@ -507,6 +507,7 @@ class Post < ActiveRecord::Base
|
||||
}
|
||||
args[:image_sizes] = image_sizes if image_sizes.present?
|
||||
args[:invalidate_oneboxes] = true if invalidate_oneboxes.present?
|
||||
args[:cooking_options] = self.cooking_options
|
||||
Jobs.enqueue(:process_post, args)
|
||||
DiscourseEvent.trigger(:after_trigger_post_process, self)
|
||||
end
|
||||
|
||||
@ -66,7 +66,7 @@ class PostAction < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def self.counts_for(collection, user)
|
||||
return {} if collection.blank?
|
||||
return {} if collection.blank? || !user
|
||||
|
||||
collection_ids = collection.map(&:id)
|
||||
user_id = user.try(:id) || 0
|
||||
|
||||
@ -1,7 +1,17 @@
|
||||
require_dependency 'enum'
|
||||
require_dependency 'distributed_cache'
|
||||
|
||||
class PostActionType < ActiveRecord::Base
|
||||
after_save :expire_cache
|
||||
after_destroy :expire_cache
|
||||
|
||||
def expire_cache
|
||||
ApplicationSerializer.expire_cache_fragment!("post_action_types")
|
||||
ApplicationSerializer.expire_cache_fragment!("post_action_flag_types")
|
||||
end
|
||||
|
||||
class << self
|
||||
|
||||
def ordered
|
||||
order('position asc')
|
||||
end
|
||||
|
||||
@ -11,7 +11,7 @@ class PostAnalyzer
|
||||
def cook(*args)
|
||||
cooked = PrettyText.cook(*args)
|
||||
|
||||
result = Oneboxer.apply(cooked) do |url, _|
|
||||
result = Oneboxer.apply(cooked, topic_id: @topic_id) do |url, _|
|
||||
Oneboxer.invalidate(url) if args.last[:invalidate_oneboxes]
|
||||
Oneboxer.cached_onebox url
|
||||
end
|
||||
|
||||
@ -58,7 +58,7 @@ class Report
|
||||
if filter == :page_view_total
|
||||
ApplicationRequest.where(req_type: [
|
||||
ApplicationRequest.req_types.reject{|k,v| k =~ /mobile/}.map{|k,v| v if k =~ /page_view/}.compact
|
||||
])
|
||||
].flatten)
|
||||
else
|
||||
ApplicationRequest.where(req_type: ApplicationRequest.req_types[filter])
|
||||
end
|
||||
@ -98,6 +98,14 @@ class Report
|
||||
report_about report, User.real, :count_by_signup_date
|
||||
end
|
||||
|
||||
def self.report_profile_views(report)
|
||||
start_date = report.start_date.to_date
|
||||
end_date = report.end_date.to_date
|
||||
basic_report_about report, UserProfileView, :profile_views_by_day, start_date, end_date
|
||||
report.total = UserProfile.sum(:views)
|
||||
report.prev30Days = UserProfileView.where("viewed_at >= ? AND viewed_at < ?", start_date - 30.days, start_date + 1).count
|
||||
end
|
||||
|
||||
def self.report_topics(report)
|
||||
basic_report_about report, Topic, :listable_count_per_day, report.start_date, report.end_date, report.category_id
|
||||
countable = Topic.listable_topics
|
||||
|
||||
@ -75,6 +75,7 @@ class ScreenedIpAddress < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def self.block_admin_login?(user, ip_address)
|
||||
return false unless SiteSetting.use_admin_ip_whitelist
|
||||
return false if user.nil?
|
||||
return false if !user.admin?
|
||||
return false if ScreenedIpAddress.where(action_type: actions[:allow_admin]).count == 0
|
||||
|
||||
@ -13,14 +13,6 @@ class Site
|
||||
SiteSetting
|
||||
end
|
||||
|
||||
def post_action_types
|
||||
PostActionType.ordered
|
||||
end
|
||||
|
||||
def topic_flag_types
|
||||
post_action_types.where(name_key: ['inappropriate', 'spam', 'notify_moderators'])
|
||||
end
|
||||
|
||||
def notification_types
|
||||
Notification.types
|
||||
end
|
||||
@ -29,10 +21,6 @@ class Site
|
||||
TrustLevel.all
|
||||
end
|
||||
|
||||
def groups
|
||||
@groups ||= Group.order(:name).map { |g| { id: g.id, name: g.name } }
|
||||
end
|
||||
|
||||
def user_fields
|
||||
UserField.all
|
||||
end
|
||||
@ -41,16 +29,22 @@ class Site
|
||||
@categories ||= begin
|
||||
categories = Category
|
||||
.secured(@guardian)
|
||||
.includes(:topic_only_relative_url, :subcategories)
|
||||
.joins('LEFT JOIN topics t on t.id = categories.topic_id')
|
||||
.select('categories.*, t.slug topic_slug')
|
||||
.order(:position)
|
||||
|
||||
unless SiteSetting.allow_uncategorized_topics
|
||||
categories = categories.where('categories.id <> ?', SiteSetting.uncategorized_category_id)
|
||||
end
|
||||
|
||||
categories = categories.to_a
|
||||
|
||||
allowed_topic_create = Set.new(Category.topic_create_allowed(@guardian).pluck(:id))
|
||||
with_children = Set.new
|
||||
categories.each do |c|
|
||||
if c.parent_category_id
|
||||
with_children << c.parent_category_id
|
||||
end
|
||||
end
|
||||
|
||||
allowed_topic_create_ids =
|
||||
@guardian.anonymous? ? [] : Category.topic_create_allowed(@guardian).pluck(:id)
|
||||
allowed_topic_create = Set.new(allowed_topic_create_ids)
|
||||
|
||||
by_id = {}
|
||||
|
||||
@ -62,7 +56,7 @@ class Site
|
||||
categories.each do |category|
|
||||
category.notification_level = category_user[category.id]
|
||||
category.permission = CategoryGroup.permission_types[:full] if allowed_topic_create.include?(category.id)
|
||||
category.has_children = category.subcategories.present?
|
||||
category.has_children = with_children.include?(category.id)
|
||||
by_id[category.id] = category
|
||||
end
|
||||
|
||||
@ -91,8 +85,37 @@ class Site
|
||||
}.to_json
|
||||
end
|
||||
|
||||
seq = nil
|
||||
|
||||
if guardian.anonymous?
|
||||
seq = MessageBus.last_id('/site_json')
|
||||
|
||||
cached_json, cached_seq, cached_version = $redis.mget('site_json', 'site_json_seq', 'site_json_version')
|
||||
|
||||
if cached_json && seq == cached_seq.to_i && Discourse.git_version == cached_version
|
||||
return cached_json
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
site = Site.new(guardian)
|
||||
MultiJson.dump(SiteSerializer.new(site, root: false, scope: guardian))
|
||||
json = MultiJson.dump(SiteSerializer.new(site, root: false, scope: guardian))
|
||||
|
||||
if guardian.anonymous?
|
||||
$redis.multi do
|
||||
$redis.setex 'site_json', 1800, json
|
||||
$redis.set 'site_json_seq', seq
|
||||
$redis.set 'site_json_version', Discourse.git_version
|
||||
end
|
||||
end
|
||||
|
||||
json
|
||||
end
|
||||
|
||||
def self.clear_anon_cache!
|
||||
# publishing forces the sequence up
|
||||
# the cache is validated based on the sequence
|
||||
MessageBus.publish('/site_json','')
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@ -5,6 +5,7 @@ require_dependency 'rate_limiter'
|
||||
require_dependency 'text_sentinel'
|
||||
require_dependency 'text_cleaner'
|
||||
require_dependency 'archetype'
|
||||
require_dependency 'html_prettify'
|
||||
|
||||
class Topic < ActiveRecord::Base
|
||||
include ActionView::Helpers::SanitizeHelper
|
||||
@ -164,6 +165,9 @@ class Topic < ActiveRecord::Base
|
||||
cancel_auto_close_job
|
||||
ensure_topic_has_a_category
|
||||
end
|
||||
if title_changed?
|
||||
write_attribute :fancy_title, Topic.fancy_title(title)
|
||||
end
|
||||
end
|
||||
|
||||
after_save do
|
||||
@ -270,17 +274,28 @@ class Topic < ActiveRecord::Base
|
||||
apply_per_day_rate_limit_for("pms", :max_private_messages_per_day)
|
||||
end
|
||||
|
||||
def self.fancy_title(title)
|
||||
escaped = ERB::Util.html_escape(title)
|
||||
return unless escaped
|
||||
HtmlPrettify.render(escaped)
|
||||
end
|
||||
|
||||
def fancy_title
|
||||
sanitized_title = ERB::Util.html_escape(title)
|
||||
return ERB::Util.html_escape(title) unless SiteSetting.title_fancy_entities?
|
||||
|
||||
return unless sanitized_title
|
||||
return sanitized_title unless SiteSetting.title_fancy_entities?
|
||||
unless fancy_title = read_attribute(:fancy_title)
|
||||
|
||||
# We don't always have to require this, if fancy is disabled
|
||||
# see: http://meta.discourse.org/t/pattern-for-defer-loading-gems-and-profiling-with-perftools-rb/4629
|
||||
require 'redcarpet' unless defined? Redcarpet
|
||||
fancy_title = Topic.fancy_title(title)
|
||||
write_attribute(:fancy_title, fancy_title)
|
||||
|
||||
Redcarpet::Render::SmartyPants.render(sanitized_title)
|
||||
unless new_record?
|
||||
# make sure data is set in table, this also allows us to change algorithm
|
||||
# by simply nulling this column
|
||||
exec_sql("UPDATE topics SET fancy_title = :fancy_title where id = :id", id: self.id, fancy_title: fancy_title)
|
||||
end
|
||||
end
|
||||
|
||||
fancy_title
|
||||
end
|
||||
|
||||
def pending_posts_count
|
||||
@ -701,6 +716,7 @@ class Topic < ActiveRecord::Base
|
||||
def title=(t)
|
||||
slug = Slug.for(t.to_s)
|
||||
write_attribute(:slug, slug)
|
||||
write_attribute(:fancy_title, nil)
|
||||
write_attribute(:title,t)
|
||||
end
|
||||
|
||||
@ -720,12 +736,16 @@ class Topic < ActiveRecord::Base
|
||||
self.class.url id, slug, post_number
|
||||
end
|
||||
|
||||
def relative_url(post_number=nil)
|
||||
def self.relative_url(id, slug, post_number=nil)
|
||||
url = "#{Discourse.base_uri}/t/#{slug}/#{id}"
|
||||
url << "/#{post_number}" if post_number.to_i > 1
|
||||
url
|
||||
end
|
||||
|
||||
def relative_url(post_number=nil)
|
||||
Topic.relative_url(id, slug, post_number)
|
||||
end
|
||||
|
||||
def unsubscribe_url
|
||||
"#{url}/unsubscribe"
|
||||
end
|
||||
|
||||
@ -2,8 +2,14 @@ require 'uri'
|
||||
require_dependency 'slug'
|
||||
|
||||
class TopicLink < ActiveRecord::Base
|
||||
MAX_DOMAIN_LENGTH = 100 unless defined? MAX_DOMAIN_LENGTH
|
||||
MAX_URL_LENGTH = 500 unless defined? MAX_URL_LENGTH
|
||||
|
||||
def self.max_domain_length
|
||||
100
|
||||
end
|
||||
|
||||
def self.max_url_length
|
||||
500
|
||||
end
|
||||
|
||||
belongs_to :topic
|
||||
belongs_to :user
|
||||
@ -147,8 +153,8 @@ class TopicLink < ActiveRecord::Base
|
||||
reflected_post = Post.find_by(topic_id: topic_id, post_number: post_number.to_i)
|
||||
end
|
||||
|
||||
next if url && url.length > MAX_URL_LENGTH
|
||||
next if parsed && parsed.host && parsed.host.length > MAX_DOMAIN_LENGTH
|
||||
url = url[0...TopicLink.max_url_length]
|
||||
next if parsed && parsed.host && parsed.host.length > TopicLink.max_domain_length
|
||||
|
||||
added_urls << url
|
||||
TopicLink.create(post_id: post.id,
|
||||
|
||||
@ -13,7 +13,7 @@ class TopicLinkClick < ActiveRecord::Base
|
||||
|
||||
# Create a click from a URL and post_id
|
||||
def self.create_from(args={})
|
||||
url = args[:url]
|
||||
url = args[:url][0...TopicLink.max_url_length]
|
||||
return nil if url.blank?
|
||||
|
||||
uri = URI.parse(url) rescue nil
|
||||
|
||||
@ -128,13 +128,9 @@ class TopicTrackingState
|
||||
# cycles from usual requests
|
||||
#
|
||||
#
|
||||
sql = report_raw_sql(topic_id: topic_id)
|
||||
|
||||
sql = <<SQL
|
||||
WITH x AS (
|
||||
#{sql}
|
||||
) SELECT * FROM x LIMIT #{SiteSetting.max_tracked_new_unread.to_i}
|
||||
SQL
|
||||
sql = report_raw_sql(topic_id: topic_id, skip_unread: true, skip_order: true)
|
||||
sql << "\nUNION ALL\n\n"
|
||||
sql << report_raw_sql(topic_id: topic_id, skip_new: true, skip_order: true)
|
||||
|
||||
SqlBuilder.new(sql)
|
||||
.map_exec(TopicTrackingState, user_id: user_id, topic_id: topic_id)
|
||||
@ -198,7 +194,11 @@ SQL
|
||||
sql << " AND topics.id = :topic_id"
|
||||
end
|
||||
|
||||
sql << " ORDER BY topics.bumped_at DESC"
|
||||
unless opts && opts[:skip_order]
|
||||
sql << " ORDER BY topics.bumped_at DESC"
|
||||
end
|
||||
|
||||
sql
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@ -306,21 +306,6 @@ SQL
|
||||
TopicUser.exec_sql(sql, user_id: user_id, count: count)
|
||||
end
|
||||
|
||||
def self.unread_cap_key
|
||||
"unread_cap_user".freeze
|
||||
end
|
||||
|
||||
def self.cap_unread_later(user_id)
|
||||
$redis.hset TopicUser.unread_cap_key, user_id, ""
|
||||
end
|
||||
|
||||
def self.cap_unread_backlog!
|
||||
$redis.hkeys(unread_cap_key).map(&:to_i).each do |user_id|
|
||||
cap_unread!(user_id, (SiteSetting.max_tracked_new_unread * (2/5.0)).to_i)
|
||||
$redis.hdel unread_cap_key, user_id
|
||||
end
|
||||
end
|
||||
|
||||
def self.ensure_consistency!(topic_id=nil)
|
||||
update_post_action_cache(topic_id: topic_id)
|
||||
|
||||
|
||||
@ -95,15 +95,16 @@ class Upload < ActiveRecord::Base
|
||||
when "avatar"
|
||||
allow_animation = SiteSetting.allow_animated_avatars
|
||||
width = height = Discourse.avatar_sizes.max
|
||||
OptimizedImage.resize(file.path, file.path, width, height, filename: filename, allow_animation: allow_animation)
|
||||
when "profile_background"
|
||||
max_width = 850 * max_pixel_ratio
|
||||
width, height = ImageSizer.resize(w, h, max_width: max_width, max_height: max_width)
|
||||
OptimizedImage.downsize(file.path, file.path, "#{width}x#{height}", filename: filename, allow_animation: allow_animation)
|
||||
when "card_background"
|
||||
max_width = 590 * max_pixel_ratio
|
||||
width, height = ImageSizer.resize(w, h, max_width: max_width, max_height: max_width)
|
||||
OptimizedImage.downsize(file.path, file.path, "#{width}x#{height}", filename: filename, allow_animation: allow_animation)
|
||||
end
|
||||
|
||||
OptimizedImage.resize(file.path, file.path, width, height, filename: filename, allow_animation: allow_animation)
|
||||
end
|
||||
|
||||
# optimize image
|
||||
|
||||
@ -7,6 +7,7 @@ class UserHistory < ActiveRecord::Base
|
||||
|
||||
belongs_to :post
|
||||
belongs_to :topic
|
||||
belongs_to :category
|
||||
|
||||
validates_presence_of :action
|
||||
|
||||
@ -39,7 +40,10 @@ class UserHistory < ActiveRecord::Base
|
||||
:custom,
|
||||
:custom_staff,
|
||||
:anonymize_user,
|
||||
:reviewed_post)
|
||||
:reviewed_post,
|
||||
:change_category_settings,
|
||||
:delete_category,
|
||||
:create_category)
|
||||
end
|
||||
|
||||
# Staff actions is a subset of all actions, used to audit actions taken by staff users.
|
||||
@ -61,7 +65,10 @@ class UserHistory < ActiveRecord::Base
|
||||
:change_username,
|
||||
:custom_staff,
|
||||
:anonymize_user,
|
||||
:reviewed_post]
|
||||
:reviewed_post,
|
||||
:change_category_settings,
|
||||
:delete_category,
|
||||
:create_category]
|
||||
end
|
||||
|
||||
def self.staff_action_ids
|
||||
@ -144,11 +151,13 @@ end
|
||||
# admin_only :boolean default(FALSE)
|
||||
# post_id :integer
|
||||
# custom_type :string(255)
|
||||
# category_id :integer
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_user_histories_on_acting_user_id_and_action_and_id (acting_user_id,action,id)
|
||||
# index_user_histories_on_action_and_id (action,id)
|
||||
# index_user_histories_on_category_id (category_id)
|
||||
# index_user_histories_on_subject_and_id (subject,id)
|
||||
# index_user_histories_on_target_user_id_and_id (target_user_id,id)
|
||||
#
|
||||
|
||||
@ -7,6 +7,7 @@ class UserProfile < ActiveRecord::Base
|
||||
after_save :trigger_badges
|
||||
|
||||
belongs_to :card_image_badge, class_name: 'Badge'
|
||||
has_many :user_profile_views, dependent: :destroy
|
||||
|
||||
BAKED_VERSION = 1
|
||||
|
||||
@ -112,6 +113,7 @@ end
|
||||
# badge_granted_title :boolean default(FALSE)
|
||||
# card_background :string(255)
|
||||
# card_image_badge_id :integer
|
||||
# views :integer default(0), not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
|
||||
47
app/models/user_profile_view.rb
Normal file
47
app/models/user_profile_view.rb
Normal file
@ -0,0 +1,47 @@
|
||||
class UserProfileView < ActiveRecord::Base
|
||||
validates_presence_of :user_profile_id, :ip_address, :viewed_at
|
||||
|
||||
belongs_to :user_profile
|
||||
|
||||
def self.add(user_profile_id, ip, user_id=nil, at=nil, skip_redis=false)
|
||||
at ||= Time.zone.now
|
||||
redis_key = "user-profile-view:#{user_profile_id}:#{at.to_date}"
|
||||
if user_id
|
||||
redis_key << ":user-#{user_id}"
|
||||
else
|
||||
redis_key << ":ip-#{ip}"
|
||||
end
|
||||
|
||||
if skip_redis || $redis.setnx(redis_key, '1')
|
||||
skip_redis || $redis.expire(redis_key, SiteSetting.user_profile_view_duration_hours.hours)
|
||||
|
||||
self.transaction do
|
||||
sql = "INSERT INTO user_profile_views (user_profile_id, ip_address, viewed_at, user_id)
|
||||
SELECT :user_profile_id, :ip_address, :viewed_at, :user_id
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM user_profile_views
|
||||
/*where*/
|
||||
)"
|
||||
|
||||
builder = SqlBuilder.new(sql)
|
||||
|
||||
if !user_id
|
||||
builder.where("viewed_at = :viewed_at AND ip_address = :ip_address AND user_profile_id = :user_profile_id AND user_id IS NULL")
|
||||
else
|
||||
builder.where("viewed_at = :viewed_at AND user_id = :user_id AND user_profile_id = :user_profile_id")
|
||||
end
|
||||
|
||||
result = builder.exec(user_profile_id: user_profile_id, ip_address: ip, viewed_at: at, user_id: user_id)
|
||||
|
||||
if result.cmd_tuples > 0
|
||||
UserProfile.find(user_profile_id).increment!(:views)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.profile_views_by_day(start_date, end_date)
|
||||
profile_views = self.where("viewed_at >= ? AND viewed_at < ?", start_date, end_date + 1.day)
|
||||
profile_views.group("date(viewed_at)").order("date(viewed_at)").count
|
||||
end
|
||||
end
|
||||
@ -1,3 +1,28 @@
|
||||
require 'distributed_cache'
|
||||
|
||||
class ApplicationSerializer < ActiveModel::Serializer
|
||||
embed :ids, include: true
|
||||
|
||||
class CachedFragment
|
||||
def initialize(json)
|
||||
@json = json
|
||||
end
|
||||
def as_json(*_args)
|
||||
@json
|
||||
end
|
||||
end
|
||||
|
||||
def self.expire_cache_fragment!(name)
|
||||
fragment_cache.delete(name)
|
||||
end
|
||||
|
||||
def self.fragment_cache
|
||||
@cache ||= DistributedCache.new("am_serializer_fragment_cache")
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def cache_fragment(name)
|
||||
ApplicationSerializer.fragment_cache[name] ||= yield
|
||||
end
|
||||
end
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
class BadgeSerializer < ApplicationSerializer
|
||||
attributes :id, :name, :description, :grant_count, :allow_title,
|
||||
:multiple_grant, :icon, :image, :listable, :enabled, :badge_grouping_id,
|
||||
:system, :long_description
|
||||
:system, :long_description, :slug
|
||||
|
||||
has_one :badge_type
|
||||
|
||||
@ -12,17 +12,4 @@ class BadgeSerializer < ApplicationSerializer
|
||||
def include_long_description?
|
||||
options[:include_long_description]
|
||||
end
|
||||
|
||||
def long_description
|
||||
if object.long_description.present?
|
||||
object.long_description
|
||||
else
|
||||
key = "badges.long_descriptions.#{object.name.downcase.gsub(" ", "_")}"
|
||||
if I18n.exists?(key)
|
||||
I18n.t(key)
|
||||
else
|
||||
""
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -9,15 +9,15 @@ class BasicPostSerializer < ApplicationSerializer
|
||||
:cooked_hidden
|
||||
|
||||
def name
|
||||
object.user.try(:name)
|
||||
object.user && object.user.name
|
||||
end
|
||||
|
||||
def username
|
||||
object.user.try(:username)
|
||||
object.user && object.user.username
|
||||
end
|
||||
|
||||
def avatar_template
|
||||
object.user.try(:avatar_template)
|
||||
object.user && object.user.avatar_template
|
||||
end
|
||||
|
||||
def cooked_hidden
|
||||
|
||||
@ -74,7 +74,7 @@ class PostSerializer < BasicPostSerializer
|
||||
end
|
||||
|
||||
def topic_slug
|
||||
object.try(:topic).try(:slug)
|
||||
object.topic && object.topic.slug
|
||||
end
|
||||
|
||||
def include_topic_title?
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user