Version bump

This commit is contained in:
Neil Lalonde 2015-10-02 11:11:33 -04:00
commit a33dc7403f
238 changed files with 3408 additions and 1045 deletions

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -180,7 +180,6 @@ td.flaggers td {
.result-message {
display: inline-block;
padding-left: 10px;
padding-top: 5px;
}
.username {
input[type=text] {

View File

@ -248,7 +248,7 @@ aside.onebox.twitterstatus .onebox-body {
.thumbnail {
float: left;
}
.tweet {
p, .tweet {
float: left;
display: inline-block;
white-space: pre-wrap;

View File

@ -9,9 +9,6 @@
.badge-wrapper {
float: left;
&.bullet {
margin-top: 5px;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View 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

View File

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

View File

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

View File

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

View File

@ -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?("//")

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ class AdminDashboardData
GLOBAL_REPORTS ||= [
'visits',
'signups',
'profile_views',
'topics',
'posts',
'time_to_first_response',

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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