Merge remote-tracking branch 'upstream/master' into update-locale-vietnamese

* upstream/master: (185 commits)
  SECURITY: Upgrade rails.
  FIX: new user summary page was broken
  Version bump to v1.5.0.beta9
  Remove addressable from Discourse.
  UX: change glyph when inviting existing user to a topic
  FIX: Allow for large free disk space
  Revert "FIX: disk_space should be a BigDecimal to handle large disk (closes #3923)"
  UX: improve styling of messages and mobile view of messages
  FIX: correct counts on user summary
  FIX: link to filtered down list of badges from summary FEATURE: pick featured badges in summary page
  FIX: do not allow new email to be duplicate FIX: return proper error message when email already exists
  retain unactivated accounts a bit longer default
  FEATURE: blocked users can send and reply to private messages from staff
  Remove Arel patch that has been merged upstream.
  correct path
  little typo
  FIX: Missing tag in CSS.
  PERF: remove 10-20ms of work from every page view
  FIX: remove green background for wiki (this can be re-added via a customization if needed)
  Hotfix for unsubscribe via email
  ...

# Conflicts:
#	.tx/config
This commit is contained in:
Khoa, Le Ngoc 2016-01-26 12:44:29 +07:00
commit 06e637fc4a
438 changed files with 16120 additions and 10710 deletions

View File

@ -1,6 +1,6 @@
[main]
host = https://www.transifex.com
lang_map = es_ES: es, fr_FR: fr, ko_KR: ko, pt_PT: pt, vi_VN: vi
lang_map = es_ES: es, fr_FR: fr, ko_KR: ko, pt_PT: pt, sk_SK: sk, vi_VN: vi
[discourse-org.clientenyml]
file_filter = config/locales/client.<lang>.yml

10
Gemfile
View File

@ -46,11 +46,11 @@ gem 'active_model_serializers', '~> 0.8.3'
gem 'onebox'
gem 'ember-rails'
gem 'ember-source', '1.12.1'
gem 'ember-source', '1.12.2'
gem 'barber'
gem 'babel-transpiler'
gem 'message_bus'
gem 'message_bus', '2.0.0.beta.2'
gem 'rails_multisite'
@ -83,7 +83,9 @@ gem 'omniauth-twitter'
gem 'omniauth-github-discourse', require: 'omniauth-github'
gem 'omniauth-oauth2', require: false
gem 'omniauth-google-oauth2'
# this removes the dependency on 'addressable'
gem 'omniauth-google-oauth2', git: 'git://github.com/zquestz/omniauth-google-oauth2.git', ref: 'b492c4bb8286d35'
gem 'oj'
gem 'pg'
gem 'pry-rails', require: false
@ -183,7 +185,7 @@ begin
gem 'stackprof', require: false, platform: [:mri_21, :mri_22, :mri_23]
gem 'memory_profiler', require: false, platform: [:mri_21, :mri_22, :mri_23]
rescue Bundler::GemfileError
begin
begin
STDERR.puts "You are running an old version of bundler, please upgrade bundler ASAP, if you are using Discourse docker, rebuild your container."
gem 'stackprof', require: false, platform: [:mri_21, :mri_22]
gem 'memory_profiler', require: false, platform: [:mri_21, :mri_22]

View File

@ -1,54 +1,65 @@
GIT
remote: git://github.com/zquestz/omniauth-google-oauth2.git
revision: b492c4bb8286d35ae1168d7f2e5c57769bfe45a0
ref: b492c4bb8286d35
specs:
omniauth-google-oauth2 (0.3.0)
jwt (~> 1.0)
multi_json (~> 1.3)
omniauth (>= 1.1.1)
omniauth-oauth2 (>= 1.3.1)
GEM
remote: https://rubygems.org/
specs:
actionmailer (4.2.5)
actionpack (= 4.2.5)
actionview (= 4.2.5)
activejob (= 4.2.5)
actionmailer (4.2.5.1)
actionpack (= 4.2.5.1)
actionview (= 4.2.5.1)
activejob (= 4.2.5.1)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 1.0, >= 1.0.5)
actionpack (4.2.5)
actionview (= 4.2.5)
activesupport (= 4.2.5)
actionpack (4.2.5.1)
actionview (= 4.2.5.1)
activesupport (= 4.2.5.1)
rack (~> 1.6)
rack-test (~> 0.6.2)
rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (4.2.5)
activesupport (= 4.2.5)
actionview (4.2.5.1)
activesupport (= 4.2.5.1)
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)
activejob (4.2.5)
activesupport (= 4.2.5)
activejob (4.2.5.1)
activesupport (= 4.2.5.1)
globalid (>= 0.3.0)
activemodel (4.2.5)
activesupport (= 4.2.5)
activemodel (4.2.5.1)
activesupport (= 4.2.5.1)
builder (~> 3.1)
activerecord (4.2.5)
activemodel (= 4.2.5)
activesupport (= 4.2.5)
activerecord (4.2.5.1)
activemodel (= 4.2.5.1)
activesupport (= 4.2.5.1)
arel (~> 6.0)
activesupport (4.2.5)
activesupport (4.2.5.1)
i18n (~> 0.7)
json (~> 1.7, >= 1.7.7)
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
annotate (2.6.10)
activerecord (>= 3.2, <= 4.3)
annotate (2.7.0)
activerecord (>= 3.2, < 6.0)
rake (~> 10.4)
arel (6.0.3)
aws-sdk (2.1.29)
aws-sdk-resources (= 2.1.29)
aws-sdk-core (2.1.29)
aws-sdk (2.2.9)
aws-sdk-resources (= 2.2.9)
aws-sdk-core (2.2.9)
jmespath (~> 1.0)
aws-sdk-resources (2.1.29)
aws-sdk-core (= 2.1.29)
babel-source (5.8.19)
aws-sdk-resources (2.2.9)
aws-sdk-core (= 2.2.9)
babel-source (5.8.34)
babel-transpiler (0.7.0)
babel-source (>= 4.0, < 6)
execjs (~> 2.0)
@ -62,7 +73,7 @@ GEM
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
builder (3.2.2)
byebug (6.0.2)
byebug (8.2.1)
certified (1.0.0)
coderay (1.1.0)
concurrent-ruby (1.0.0)
@ -89,15 +100,15 @@ GEM
ember-source (>= 1.1.0)
jquery-rails (>= 1.0.17)
railties (>= 3.1)
ember-source (1.12.1)
ember-source (1.12.2)
erubis (2.7.0)
eventmachine (1.0.8)
excon (0.45.4)
execjs (2.6.0)
exifr (1.2.3.1)
exifr (1.2.4)
fabrication (2.9.8)
fakeweb (1.3.0)
faraday (0.9.1)
faraday (0.9.2)
multipart-post (>= 1.2, < 3)
fast_blank (1.0.0)
fast_stack (0.1.0)
@ -115,15 +126,15 @@ GEM
thor (~> 0.19.1)
fspath (2.1.1)
gctools (0.2.3)
given_core (3.5.4)
given_core (3.7.1)
sorcerer (>= 0.3.7)
globalid (0.3.6)
activesupport (>= 4.1.0)
guess_html_encoding (0.0.11)
hashie (3.4.2)
highline (1.7.7)
hashie (3.4.3)
highline (1.7.8)
hike (1.2.3)
hiredis (0.6.0)
hiredis (0.6.1)
htmlentities (4.3.4)
http-cookie (1.0.2)
domain_name (~> 0.5)
@ -142,12 +153,12 @@ GEM
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (1.8.3)
jwt (1.5.1)
jwt (1.5.2)
kgio (2.10.0)
librarian (0.1.2)
highline
thor (~> 0.15)
libv8 (3.16.14.11)
libv8 (3.16.14.13)
listen (0.7.3)
logster (1.0.1)
loofah (2.0.3)
@ -155,28 +166,28 @@ GEM
lru_redux (1.1.0)
mail (2.6.3)
mime-types (>= 1.16, < 3)
memory_profiler (0.9.4)
message_bus (1.1.1)
memory_profiler (0.9.6)
message_bus (2.0.0.beta.2)
rack (>= 1.1.3)
redis
metaclass (0.0.4)
method_source (0.8.2)
mime-types (2.99)
mini_portile2 (2.0.0)
minitest (5.8.3)
minitest (5.8.4)
mocha (1.1.0)
metaclass (~> 0.0.1)
mock_redis (0.15.2)
mock_redis (0.15.4)
moneta (0.8.0)
msgpack (0.6.2)
msgpack (0.7.4)
multi_json (1.11.2)
multi_xml (0.5.5)
multipart-post (2.0.0)
mustache (1.0.2)
netrc (0.11.0)
nokogiri (1.6.7.1)
nokogiri (1.6.7.2)
mini_portile2 (~> 2.0.0.rc2)
nokogumbo (1.4.1)
nokogumbo (1.4.7)
nokogiri
oauth (0.4.7)
oauth2 (1.0.0)
@ -185,18 +196,15 @@ GEM
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (~> 1.2)
oj (2.12.14)
omniauth (1.2.2)
oj (2.14.3)
omniauth (1.3.1)
hashie (>= 1.2, < 4)
rack (~> 1.0)
omniauth-facebook (2.0.1)
rack (>= 1.0, < 3)
omniauth-facebook (3.0.0)
omniauth-oauth2 (~> 1.2)
omniauth-github-discourse (1.1.2)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.1)
omniauth-google-oauth2 (0.2.5)
omniauth (> 1.0)
omniauth-oauth2 (~> 1.1)
omniauth-oauth (1.1.0)
oauth
omniauth (~> 1.0)
@ -209,7 +217,7 @@ GEM
omniauth-twitter (1.2.1)
json (~> 1.3)
omniauth-oauth (~> 1.1)
onebox (1.5.31)
onebox (1.5.33)
moneta (~> 0.8)
multi_json (~> 1.11)
mustache
@ -217,9 +225,9 @@ GEM
openid-redis-store (0.0.2)
redis
ruby-openid
pg (0.18.3)
progress (3.1.0)
pry (0.10.1)
pg (0.18.4)
progress (3.1.1)
pry (0.10.3)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
slop (~> 3.4)
@ -227,10 +235,10 @@ GEM
pry (>= 0.9.10, < 0.11.0)
pry-rails (0.3.4)
pry (>= 0.9.10)
puma (2.14.0)
r2 (0.2.5)
puma (2.15.3)
r2 (0.2.6)
rack (1.6.4)
rack-mini-profiler (0.9.7)
rack-mini-profiler (0.9.8)
rack (>= 1.1.3)
rack-openid (1.3.1)
rack (>= 1.1.0)
@ -239,16 +247,16 @@ GEM
rack
rack-test (0.6.3)
rack (>= 1.0)
rails (4.2.5)
actionmailer (= 4.2.5)
actionpack (= 4.2.5)
actionview (= 4.2.5)
activejob (= 4.2.5)
activemodel (= 4.2.5)
activerecord (= 4.2.5)
activesupport (= 4.2.5)
rails (4.2.5.1)
actionmailer (= 4.2.5.1)
actionpack (= 4.2.5.1)
actionview (= 4.2.5.1)
activejob (= 4.2.5.1)
activemodel (= 4.2.5.1)
activerecord (= 4.2.5.1)
activesupport (= 4.2.5.1)
bundler (>= 1.3.0, < 2.0)
railties (= 4.2.5)
railties (= 4.2.5.1)
sprockets-rails
rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha)
@ -256,21 +264,21 @@ GEM
activesupport (>= 4.2.0.beta, < 5.0)
nokogiri (~> 1.6.0)
rails-deprecated_sanitizer (>= 1.0.1)
rails-html-sanitizer (1.0.2)
rails-html-sanitizer (1.0.3)
loofah (~> 2.0)
rails-observers (0.1.2)
activemodel (~> 4.0)
rails_multisite (1.0.3)
railties (4.2.5)
actionpack (= 4.2.5)
activesupport (= 4.2.5)
railties (4.2.5.1)
actionpack (= 4.2.5.1)
activesupport (= 4.2.5.1)
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
raindrops (0.15.0)
rake (10.4.2)
rake (10.5.0)
rake-compiler (0.9.5)
rake
rb-fsevent (0.9.6)
rb-fsevent (0.9.7)
rb-inotify (0.9.5)
ffi (>= 0.5.0)
rbtrace (0.4.7)
@ -296,16 +304,16 @@ GEM
rspec-expectations (3.2.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.2.0)
rspec-given (3.5.4)
given_core (= 3.5.4)
rspec (>= 2.12)
rspec-given (3.7.1)
given_core (= 3.7.1)
rspec (>= 2.14.0)
rspec-html-matchers (0.7.0)
nokogiri (~> 1)
rspec (~> 3)
rspec-mocks (3.2.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.2.0)
rspec-rails (3.2.1)
rspec-rails (3.2.3)
actionpack (>= 3.0, < 4.3)
activesupport (>= 3.0, < 4.3)
railties (>= 3.0, < 4.3)
@ -319,10 +327,10 @@ GEM
ruby-readability (0.7.0)
guess_html_encoding (>= 0.0.4)
nokogiri (>= 1.6.0)
sanitize (4.0.0)
sanitize (4.0.1)
crass (~> 1.0.2)
nokogiri (>= 1.4.4)
nokogumbo (= 1.4.1)
nokogumbo (~> 1.4.1)
sass (3.2.19)
sass-rails (4.0.5)
railties (>= 4.0.0, < 5.0)
@ -336,17 +344,16 @@ GEM
shoulda-context (~> 1.0, >= 1.0.1)
shoulda-matchers (>= 1.4.1, < 3.0)
shoulda-context (1.2.1)
shoulda-matchers (2.7.0)
shoulda-matchers (2.8.0)
activesupport (>= 3.0.0)
sidekiq (4.0.1)
sidekiq (4.0.2)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
json (~> 1.0)
redis (~> 3.2, >= 3.2.1)
sidekiq-statistic (1.2.0)
sidekiq (>= 3.3.4, < 5)
simple-rss (1.3.1)
simplecov (0.10.0)
simplecov (0.11.1)
docile (~> 1.1.0)
json (~> 1.8)
simplecov-html (~> 0.10.0)
@ -382,7 +389,7 @@ GEM
thread_safe (0.3.5)
tilt (1.4.1)
timecop (0.8.0)
trollop (2.1.1)
trollop (2.1.2)
tzinfo (1.2.2)
thread_safe (~> 0.1)
uglifier (2.7.2)
@ -390,8 +397,8 @@ GEM
json (>= 1.8.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.6)
unicorn (4.9.0)
unf_ext (0.0.7.1)
unicorn (5.0.1)
kgio (~> 2.6)
rack
raindrops (~> 0.7)
@ -412,7 +419,7 @@ DEPENDENCIES
discourse-qunit-rails
discourse_email_parser
ember-rails
ember-source (= 1.12.1)
ember-source (= 1.12.2)
excon
fabrication (= 2.9.8)
fakeweb (~> 1.3.0)
@ -433,7 +440,7 @@ DEPENDENCIES
lru_redux
mail
memory_profiler
message_bus
message_bus (= 2.0.0.beta.2)
mime-types
minitest
mocha
@ -445,7 +452,7 @@ DEPENDENCIES
omniauth
omniauth-facebook
omniauth-github-discourse
omniauth-google-oauth2
omniauth-google-oauth2!
omniauth-oauth2
omniauth-openid
omniauth-twitter

View File

@ -56,7 +56,7 @@ Plus *lots* of Ruby Gems, a complete list of which is at [/master/Gemfile](https
## Contributing
[![Build Status](https://travis-ci.org/discourse/discourse.svg)](https://travis-ci.org/discourse/discourse)
[![Build Status](https://api.travis-ci.org/discourse/discourse.svg?branch=master)](https://travis-ci.org/discourse/discourse)
[![Code Climate](https://codeclimate.com/github/discourse/discourse.svg)](https://codeclimate.com/github/discourse/discourse)
Discourse is **100% free** and **open source**. We encourage and support an active, healthy community that

View File

@ -1,3 +0,0 @@
import AdminEmailSkippedController from "admin/controllers/admin-email-skipped";
export default AdminEmailSkippedController.extend();

View File

@ -0,0 +1,11 @@
import IncomingEmail from 'admin/models/incoming-email';
export default Ember.Controller.extend({
loadMore() {
return IncomingEmail.findAll(this.get("filter"), this.get("model.length"))
.then(incoming => {
if (incoming.length < 50) { this.get("model").set("allLoaded", true); }
this.get("model").addObjects(incoming);
});
}
});

View File

@ -0,0 +1,11 @@
import EmailLog from 'admin/models/email-log';
export default Ember.Controller.extend({
loadMore() {
return EmailLog.findAll(this.get("filter"), this.get("model.length"))
.then(logs => {
if (logs.length < 50) { this.get("model").set("allLoaded", true); }
this.get("model").addObjects(logs);
});
}
});

View File

@ -0,0 +1,9 @@
import AdminEmailIncomingsController from 'admin/controllers/admin-email-incomings';
import debounce from 'discourse/lib/debounce';
import IncomingEmail from 'admin/models/incoming-email';
export default AdminEmailIncomingsController.extend({
filterIncomingEmails: debounce(function() {
IncomingEmail.findAll(this.get("filter")).then(incomings => this.set("model", incomings));
}, 250).observes("filter.{from,to,subject}")
});

View File

@ -0,0 +1,9 @@
import AdminEmailIncomingsController from 'admin/controllers/admin-email-incomings';
import debounce from 'discourse/lib/debounce';
import IncomingEmail from 'admin/models/incoming-email';
export default AdminEmailIncomingsController.extend({
filterIncomingEmails: debounce(function() {
IncomingEmail.findAll(this.get("filter")).then(incomings => this.set("model", incomings));
}, 250).observes("filter.{from,to,subject,error}")
});

View File

@ -1,12 +1,9 @@
import AdminEmailLogsController from 'admin/controllers/admin-email-logs';
import debounce from 'discourse/lib/debounce';
import EmailLog from 'admin/models/email-log';
export default Ember.Controller.extend({
export default AdminEmailLogsController.extend({
filterEmailLogs: debounce(function() {
var self = this;
EmailLog.findAll(this.get("filter")).then(function(logs) {
self.set("model", logs);
});
}, 250).observes("filter.user", "filter.address", "filter.type", "filter.reply_key")
EmailLog.findAll(this.get("filter")).then(logs => this.set("model", logs));
}, 250).observes("filter.{user,address,type,reply_key}")
});

View File

@ -1,8 +1,9 @@
import AdminEmailLogsController from 'admin/controllers/admin-email-logs';
import debounce from 'discourse/lib/debounce';
import EmailLog from 'admin/models/email-log';
export default Ember.Controller.extend({
export default AdminEmailLogsController.extend({
filterEmailLogs: debounce(function() {
const EmailLog = require('admin/models/email-log').default;
EmailLog.findAll(this.get("filter")).then(logs => this.set("model", logs));
}, 250).observes("filter.user", "filter.address", "filter.type", "filter.skipped_reason")
}, 250).observes("filter.{user,address,type,skipped_reason}")
});

View File

@ -1,5 +1,6 @@
import { exportEntity } from 'discourse/lib/export-csv';
import { outputExportResult } from 'discourse/lib/export-result';
import Report from 'admin/models/report';
export default Ember.Controller.extend({
viewMode: 'table',
@ -20,9 +21,9 @@ export default Ember.Controller.extend({
var q;
this.set("refreshing", true);
if (this.get('categoryId') === "all") {
q = Discourse.Report.find(this.get("model.type"), this.get("startDate"), this.get("endDate"));
q = Report.find(this.get("model.type"), this.get("startDate"), this.get("endDate"));
} else {
q = Discourse.Report.find(this.get("model.type"), this.get("startDate"), this.get("endDate"), this.get("categoryId"));
q = Report.find(this.get("model.type"), this.get("startDate"), this.get("endDate"), this.get("categoryId"));
}
q.then(m => this.set("model", m)).finally(() => this.set("refreshing", false));
},

View File

@ -2,15 +2,13 @@ export default Ember.Controller.extend({
needs: ['modal'],
modelChanged: function(){
var grouping = Em.Object.extend({});
var model = this.get('model');
var copy = Em.A();
const model = this.get('model');
const copy = Em.A();
const store = this.store;
if(model){
model.forEach(function(o){
copy.pushObject(grouping.create(o));
copy.pushObject(store.createRecord('badge-grouping', o));
});
}
@ -18,8 +16,8 @@ export default Ember.Controller.extend({
}.observes('model'),
moveItem: function(item, delta){
var copy = this.get('workingCopy');
var index = copy.indexOf(item);
const copy = this.get('workingCopy');
const index = copy.indexOf(item);
if (index + delta < 0 || index + delta >= copy.length){
return;
}
@ -50,14 +48,14 @@ export default Ember.Controller.extend({
item.set("editing", false);
},
add: function(){
var obj = Em.Object.create({editing: true, name: "Enter Name"});
const obj = this.store.createRecord('badge-grouping', {editing: true, name: I18n.t('admin.badges.badge_grouping')});
this.get('workingCopy').pushObject(obj);
},
saveAll: function(){
var self = this;
const self = this;
var items = this.get('workingCopy');
var groupIds = items.map(function(i){return i.get("id") || -1;});
var names = items.map(function(i){return i.get("name");});
const groupIds = items.map(function(i){return i.get("id") || -1;});
const names = items.map(function(i){return i.get("name");});
Discourse.ajax('/admin/badges/badge_groupings',{
data: {ids: groupIds, names: names},
@ -66,14 +64,13 @@ export default Ember.Controller.extend({
items = self.get("model");
items.clear();
data.badge_groupings.forEach(function(g){
items.pushObject(Em.Object.create(g));
items.pushObject(self.store.createRecord('badge-grouping', g));
});
self.set('model', null);
self.set('workingCopy', null);
self.send('closeModal');
},function(){
// TODO we can do better
bootbox.alert("Something went wrong");
bootbox.alert(I18n.t('generic_error'));
});
}
}

View File

@ -9,6 +9,8 @@ const AdminUser = Discourse.User.extend({
customGroups: Em.computed.filter("groups", (g) => !g.automatic && Group.create(g)),
automaticGroups: Em.computed.filter("groups", (g) => g.automatic && Group.create(g)),
canViewProfile: Ember.computed.or("active", "staged"),
generateApiKey() {
const self = this;
return Discourse.ajax("/admin/users/" + this.get('id') + "/generate_api_key", {
@ -264,6 +266,7 @@ const AdminUser = Discourse.User.extend({
},
unblock() {
this.set('blockingUser', true);
return Discourse.ajax('/admin/users/' + this.id + '/unblock', {
type: 'PUT'
}).then(function() {
@ -275,14 +278,33 @@ const AdminUser = Discourse.User.extend({
},
block() {
return Discourse.ajax('/admin/users/' + this.id + '/block', {
type: 'PUT'
}).then(function() {
window.location.reload();
}).catch(function(e) {
var error = I18n.t('admin.user.block_failed', { error: "http: " + e.status + " - " + e.body });
bootbox.alert(error);
});
const user = this,
message = I18n.t("admin.user.block_confirm");
const performBlock = function() {
user.set('blockingUser', true);
return Discourse.ajax('/admin/users/' + user.id + '/block', {
type: 'PUT'
}).then(function() {
window.location.reload();
}).catch(function(e) {
var error = I18n.t('admin.user.block_failed', { error: "http: " + e.status + " - " + e.body });
bootbox.alert(error);
user.set('blockingUser', false);
});
};
const buttons = [{
"label": I18n.t("composer.cancel"),
"class": "cancel",
"link": true
}, {
"label": '<i class="fa fa-exclamation-triangle"></i>' + I18n.t('admin.user.block_accept'),
"class": "btn btn-danger",
"callback": function() { performBlock(); }
}];
bootbox.dialog(message, buttons, { "classes": "delete-user-modal" });
},
sendActivationEmail() {

View File

@ -4,7 +4,7 @@ const EmailLog = Discourse.Model.extend({});
EmailLog.reopenClass({
create: function(attrs) {
create(attrs) {
attrs = attrs || {};
if (attrs.user) {
@ -14,16 +14,15 @@ EmailLog.reopenClass({
return this._super(attrs);
},
findAll: function(filter) {
findAll(filter, offset) {
filter = filter || {};
var status = filter.status || "all";
offset = offset || 0;
const status = filter.status || "sent";
filter = _.omit(filter, "status");
return Discourse.ajax("/admin/email/" + status + ".json", { data: filter }).then(function(logs) {
return _.map(logs, function (log) {
return EmailLog.create(log);
});
});
return Discourse.ajax(`/admin/email/${status}.json?offset=${offset}`, { data: filter })
.then(logs => _.map(logs, log => EmailLog.create(log)));
}
});

View File

@ -0,0 +1,29 @@
import AdminUser from 'admin/models/admin-user';
const IncomingEmail = Discourse.Model.extend({});
IncomingEmail.reopenClass({
create(attrs) {
attrs = attrs || {};
if (attrs.user) {
attrs.user = AdminUser.create(attrs.user);
}
return this._super(attrs);
},
findAll(filter, offset) {
filter = filter || {};
offset = offset || 0;
const status = filter.status || "received";
filter = _.omit(filter, "status");
return Discourse.ajax(`/admin/email/${status}.json?offset=${offset}`, { data: filter })
.then(incomings => _.map(incomings, incoming => IncomingEmail.create(incoming)));
}
});
export default IncomingEmail;

View File

@ -1,4 +1,5 @@
import Badge from 'discourse/models/badge';
import BadgeGrouping from 'discourse/models/badge-grouping';
export default Discourse.Route.extend({
_json: null,
@ -13,14 +14,19 @@ export default Discourse.Route.extend({
setupController: function(controller, model) {
var json = this._json,
triggers = [];
triggers = [],
badgeGroupings = [];
_.each(json.admin_badges.triggers,function(v,k){
triggers.push({id: v, name: I18n.t('admin.badges.trigger_type.'+k)});
});
json.badge_groupings.forEach(function(badgeGroupingJson) {
badgeGroupings.push(BadgeGrouping.create(badgeGroupingJson));
});
controller.setProperties({
badgeGroupings: json.badge_groupings,
badgeGroupings: badgeGroupings,
badgeTypes: json.badge_types,
protectedSystemFields: json.admin_badges.protected_system_fields,
badgeTriggers: triggers,

View File

@ -1,2 +0,0 @@
import AdminEmailLogs from 'admin/routes/admin-email-logs';
export default AdminEmailLogs.extend({ status: "all" });

View File

@ -0,0 +1,14 @@
import IncomingEmail from 'admin/models/incoming-email';
export default Discourse.Route.extend({
model() {
return IncomingEmail.findAll({ status: this.get("status") });
},
setupController(controller, model) {
controller.set("model", model);
controller.set("filter", { status: this.get("status") });
}
});

View File

@ -1,11 +1,11 @@
import EmailSettings from 'admin/models/email-settings';
export default Discourse.Route.extend({
model: function() {
model() {
return EmailSettings.find();
},
renderTemplate: function() {
renderTemplate() {
this.render('admin/templates/email_index', { into: 'adminEmail' });
}
});

View File

@ -1,27 +1,14 @@
import EmailLog from 'admin/models/email-log';
/**
Handles routes related to viewing email logs.
@class AdminEmailSentRoute
@extends Discourse.Route
@namespace Discourse
@module Discourse
**/
export default Discourse.Route.extend({
model: function() {
model() {
return EmailLog.findAll({ status: this.get("status") });
},
setupController: function(controller, model) {
setupController(controller, model) {
controller.set("model", model);
// resets the filters
controller.set("filter", { status: this.get("status") });
},
renderTemplate: function() {
this.render("admin/templates/email_" + this.get("status"), { into: "adminEmail" });
}
});

View File

@ -0,0 +1,2 @@
import AdminEmailIncomings from 'admin/routes/admin-email-incomings';
export default AdminEmailIncomings.extend({ status: "received" });

View File

@ -0,0 +1,2 @@
import AdminEmailIncomings from 'admin/routes/admin-email-incomings';
export default AdminEmailIncomings.extend({ status: "rejected" });

View File

@ -8,9 +8,10 @@ export default {
});
this.resource('adminEmail', { path: '/email'}, function() {
this.route('all');
this.route('sent');
this.route('skipped');
this.route('received');
this.route('rejected');
this.route('previewDigest', { path: '/preview-digest' });
});

View File

@ -2,25 +2,22 @@
<form class="form-horizontal">
<div>
<label for="name">{{i18n 'admin.badges.name'}}</label>
{{input type="text" name="name" value=buffered.name}}
{{#if readOnly}}
{{input type="text" name="name" value=buffered.displayName disabled=true}}
{{else}}
{{input type="text" name="name" value=buffered.name}}
{{/if}}
</div>
{{#if showDisplayName}}
<div>
<strong>{{i18n 'admin.badges.display_name'}}</strong>
{{buffered.displayName}}
</div>
{{/if}}
<div>
<label for="name">{{i18n 'admin.badges.icon'}}</label>
{{input type="text" name="name" value=buffered.icon}}
<label for="icon">{{i18n 'admin.badges.icon'}}</label>
{{input type="text" name="icon" value=buffered.icon}}
<p class='help'>{{i18n 'admin.badges.icon_help'}}</p>
</div>
<div>
<label for="name">{{i18n 'admin.badges.image'}}</label>
{{input type="text" name="name" value=buffered.image}}
<label for="image">{{i18n 'admin.badges.image'}}</label>
{{input type="text" name="image" value=buffered.image}}
<p class='help'>{{i18n 'admin.badges.icon_help'}}</p>
</div>
@ -40,7 +37,7 @@
value=buffered.badge_grouping_id
content=badgeGroupings
optionValuePath="content.id"
optionLabelPath="content.name"}}
optionLabelPath="content.displayName"}}
&nbsp;<button {{action "editGroupings"}} class='btn'>{{fa-icon 'pencil'}}</button>
</div>

View File

@ -0,0 +1,55 @@
<table class='table email-list'>
<thead>
<tr>
<th>{{i18n 'admin.email.time'}}</th>
<th>{{i18n 'admin.email.incoming_emails.from_address'}}</th>
<th>{{i18n 'admin.email.incoming_emails.to_addresses'}}</th>
<th>{{i18n 'admin.email.incoming_emails.subject'}}</th>
</tr>
</thead>
<tr class="filters">
<td>{{i18n 'admin.email.logs.filters.title'}}</td>
<td>{{text-field value=filter.from placeholderKey="admin.email.incoming_emails.filters.from_placeholder"}}</td>
<td>{{text-field value=filter.to placeholderKey="admin.email.incoming_emails.filters.to_placeholder"}}</td>
<td>{{text-field value=filter.subject placeholderKey="admin.email.incoming_emails.filters.subject_placeholder"}}</td>
</tr>
{{#each email in model}}
<tr>
<td class="time">{{format-date email.created_at}}</td>
<td class="username">
<div>
{{#if email.user}}
{{#link-to 'adminUser' email.user}}
{{avatar email.user imageSize="tiny"}}
{{email.from_address}}
{{/link-to}}
{{else}}
&mdash;
{{/if}}
</div>
</td>
<td class="addresses">
{{#each to in email.to_addresses}}
<p><a href="mailto:{{unbound to}}" title="TO">{{unbound to}}</a></p>
{{/each}}
{{#each cc in email.cc_addresses}}
<p><a href="mailto:{{unbound cc}}" title="CC">{{unbound cc}}</a></p>
{{/each}}
</td>
<td>
{{#if email.post_url}}
<a href="{{email.post_url}}">{{email.subject}}</a>
{{else}}
{{email.subject}}
{{/if}}
</td>
</tr>
{{else}}
<tr><td colspan="4">{{i18n 'admin.email.incoming_emails.none'}}</td></tr>
{{/each}}
</table>
{{conditional-loading-spinner condition=view.loading}}

View File

@ -0,0 +1,52 @@
<table class='table email-list'>
<thead>
<tr>
<th>{{i18n 'admin.email.time'}}</th>
<th>{{i18n 'admin.email.incoming_emails.from_address'}}</th>
<th>{{i18n 'admin.email.incoming_emails.to_addresses'}}</th>
<th>{{i18n 'admin.email.incoming_emails.subject'}}</th>
<th>{{i18n 'admin.email.incoming_emails.error'}}</th>
</tr>
</thead>
<tr class="filters">
<td>{{i18n 'admin.email.logs.filters.title'}}</td>
<td>{{text-field value=filter.from placeholderKey="admin.email.incoming_emails.filters.from_placeholder"}}</td>
<td>{{text-field value=filter.to placeholderKey="admin.email.incoming_emails.filters.to_placeholder"}}</td>
<td>{{text-field value=filter.subject placeholderKey="admin.email.incoming_emails.filters.subject_placeholder"}}</td>
<td>{{text-field value=filter.error placeholderKey="admin.email.incoming_emails.filters.error_placeholder"}}</td>
</tr>
{{#each email in model}}
<tr>
<td class="time">{{format-date email.created_at}}</td>
<td class="username">
<div>
{{#if email.user}}
{{#link-to 'adminUser' email.user}}
{{avatar email.user imageSize="tiny"}}
{{email.from_address}}
{{/link-to}}
{{else}}
&mdash;
{{/if}}
</div>
</td>
<td class="addresses">
{{#each to in email.to_addresses}}
<p><a href="mailto:{{unbound to}}" title="TO">{{unbound to}}</a></p>
{{/each}}
{{#each cc in email.cc_addresses}}
<p><a href="mailto:{{unbound cc}}" title="CC">{{unbound cc}}</a></p>
{{/each}}
</td>
<td>{{email.subject}}</td>
<td class="error">{{email.error}}</td>
</tr>
{{else}}
<tr><td colspan="5">{{i18n 'admin.email.incoming_emails.none'}}</td></tr>
{{/each}}
</table>
{{conditional-loading-spinner condition=view.loading}}

View File

@ -1,4 +1,4 @@
<table class='table'>
<table class='table email-list'>
<thead>
<tr>
<th>{{i18n 'admin.email.sent_at'}}</th>
@ -37,3 +37,5 @@
{{/each}}
</table>
{{conditional-loading-spinner condition=view.loading}}

View File

@ -1,4 +1,4 @@
<table class='table'>
<table class='table email-list'>
<thead>
<tr>
<th>{{i18n 'admin.email.time'}}</th>
@ -37,3 +37,5 @@
{{/each}}
</table>
{{conditional-loading-spinner condition=view.loading}}

View File

@ -1,10 +1,11 @@
{{#admin-nav}}
{{nav-item route='adminEmail.index' label='admin.email.settings'}}
{{nav-item route='adminEmail.all' label='admin.email.all'}}
{{nav-item route='adminEmail.previewDigest' label='admin.email.preview_digest'}}
{{nav-item route='adminCustomizeEmailTemplates' label='admin.email.templates'}}
{{nav-item route='adminEmail.sent' label='admin.email.sent'}}
{{nav-item route='adminEmail.skipped' label='admin.email.skipped'}}
{{nav-item route='adminEmail.previewDigest' label='admin.email.preview_digest'}}
{{nav-item route='adminCustomizeEmailTemplates' label='admin.customize.email_templates.title'}}
{{nav-item route='adminEmail.received' label='admin.email.received'}}
{{nav-item route='adminEmail.rejected' label='admin.email.rejected'}}
{{/admin-nav}}
<div class="admin-container">

View File

@ -1,39 +0,0 @@
<table class='table'>
<thead>
<tr>
<th>{{i18n 'admin.email.time'}}</th>
<th>{{i18n 'admin.email.user'}}</th>
<th>{{i18n 'admin.email.to_address'}}</th>
<th>{{i18n 'admin.email.email_type'}}</th>
<th>{{i18n 'admin.email.skipped_reason'}}</th>
</tr>
</thead>
<tr class="filters">
<td>{{i18n 'admin.email.logs.filters.title'}}</td>
<td>{{text-field value=filter.user placeholderKey="admin.email.logs.filters.user_placeholder"}}</td>
<td>{{text-field value=filter.address placeholderKey="admin.email.logs.filters.address_placeholder"}}</td>
<td>{{text-field value=filter.type placeholderKey="admin.email.logs.filters.type_placeholder"}}</td>
<td>{{text-field value=filter.skipped_reason placeholderKey="admin.email.logs.filters.skipped_reason_placeholder"}}</td>
</tr>
{{#each l in model}}
<tr>
<td>{{format-date l.created_at}}</td>
<td>
{{#if l.user}}
{{#link-to 'adminUser' l.user}}{{avatar l.user imageSize="tiny"}}{{/link-to}}
{{#link-to 'adminUser' l.user}}{{l.user.username}}{{/link-to}}
{{else}}
&mdash;
{{/if}}
</td>
<td><a href='mailto:{{unbound l.to_address}}'>{{l.to_address}}</a></td>
<td>{{l.email_type}}</td>
<td>{{l.skipped_reason}}</td>
</tr>
{{else}}
<tr><td colspan="5">{{i18n 'admin.email.logs.none'}}</td></tr>
{{/each}}
</table>

View File

@ -5,15 +5,15 @@
<li>
{{#if wc.editing}}
{{input value=wc.name}}
<button {{action "save" wc}}><i class="fa fa-check"></i></button>
<button {{action "save" wc}} class="btn no-text">{{fa-icon 'check'}}</button>
{{else}}
{{wc.name}}
{{wc.displayName}}
{{/if}}
<div class='actions'>
<button {{action "edit" wc}}><i class="fa fa-pencil"></i></button>
<button {{action "up" wc}}><i class="fa fa-toggle-up"></i></button>
<button {{action "down" wc}}><i class="fa fa-toggle-down"></i></button>
<button {{action "delete" wc}}><i class="fa fa-times"></i></button>
<button {{action "edit" wc}} class="btn no-text" {{bind-attr disabled="wc.system"}}>{{fa-icon 'pencil'}}</button>
<button {{action "up" wc}} class="btn no-text">{{fa-icon 'toggle-up'}}</button>
<button {{action "down" wc}} class="btn no-text">{{fa-icon 'toggle-down'}}</button>
<button {{action "delete" wc}} class="btn no-text btn-danger" {{bind-attr disabled="wc.system"}}>{{fa-icon 'times'}}</button>
</div>
</li>
{{/each}}

View File

@ -1,11 +1,13 @@
<section class="details {{unless model.active 'not-activated'}}">
<div class='user-controls'>
{{#if model.active}}
{{#if model.canViewProfile}}
{{#link-to 'user' model class="btn"}}
{{fa-icon "user"}}
{{i18n 'admin.user.show_public_profile'}}
{{/link-to}}
{{/if}}
{{#if model.active}}
{{#if model.can_impersonate}}
<button class='btn btn-danger' {{action "impersonate" target="content"}} title="{{i18n 'admin.impersonate.help'}}">
{{fa-icon "crosshairs"}}
@ -327,15 +329,29 @@
<div class='field'>{{i18n 'admin.user.blocked'}}</div>
<div class='value'>{{model.blocked}}</div>
<div class='controls'>
{{#if model.blocked}}
<button class='btn' {{action "unblock" target="content"}}>
{{fa-icon "thumbs-o-up"}}
{{i18n 'admin.user.unblock'}}
</button>
{{i18n 'admin.user.block_explanation'}}
{{/if}}
{{#conditional-loading-spinner size="small" condition=model.blockingUser}}
{{#if model.blocked}}
<button class='btn' {{action "unblock" target="content"}}>
{{fa-icon "thumbs-o-up"}}
{{i18n 'admin.user.unblock'}}
</button>
{{i18n 'admin.user.block_explanation'}}
{{else}}
<button class='btn' {{action "block" target="content"}}>
{{fa-icon "ban"}}
{{i18n 'admin.user.block'}}
</button>
{{i18n 'admin.user.block_explanation'}}
{{/if}}
{{/conditional-loading-spinner}}
</div>
</div>
<div class="display-row">
<div class='field'>{{i18n 'admin.user.staged'}}</div>
<div class='value'>{{model.staged}}</div>
<div class='controls'>{{i18n 'admin.user.stage_explanation'}}</div>
</div>
</section>
<section class='details'>

View File

@ -0,0 +1,14 @@
import LoadMore from "discourse/mixins/load-more";
export default Ember.View.extend(LoadMore, {
loading: false,
eyelineSelector: ".email-list tr",
actions: {
loadMore() {
if (this.get("loading") || this.get("model.allLoaded")) { return; }
this.set("loading", true);
return this.get("controller").loadMore().then(() => this.set("loading", false));
}
}
});

View File

@ -0,0 +1,14 @@
import LoadMore from "discourse/mixins/load-more";
export default Ember.View.extend(LoadMore, {
loading: false,
eyelineSelector: ".email-list tr",
actions: {
loadMore() {
if (this.get("loading") || this.get("model.allLoaded")) { return; }
this.set("loading", true);
return this.get("controller").loadMore().then(() => this.set("loading", false));
}
}
});

View File

@ -0,0 +1,5 @@
import AdminEmailIncomingsView from "admin/views/admin-email-incomings";
export default AdminEmailIncomingsView.extend({
templateName: "admin/templates/email-received"
});

View File

@ -0,0 +1,5 @@
import AdminEmailIncomingsView from "admin/views/admin-email-incomings";
export default AdminEmailIncomingsView.extend({
templateName: "admin/templates/email-rejected"
});

View File

@ -0,0 +1,5 @@
import AdminEmailLogsView from "admin/views/admin-email-logs";
export default AdminEmailLogsView.extend({
templateName: "admin/templates/email-sent"
});

View File

@ -0,0 +1,5 @@
import AdminEmailLogsView from "admin/views/admin-email-logs";
export default AdminEmailLogsView.extend({
templateName: "admin/templates/email-skipped"
});

View File

@ -6,9 +6,9 @@ import PermissionType from 'discourse/models/permission-type';
export default ComboboxView.extend({
classNames: ['combobox category-combobox'],
overrideWidths: true,
dataAttributes: ['id', 'description_text'],
valueBinding: Ember.Binding.oneWay('source'),
overrideWidths: true,
castInteger: true,
@computed("scopedCategoryId", "categories")
@ -22,7 +22,6 @@ export default ComboboxView.extend({
return categories.filter(c => {
if (scopedCategoryId && c.get('id') !== scopedCategoryId && c.get('parent_category_id') !== scopedCategoryId) { return false; }
if (c.get('isUncategorizedCategory')) { return false; }
if (c.get('contains_messages')) { return false; }
return c.get('permission') === PermissionType.FULL;
});
},

View File

@ -1,18 +1,19 @@
import { categoryBadgeHTML } from 'discourse/helpers/category-link';
import Category from 'discourse/models/category';
export default Ember.Component.extend({
_initializeAutocomplete: function() {
const self = this,
template = this.container.lookup('template:category-group-autocomplete.raw'),
regexp = new RegExp("href=['\"]" + Discourse.getURL('/c/') + "([^'\"]+)");
regexp = new RegExp(`href=['\"]${Discourse.getURL('/c/')}([^'\"]+)`);
this.$('input').autocomplete({
items: this.get('categories'),
single: false,
allowAny: false,
dataSource(term){
return Discourse.Category.list().filter(function(category){
return Category.list().filter(function(category){
const regex = new RegExp(term, "i");
return category.get("name").match(regex) &&
!_.contains(self.get('blacklist') || [], category) &&
@ -22,7 +23,7 @@ export default Ember.Component.extend({
onChangeItems(items) {
const categories = _.map(items, function(link) {
const slug = link.match(regexp)[1];
return Discourse.Category.findSingleBySlug(slug);
return Category.findSingleBySlug(slug);
});
Em.run.next(() => self.set("categories", categories));
},

View File

@ -1,6 +1,7 @@
import userSearch from 'discourse/lib/user-search';
import { default as computed, on } from 'ember-addons/ember-computed-decorators';
import { linkSeenMentions, fetchUnseenMentions } from 'discourse/lib/link-mentions';
import { linkSeenCategoryHashtags, fetchUnseenCategoryHashtags } from 'discourse/lib/link-category-hashtags';
export default Ember.Component.extend({
classNames: ['wmd-controls'],
@ -111,13 +112,19 @@ export default Ember.Component.extend({
$preview.scrollTop(desired + 50);
},
_renderUnseen: function($preview, unseen) {
fetchUnseenMentions($preview, unseen, this.siteSettings).then(() => {
_renderUnseenMentions: function($preview, unseen) {
fetchUnseenMentions($preview, unseen).then(() => {
linkSeenMentions($preview, this.siteSettings);
this._warnMentionedGroups($preview);
});
},
_renderUnseenCategoryHashtags: function($preview, unseen) {
fetchUnseenCategoryHashtags(unseen).then(() => {
linkSeenCategoryHashtags($preview);
});
},
_warnMentionedGroups($preview) {
Ember.run.scheduleOnce('afterRender', () => {
this._warnedMentions = this._warnedMentions || [];
@ -386,11 +393,17 @@ export default Ember.Component.extend({
// Paint mentions
const unseen = linkSeenMentions($preview, this.siteSettings);
if (unseen.length) {
Ember.run.debounce(this, this._renderUnseen, $preview, unseen, 500);
Ember.run.debounce(this, this._renderUnseenMentions, $preview, unseen, 500);
}
this._warnMentionedGroups($preview);
// Paint category hashtags
const unseenHashtags = linkSeenCategoryHashtags($preview);
if (unseenHashtags.length) {
Ember.run.debounce(this, this._renderUnseenCategoryHashtags, $preview, unseenHashtags, 500);
}
const post = this.get('composer.post');
let refresh = false;

View File

@ -2,6 +2,8 @@
import loadScript from 'discourse/lib/load-script';
import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
import { showSelector } from "discourse/lib/emoji/emoji-toolbar";
import Category from 'discourse/models/category';
import { SEPARATOR as categoryHashtagSeparator } from 'discourse/lib/category-hashtags';
// Our head can be a static string or a function that returns a string
// based on input (like for numbered lists).
@ -41,7 +43,7 @@ function Toolbar() {
id: 'italic',
group: 'fontStyles',
shortcut: 'I',
perform: e => e.applySurround('*', '*', 'italic_text')
perform: e => e.applySurround('_', '_', 'italic_text')
});
this.addButton({id: 'link', group: 'insertions', shortcut: 'K', action: 'showLinkModal'});
@ -175,7 +177,11 @@ export default Ember.Component.extend({
@on('didInsertElement')
_startUp() {
this._applyEmojiAutocomplete();
const container = this.get('container'),
$editorInput = this.$('.d-editor-input');
this._applyEmojiAutocomplete(container, $editorInput);
this._applyCategoryHashtagAutocomplete(container, $editorInput);
loadScript('defer/html-sanitizer-bundle').then(() => this.set('ready', true));
@ -243,14 +249,49 @@ export default Ember.Component.extend({
Ember.run.debounce(this, this._updatePreview, 30);
},
_applyEmojiAutocomplete() {
_applyCategoryHashtagAutocomplete(container, $editorInput) {
const template = container.lookup('template:category-group-autocomplete.raw');
$editorInput.autocomplete({
template: template,
key: '#',
transformComplete(category) {
return Category.slugFor(category, categoryHashtagSeparator);
},
dataSource(term) {
return Category.search(term);
},
triggerRule(textarea, opts) {
const result = Discourse.Utilities.caretRowCol(textarea);
const row = result.rowNum;
var col = result.colNum;
var line = textarea.value.split("\n")[row - 1];
if (opts && opts.backSpace) {
col = col - 1;
line = line.slice(0, line.length - 1);
// Don't trigger autocomplete when backspacing into a `#category |` => `#category|`
if (/^#{1}\w+/.test(line)) return false;
}
if (col < 6) {
// Don't trigger autocomplete when ATX-style headers are used
return (line.slice(0, col) !== "#".repeat(col));
} else {
return true;
}
}
});
},
_applyEmojiAutocomplete(container, $editorInput) {
if (!this.siteSettings.enable_emoji) { return; }
const container = this.container;
const template = container.lookup('template:emoji-selector-autocomplete.raw');
const self = this;
this.$('.d-editor-input').autocomplete({
$editorInput.autocomplete({
template: template,
key: ":",

View File

@ -3,22 +3,24 @@ import loadScript from "discourse/lib/load-script";
import { on } from "ember-addons/ember-computed-decorators";
export default Em.Component.extend({
tagName: "input",
classNames: ["date-picker"],
classNames: ["date-picker-wrapper"],
_picker: null,
@on("didInsertElement")
_loadDatePicker() {
const input = this.$()[0];
const input = this.$(".date-picker")[0];
loadScript("/javascripts/pikaday.js").then(() => {
this._picker = new Pikaday({
let default_opts = {
field: input,
container: this.$()[0],
format: "YYYY-MM-DD",
defaultDate: moment().add(1, "day").toDate(),
minDate: new Date(),
onSelect: date => this.set("value", moment(date).format("YYYY-MM-DD")),
});
onSelect: date => this.set("value", moment(date).format("YYYY-MM-DD"))
};
this._picker = new Pikaday(Object.assign(default_opts, this._opts()));
});
},
@ -27,4 +29,8 @@ export default Em.Component.extend({
this._picker = null;
},
_opts() {
return null;
}
});

View File

@ -4,10 +4,15 @@ import { setting } from 'discourse/lib/computed';
export default Ember.Component.extend({
classNames: ["title"],
linkUrl: function() {
return Discourse.getURL('/');
targetUrl: function() {
// For overriding by customizations
return '/';
}.property(),
linkUrl: function() {
return Discourse.getURL(this.get('targetUrl'));
}.property('targetUrl'),
showSmallLogo: function() {
return !Discourse.Mobile.mobileView && this.get("minimized");
}.property("minimized"),
@ -27,7 +32,7 @@ export default Ember.Component.extend({
e.preventDefault();
DiscourseURL.routeTo('/');
DiscourseURL.routeTo(this.get('targetUrl'));
return false;
}
});

View File

@ -29,7 +29,9 @@ export default Ember.Component.extend({
badgeSlug = badgeName.replace(/[^A-Za-z0-9_]+/g, '-').toLowerCase();
}
return Discourse.getURL('/badges/' + badgeId + '/' + badgeSlug);
var username = it.get('data.username');
username = username ? "?username=" + username.toLowerCase() : "";
return Discourse.getURL('/badges/' + badgeId + '/' + badgeSlug + username);
}
const topicId = it.get('topic_id');

View File

@ -349,6 +349,21 @@ const PostMenuComponent = Ember.Component.extend(StringBuffer, {
this.sendAction('toggleBookmark', post);
},
// Wiki button
buttonForWiki(post) {
if (!post.get('can_wiki')) return;
if (post.get('wiki')) {
return new Button('wiki', 'post.controls.unwiki', 'pencil-square-o', {className: 'wiki wikied'});
} else {
return new Button('wiki', 'post.controls.wiki', 'pencil-square-o', {className: 'wiki'});
}
},
clickWiki(post) {
this.sendAction('toggleWiki', post);
},
buttonForAdmin() {
if (!Discourse.User.currentProp('canManageTopic')) { return; }
return new Button('admin', 'post.controls.admin', 'wrench');
@ -357,10 +372,7 @@ const PostMenuComponent = Ember.Component.extend(StringBuffer, {
renderAdminPopup(post, buffer) {
if (!Discourse.User.currentProp('canManageTopic')) { return; }
const isWiki = post.get('wiki'),
wikiIcon = iconHTML('pencil-square-o'),
wikiText = isWiki ? I18n.t('post.controls.unwiki') : I18n.t('post.controls.wiki'),
isModerator = post.get('post_type') === this.site.get('post_types.moderator_action'),
const isModerator = post.get('post_type') === this.site.get('post_types.moderator_action'),
postTypeIcon = iconHTML('shield'),
postTypeText = isModerator ? I18n.t('post.controls.revert_to_regular') : I18n.t('post.controls.convert_to_moderator'),
rebakePostIcon = iconHTML('cog'),
@ -373,7 +385,6 @@ const PostMenuComponent = Ember.Component.extend(StringBuffer, {
const html = '<div class="post-admin-menu popup-menu">' +
'<h3>' + I18n.t('admin_title') + '</h3>' +
'<ul>' +
'<li class="btn" data-action="toggleWiki">' + wikiIcon + wikiText + '</li>' +
(Discourse.User.currentProp('staff') ? '<li class="btn" data-action="togglePostType">' + postTypeIcon + postTypeText + '</li>' : '') +
'<li class="btn" data-action="rebakePost">' + rebakePostIcon + rebakePostText + '</li>' +
(post.hidden ? '<li class="btn" data-action="unhidePost">' + unhidePostIcon + unhidePostText + '</li>' : '') +
@ -393,10 +404,6 @@ const PostMenuComponent = Ember.Component.extend(StringBuffer, {
});
},
clickToggleWiki() {
this.sendAction('toggleWiki', this.get('post'));
},
clickTogglePostType() {
this.sendAction("togglePostType", this.get("post"));
},

View File

@ -1,4 +1,5 @@
import { relativeAge } from 'discourse/lib/formatter';
import { autoUpdatingRelativeAge } from 'discourse/lib/formatter';
import computed from 'ember-addons/ember-computed-decorators';
const icons = {
'closed.enabled': 'lock',
@ -13,16 +14,20 @@ const icons = {
'pinned_globally.disabled': 'thumb-tack unpinned',
'visible.enabled': 'eye',
'visible.disabled': 'eye-slash',
'split_topic': 'sign-out'
'split_topic': 'sign-out',
'invited_user': 'plus-circle',
'removed_user': 'minus-circle'
};
export function actionDescription(actionCode, createdAt) {
export function actionDescription(actionCode, createdAt, username) {
return function() {
const ac = this.get(actionCode);
if (ac) {
const dt = new Date(this.get(createdAt));
const when = relativeAge(dt, {format: 'medium-with-ago'});
return I18n.t(`action_codes.${ac}`, {when}).htmlSafe();
const when = autoUpdatingRelativeAge(dt, { format: 'medium-with-ago' });
const u = this.get(username);
const who = u ? `<a class="mention" href="/users/${u}">@${u}</a>` : "";
return I18n.t(`action_codes.${ac}`, { who, when }).htmlSafe();
}
}.property(actionCode, createdAt);
}
@ -31,18 +36,19 @@ export default Ember.Component.extend({
layoutName: 'components/small-action', // needed because `time-gap` inherits from this
classNames: ['small-action'],
description: actionDescription('actionCode', 'post.created_at'),
description: actionDescription('actionCode', 'post.created_at', 'post.action_code_who'),
icon: function() {
return icons[this.get('actionCode')] || 'exclamation';
}.property('actionCode'),
@computed("actionCode")
icon(actionCode) {
return icons[actionCode] || 'exclamation';
},
actions: {
edit: function() {
edit() {
this.sendAction('editPost', this.get('post'));
},
delete: function() {
delete() {
this.sendAction('deletePost', this.get('post'));
}
}

View File

@ -4,7 +4,7 @@ import { actionDescription } from "discourse/components/small-action";
export default Ember.Component.extend({
classNameBindings: [":item", "item.hidden", "item.deleted", "moderatorAction"],
moderatorAction: propertyEqual("item.post_type", "site.post_types.moderator_action"),
actionDescription: actionDescription("item.action_code", "item.created_at"),
actionDescription: actionDescription("item.action_code", "item.created_at", "item.username"),
actions: {
removeBookmark(userAction) {

View File

@ -3,5 +3,13 @@ export default Ember.Component.extend({
showGrantCount: function() {
return this.get('count') && this.get('count') > 1;
}.property('count')
}.property('count'),
badgeUrl: function(){
// NOTE: I tried using a link-to helper here but the queryParams mean it fails
var username = this.get('user.username_lower') || '';
username = username !== '' ? "?username=" + username : '';
return this.get('badge.url') + username;
}.property("badge", "user")
});

View File

@ -1,17 +1,33 @@
import UserBadge from 'discourse/models/user-badge';
export default Ember.Controller.extend({
queryParams: ['username'],
noMoreBadges: false,
userBadges: null,
needs: ["application"],
user: function(){
if (this.get("username")) {
return this.get('userBadges')[0].get('user');
}
}.property("username"),
grantCount: function() {
if (this.get("username")) {
return this.get('userBadges.grant_count');
} else {
return this.get('model.grant_count');
}
}.property('username', 'model', 'userBadges'),
actions: {
loadMore() {
const self = this;
const userBadges = this.get('userBadges');
UserBadge.findByBadgeId(this.get('model.id'), {
offset: userBadges.length
offset: userBadges.length,
username: this.get('username'),
}).then(function(result) {
userBadges.pushObjects(result);
if(userBadges.length === 0){
@ -22,11 +38,12 @@ export default Ember.Controller.extend({
},
layoutClass: function(){
var user = this.get("user") ? " single-user" : "";
var ub = this.get("userBadges");
if(ub && ub[0] && ub[0].post_id){
return "user-badge-with-posts";
return "user-badge-with-posts" + user;
} else {
return "user-badge-no-posts";
return "user-badge-no-posts" + user;
}
}.property("userBadges"),
@ -34,7 +51,7 @@ export default Ember.Controller.extend({
if (this.get('noMoreBadges')) { return false; }
if (this.get('userBadges')) {
return this.get('model.grant_count') > this.get('userBadges.length');
return this.get('grantCount') > this.get('userBadges.length');
} else {
return false;
}

View File

@ -4,9 +4,15 @@ export default Ember.Controller.extend(ModalFunctionality, {
// You need a value in the field to submit it.
submitDisabled: function() {
return Ember.isEmpty(this.get('accountEmailOrUsername').trim()) || this.get('disabled');
return Ember.isEmpty((this.get('accountEmailOrUsername') || '').trim()) || this.get('disabled');
}.property('accountEmailOrUsername', 'disabled'),
onShow: function() {
if ($.cookie('email')) {
this.set('accountEmailOrUsername', $.cookie('email'));
}
},
actions: {
submit: function() {
var self = this;

View File

@ -6,6 +6,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
// If this isn't defined, it will proxy to the user model on the preferences
// page which is wrong.
emailOrUsername: null,
inviteIcon: "envelope",
isAdmin: function(){
return Discourse.User.currentProp("admin");
@ -88,8 +89,10 @@ export default Ember.Controller.extend(ModalFunctionality, {
if (Ember.isEmpty(this.get('emailOrUsername'))) {
return I18n.t('topic.invite_reply.to_topic_blank');
} else if (Discourse.Utilities.emailValid(this.get('emailOrUsername'))) {
this.set("inviteIcon", "envelope");
return I18n.t('topic.invite_reply.to_topic_email');
} else {
this.set("inviteIcon", "hand-o-right");
return I18n.t('topic.invite_reply.to_topic_username');
}
}

View File

@ -32,8 +32,8 @@ export default Ember.Controller.extend(CanCheckEmails, {
}
},
cannotDeleteAccount: Em.computed.not('can_delete_account'),
deleteDisabled: Em.computed.or('saving', 'deleting', 'cannotDeleteAccount'),
cannotDeleteAccount: Em.computed.not('currentUser.can_delete_account'),
deleteDisabled: Em.computed.or('model.isSaving', 'deleting', 'cannotDeleteAccount'),
canEditName: setting('enable_names'),

View File

@ -9,10 +9,10 @@ export default Ember.Controller.extend({
newEmailEmpty: Em.computed.empty('newEmail'),
saveDisabled: Em.computed.or('saving', 'newEmailEmpty', 'taken', 'unchanged'),
unchanged: propertyEqual('newEmailLower', 'email'),
unchanged: propertyEqual('newEmailLower', 'currentUser.email'),
newEmailLower: function() {
return this.get('newEmail').toLowerCase();
return this.get('newEmail').toLowerCase().trim();
}.property('newEmail'),
saveButtonText: function() {
@ -26,10 +26,10 @@ export default Ember.Controller.extend({
this.set('saving', true);
return this.get('content').changeEmail(this.get('newEmail')).then(function() {
self.set('success', true);
}, function(data) {
}, function(e) {
self.setProperties({ error: true, saving: false });
if (data.responseJSON && data.responseJSON.errors && data.responseJSON.errors[0]) {
self.set('errorMessage', data.responseJSON.errors[0]);
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors && e.jqXHR.responseJSON.errors[0]) {
self.set('errorMessage', e.jqXHR.responseJSON.errors[0]);
} else {
self.set('errorMessage', I18n.t('user.change_email.error'));
}
@ -38,5 +38,3 @@ export default Ember.Controller.extend({
}
});

View File

@ -6,6 +6,7 @@ import Quote from 'discourse/lib/quote';
import { popupAjaxError } from 'discourse/lib/ajax-error';
import computed from 'ember-addons/ember-computed-decorators';
import Composer from 'discourse/models/composer';
import DiscourseURL from 'discourse/lib/url';
export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
needs: ['header', 'modal', 'composer', 'quote-button', 'topic-progress', 'application'],
@ -17,8 +18,8 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
queryParams: ['filter', 'username_filters', 'show_deleted'],
loadedAllPosts: Em.computed.or('model.postStream.loadedAllPosts', 'model.postStream.loadingLastPost'),
enteredAt: null,
firstPostExpanded: false,
retrying: false,
firstPostExpanded: false,
adminMenuVisible: false,
showRecover: Em.computed.and('model.deleted', 'model.details.can_recover'),
@ -89,11 +90,14 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
this.set('selectedReplies', []);
}.on('init'),
@computed("model.isPrivateMessage", "model.category_id")
showCategoryChooser(isPrivateMessage, categoryId) {
const category = Discourse.Category.findById(categoryId);
const containsMessages = category && category.get("contains_messages");
return !isPrivateMessage && !containsMessages;
showCategoryChooser: Ember.computed.not("model.isPrivateMessage"),
gotoInbox(name) {
var url = '/users/' + this.get('currentUser.username_lower') + '/messages';
if (name) {
url = url + '/group/' + name;
}
DiscourseURL.routeTo(url);
},
actions: {
@ -109,12 +113,19 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
this.deleteTopic();
},
archiveMessage() {
this.get('model').archiveMessage();
const topic = this.get('model');
topic.archiveMessage().then(()=>{
this.gotoInbox(topic.get("inboxGroupName"));
});
},
moveToInbox() {
this.get('model').moveToInbox();
const topic = this.get('model');
topic.moveToInbox().then(()=>{
this.gotoInbox(topic.get("inboxGroupName"));
});
},
// Post related methods

View File

@ -1,3 +1,5 @@
import { exportUserArchive } from 'discourse/lib/export-csv';
export default Ember.Controller.extend({
userActionType: null,
needs: ["application", "user"],
@ -14,6 +16,21 @@ export default Ember.Controller.extend({
showFooter = this.get("model.statsCountNonPM") <= this.get("model.stream.itemsLoaded");
}
this.set("controllers.application.showFooter", showFooter);
}.observes("userActionType", "model.stream.itemsLoaded")
}.observes("userActionType", "model.stream.itemsLoaded"),
actions: {
exportUserArchive() {
bootbox.confirm(
I18n.t("admin.export_csv.user_archive_confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
function(confirmed) {
if (confirmed) {
exportUserArchive();
}
}
);
}
}
});

View File

@ -1,4 +1,6 @@
export default Ember.ArrayController.extend({
needs: ["user"],
user: Em.computed.alias("controllers.user.model"),
sortProperties: ['badge.badge_type.sort_order', 'badge.name'],
orderBy: function(ub1, ub2){
var sr1 = ub1.get('badge.badge_type.sort_order');

View File

@ -3,14 +3,23 @@ import Topic from 'discourse/models/topic';
export default Ember.Controller.extend({
needs: ["user-topics-list"],
needs: ["user-topics-list", "user"],
pmView: false,
viewingSelf: Em.computed.alias("controllers.user.viewingSelf"),
isGroup: Em.computed.equal('pmView', 'groups'),
selected: Em.computed.alias('controllers.user-topics-list.selected'),
bulkSelectEnabled: Em.computed.alias('controllers.user-topics-list.bulkSelectEnabled'),
mobileView: function() {
return Discourse.Mobile.mobileView;
}.property(),
showNewPM: function(){
return this.get('controllers.user.viewingSelf') &&
Discourse.User.currentProp('can_send_private_messages');
}.property('controllers.user.viewingSelf'),
@computed('selected.@each', 'bulkSelectEnabled')
hasSelection(selected, bulkSelectEnabled){
return bulkSelectEnabled && selected && selected.length > 0;

View File

@ -0,0 +1,13 @@
export default Ember.Controller.extend({
needs: ['user'],
user: Em.computed.alias('controllers.user.model'),
moreTopics: function(){
return this.get('model.topics').length > 5;
}.property('model'),
moreReplies: function(){
return this.get('model.replies').length > 5;
}.property('model'),
moreBadges: function(){
return this.get('model.badges').length > 5;
}.property('model')
});

View File

@ -14,9 +14,4 @@ export default Ember.Controller.extend({
}
},
showNewPM: function(){
return this.get('controllers.user.viewingSelf') &&
Discourse.User.currentProp('can_send_private_messages');
}.property('controllers.user.viewingSelf')
});

View File

@ -1,4 +1,3 @@
import { exportUserArchive } from 'discourse/lib/export-csv';
import CanCheckEmails from 'discourse/mixins/can-check-emails';
import computed from 'ember-addons/ember-computed-decorators';
import UserAction from 'discourse/models/user-action';
@ -89,17 +88,5 @@ export default Ember.Controller.extend(CanCheckEmails, {
.then(user => user.destroy({deletePosts: true}));
},
exportUserArchive() {
bootbox.confirm(
I18n.t("admin.export_csv.user_archive_confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
function(confirmed) {
if (confirmed) {
exportUserArchive();
}
}
);
}
}
});

View File

@ -0,0 +1,23 @@
/**
Supports Discourse's category hashtags (#category-slug) for automatically
generating a link to the category.
**/
Discourse.Dialect.inlineRegexp({
start: '#',
matcher: /^#([\w-:]{1,50})/i,
spaceOrTagBoundary: true,
emitter: function(matches) {
var slug = matches[1],
hashtag = matches[0],
attributeClass = 'hashtag',
categoryHashtagLookup = this.dialect.options.categoryHashtagLookup,
result = categoryHashtagLookup && categoryHashtagLookup(slug);
if (result && result[0] === "category") {
return ['a', { class: attributeClass, href: result[1] }, '#', ["span", {}, slug]];
} else {
return ['span', { class: attributeClass }, hashtag];
}
}
});

View File

@ -5,22 +5,23 @@
**/
Discourse.Dialect.inlineRegexp({
start: '@',
// NOTE: we really should be using SiteSettings here, but it loads later in process
// also, if we do, we must ensure serverside version works as well
matcher: /^(@[A-Za-z0-9][A-Za-z0-9_\.\-]{0,40}[A-Za-z0-9\_])/,
// NOTE: since we can't use SiteSettings here (they loads later in process)
// we are being less strict to account for more cases than allowed
matcher: /^@(\w[\w.-]{0,59})\b/i,
wordBoundary: true,
emitter: function(matches) {
var username = matches[1],
var mention = matches[0].trim(),
name = matches[1],
mentionLookup = this.dialect.options.mentionLookup;
var type = mentionLookup && mentionLookup(username.substr(1));
var type = mentionLookup && mentionLookup(name);
if (type === "user") {
return ['a', {'class': 'mention', href: Discourse.getURL("/users/") + username.substr(1).toLowerCase()}, username];
return ['a', {'class': 'mention', href: Discourse.getURL("/users/") + name.toLowerCase()}, mention];
} else if (type === "group") {
return ['a', {'class': 'mention-group', href: Discourse.getURL("/groups/") + username.substr(1)}, username];
return ['a', {'class': 'mention-group', href: Discourse.getURL("/groups/") + name}, mention];
} else {
return ['span', {'class': 'mention'}, username];
return ['span', {'class': 'mention'}, mention];
}
}
});
@ -34,10 +35,10 @@ Discourse.Dialect.on("parseNode", function(event) {
var parent = path[path.length - 1];
// If the parent is an 'a', remove it
if (parent && parent[0] === 'a') {
var username = node[2];
var name = node[2];
node.length = 0;
node[0] = "__RAW";
node[1] = username;
node[1] = name;
}
}

View File

@ -9,6 +9,16 @@ function categoryStripe(color, classes) {
return "<span class='" + classes + "' " + style + "></span>";
}
/**
Generates category badge HTML
@param {Object} category The category to generate the badge for.
@param {Object} opts
@param {String} [opts.url] The url that we want the category badge to link to.
@param {Boolean} [opts.allowUncategorized] If false, returns an empty string for the uncategorized category.
@param {Boolean} [opts.link] If false, the category badge will not be a link.
@param {Boolean} [opts.hideParaent] If true, parent category will be hidden in the badge.
**/
export function categoryBadgeHTML(category, opts) {
opts = opts || {};
@ -21,7 +31,7 @@ export function categoryBadgeHTML(category, opts) {
var description = get(category, 'description_text'),
restricted = get(category, 'read_restricted'),
url = Discourse.getURL("/c/") + Discourse.Category.slugFor(category),
url = opts.url ? opts.url : Discourse.getURL("/c/") + Discourse.Category.slugFor(category),
href = (opts.link === false ? '' : url),
tagName = (opts.link === false || opts.link === "false" ? 'span' : 'a'),
extraClasses = (opts.extraClasses ? (' ' + opts.extraClasses) : ''),
@ -50,6 +60,7 @@ export function categoryBadgeHTML(category, opts) {
">";
var name = escapeExpression(get(category, 'name'));
if (restricted) {
html += iconHTML('lock') + " " + name;
} else {

View File

@ -24,6 +24,5 @@ registerUnbound('raw', function(templateName, params) {
Ember.warn('Could not find raw template: ' + templateName);
return;
}
return renderRaw(this, template, templateName, params);
});

View File

@ -8,9 +8,9 @@ function resolveParams(ctx, options) {
if (options.hashTypes) {
Ember.keys(hash).forEach(function(k) {
const type = options.hashTypes[k];
if (type === "STRING") {
if (type === "STRING" || type === "StringLiteral") {
params[k] = hash[k];
} else if (type === "ID") {
} else if (type === "ID" || type === "PathExpression") {
params[k] = get(ctx, hash[k], options);
}
});
@ -23,7 +23,7 @@ function resolveParams(ctx, options) {
export default function registerUnbound(name, fn) {
const func = function(property, options) {
if (options.types && options.types[0] === "ID") {
if (options.types && (options.types[0] === "ID" || options.types[0] === "PathExpression")) {
property = get(this, property, options);
}

View File

@ -282,6 +282,14 @@ export default function(options) {
}, 50);
});
const checkTriggerRule = (opts) => {
if (options.triggerRule) {
return options.triggerRule(me[0], opts);
} else {
return true;
}
};
$(this).on('keypress.autocomplete', function(e) {
var caretPosition, term;
@ -289,7 +297,7 @@ export default function(options) {
if (options.key && e.which === options.key.charCodeAt(0)) {
caretPosition = Discourse.Utilities.caretPosition(me[0]);
var prevChar = me.val().charAt(caretPosition - 1);
if (!prevChar || allowedLettersRegex.test(prevChar)) {
if (checkTriggerRule() && (!prevChar || allowedLettersRegex.test(prevChar))) {
completeStart = completeEnd = caretPosition;
updateAutoComplete(options.dataSource(""));
}
@ -343,7 +351,7 @@ export default function(options) {
stopFound = prev === options.key;
if (stopFound) {
prev = me[0].value[c - 1];
if (!prev || allowedLettersRegex.test(prev)) {
if (checkTriggerRule({ backSpace: true }) && (!prev || allowedLettersRegex.test(prev))) {
completeStart = c;
caretPosition = completeEnd = initial;
term = me[0].value.substring(c + 1, initial);
@ -351,7 +359,7 @@ export default function(options) {
return true;
}
}
prevIsGood = /[a-zA-Z\.]/.test(prev);
prevIsGood = /[a-zA-Z\.-]/.test(prev);
}
}

View File

@ -0,0 +1,5 @@
export const SEPARATOR = ":";
export function replaceSpan($elem, categorySlug, categoryLink) {
$elem.replaceWith(`<a href="${categoryLink}" class="hashtag">#<span>${categorySlug}</span></a>`);
};

View File

@ -1,5 +1,10 @@
import DiscourseURL from 'discourse/lib/url';
export function isValidLink($link) {
return ($link.hasClass("track-link") ||
$link.closest('.hashtag,.badge-category,.onebox-result,.onebox-body').length === 0);
};
export default {
trackClick(e) {
// cancel click if triggered as part of selection.
@ -32,8 +37,7 @@ export default {
var $badge = $('span.badge', $link);
if ($badge.length === 1) {
// don't update counts in category badge nor in oneboxes (except when we force it)
if ($link.hasClass("track-link") ||
$link.closest('.badge-category,.onebox-result,.onebox-body').length === 0) {
if (isValidLink($link)) {
var html = $badge.html();
if (/^\d+$/.test(html)) { $badge.html(parseInt(html, 10) + 1); }
}

View File

@ -68,19 +68,32 @@
RawHandlebars.JavaScriptCompiler.prototype.compiler = RawHandlebars.JavaScriptCompiler;
RawHandlebars.JavaScriptCompiler.prototype.namespace = "Discourse.EmberCompatHandlebars";
function replaceGet(ast) {
var visitor = new Handlebars.Visitor();
visitor.mutating = true;
RawHandlebars.Compiler.prototype.mustache = function(mustache) {
if ( !(mustache.params.length || mustache.hash)) {
var id = new Handlebars.AST.IdNode([{ part: 'get' }]);
mustache = new Handlebars.AST.MustacheNode([id].concat([mustache.id]), mustache.hash, mustache.escaped);
}
return Handlebars.Compiler.prototype.mustache.call(this, mustache);
};
visitor.MustacheStatement = function(mustache) {
if (!(mustache.params.length || mustache.hash)) {
mustache.params[0] = mustache.path;
mustache.path = {
type: "PathExpression",
data: false,
depth: mustache.path.depth,
parts: ["get"],
original: "get",
loc: mustache.path.loc,
strict: true,
falsy: true
};
}
return Handlebars.Visitor.prototype.MustacheStatement.call(this, mustache);
};
visitor.accept(ast);
}
RawHandlebars.precompile = function(value, asObject) {
var ast = Handlebars.parse(value);
replaceGet(ast);
var options = {
knownHelpers: {
@ -96,9 +109,10 @@
return new RawHandlebars.JavaScriptCompiler().compile(environment, options, undefined, asObject);
};
RawHandlebars.compile = function(string) {
var ast = Handlebars.parse(string);
replaceGet(ast);
// this forces us to rewrite helpers
var options = { data: true, stringParams: true };
var environment = new RawHandlebars.Compiler().compile(ast, options);

View File

@ -0,0 +1,51 @@
import { replaceSpan } from 'discourse/lib/category-hashtags';
const validCategoryHashtags = {};
const checkedCategoryHashtags = [];
const testedKey = 'tested';
const testedClass = `hashtag-${testedKey}`;
function updateFound($hashtags, categorySlugs) {
Ember.run.schedule('afterRender', () => {
$hashtags.each((index, hashtag) => {
const categorySlug = categorySlugs[index];
const link = validCategoryHashtags[categorySlug];
const $hashtag = $(hashtag);
if (link) {
replaceSpan($hashtag, categorySlug, link);
} else if (checkedCategoryHashtags.indexOf(categorySlug) !== -1) {
$hashtag.addClass(testedClass);
}
});
});
};
export function linkSeenCategoryHashtags($elem) {
const $hashtags = $(`span.hashtag:not(.${testedClass})`, $elem);
const unseen = [];
if ($hashtags.length) {
const categorySlugs = $hashtags.map((_, hashtag) => $(hashtag).text().substr(1));
if (categorySlugs.length) {
_.uniq(categorySlugs).forEach((categorySlug) => {
if (checkedCategoryHashtags.indexOf(categorySlug) === -1) {
unseen.push(categorySlug);
}
});
}
updateFound($hashtags, categorySlugs);
}
return unseen;
};
export function fetchUnseenCategoryHashtags(categorySlugs) {
return Discourse.ajax("/category_hashtags/check", { data: { category_slugs: categorySlugs } })
.then((response) => {
response.valid.forEach((category) => {
validCategoryHashtags[category.slug] = category.url;
});
checkedCategoryHashtags.push.apply(checkedCategoryHashtags, categorySlugs);
});
}

View File

@ -239,6 +239,7 @@ Discourse.Markdown.whiteListTag('a', 'class', 'attachment');
Discourse.Markdown.whiteListTag('a', 'class', 'onebox');
Discourse.Markdown.whiteListTag('a', 'class', 'mention');
Discourse.Markdown.whiteListTag('a', 'class', 'mention-group');
Discourse.Markdown.whiteListTag('a', 'class', 'hashtag');
Discourse.Markdown.whiteListTag('a', 'target', '_blank');
Discourse.Markdown.whiteListTag('a', 'rel', 'nofollow');
@ -251,6 +252,7 @@ Discourse.Markdown.whiteListTag('div', 'class', 'title');
Discourse.Markdown.whiteListTag('div', 'class', 'quote-controls');
Discourse.Markdown.whiteListTag('span', 'class', 'mention');
Discourse.Markdown.whiteListTag('span', 'class', 'hashtag');
Discourse.Markdown.whiteListTag('aside', 'class', 'quote');
Discourse.Markdown.whiteListTag('aside', 'data-*');

View File

@ -143,6 +143,19 @@ Discourse.Utilities = {
return String(text).trim();
},
// Determine the row and col of the caret in an element
caretRowCol: function(el) {
var caretPosition = Discourse.Utilities.caretPosition(el);
var rows = el.value.slice(0, caretPosition).split("\n");
var rowNum = rows.length;
var colNum = caretPosition - rows.splice(0, rowNum - 1).reduce(function(sum, row) {
return sum + row.length + 1;
}, 0);
return { rowNum: rowNum, colNum: colNum};
},
// Determine the position of the caret in an element
caretPosition: function(el) {
var r, rc, re;
@ -249,7 +262,11 @@ Discourse.Utilities = {
return '<img src="' + upload.url + '" width="' + upload.width + '" height="' + upload.height + '">';
} else if (!Discourse.SiteSettings.prevent_anons_from_downloading_files && (/\.(mov|mp4|webm|ogv|mp3|ogg|wav)$/i).test(upload.original_filename)) {
// is Audio/Video
return "http://" + Discourse.BaseUrl + upload.url;
if (Discourse.CDN) {
return Discourse.CDN.startsWith('//') ? "http:" + Discourse.getURLWithCDN(upload.url) : Discourse.getURLWithCDN(upload.url);
} else {
return "http://" + Discourse.BaseUrl + upload.url;
}
} else {
return '<a class="attachment" href="' + upload.url + '">' + upload.original_filename + '</a> (' + I18n.toHumanSize(upload.filesize) + ')';
}

View File

@ -8,7 +8,7 @@ export default RestModel.extend({
return this.get('name').toLowerCase().replace(/\s/g, '_');
},
@computed
@computed('name')
displayName() {
const i18nKey = `badges.badge_grouping.${this.get('i18nNameKey')}.name`;
return I18n.t(i18nKey, {defaultValue: this.get('name')});

View File

@ -5,6 +5,10 @@ const Badge = RestModel.extend({
newBadge: Em.computed.none('id'),
url: function() {
return Discourse.getURL(`/badges/${this.get('id')}/${this.get('slug')}`);
}.property(),
/**
@private
@ -159,7 +163,7 @@ Badge.reopenClass({
let badges = [];
if ("badge" in json) {
badges = [json.badge];
} else {
} else if (json.badges) {
badges = json.badges;
}
badges = badges.map(function(badgeJson) {
@ -207,4 +211,3 @@ Badge.reopenClass({
});
export default Badge;

View File

@ -86,8 +86,7 @@ const Category = RestModel.extend({
allow_badges: this.get('allow_badges'),
custom_fields: this.get('custom_fields'),
topic_template: this.get('topic_template'),
suppress_from_homepage: this.get('suppress_from_homepage'),
contains_messages: this.get("contains_messages"),
suppress_from_homepage: this.get('suppress_from_homepage')
},
type: this.get('id') ? 'PUT' : 'POST'
});
@ -205,14 +204,14 @@ Category.reopenClass({
return _uncategorized;
},
slugFor(category) {
slugFor(category, separator = "/") {
if (!category) return "";
const parentCategory = Em.get(category, 'parentCategory');
let result = "";
if (parentCategory) {
result = Category.slugFor(parentCategory) + "/";
result = Category.slugFor(parentCategory) + separator;
}
const id = Em.get(category, 'id'),
@ -285,6 +284,64 @@ Category.reopenClass({
reloadById(id) {
return Discourse.ajax(`/c/${id}/show.json`);
},
search(term, opts) {
var limit = 5;
if (opts) {
if (opts.limit === 0) {
return [];
} else if (opts.limit) {
limit = opts.limit;
}
}
const emptyTerm = (term === "");
let slugTerm = term;
if (!emptyTerm) {
term = term.toLowerCase();
slugTerm = term;
term = term.replace(/-/g, " ");
}
const categories = Category.listByActivity();
const length = categories.length;
var i;
var data = [];
const done = () => {
return data.length === limit;
};
for (i = 0; i < length && !done(); i++) {
const category = categories[i];
if ((emptyTerm && !category.get('parent_category_id')) ||
(!emptyTerm &&
(category.get('name').toLowerCase().indexOf(term) === 0 ||
category.get('slug').toLowerCase().indexOf(slugTerm) === 0))) {
data.push(category);
}
}
if (!done()) {
for (i = 0; i < length && !done(); i++) {
const category = categories[i];
if (!emptyTerm &&
(category.get('name').toLowerCase().indexOf(term) > 0 ||
category.get('slug').toLowerCase().indexOf(slugTerm) > 0)) {
if (data.indexOf(category) === -1) data.push(category);
}
}
}
return _.sortBy(data, (category) => {
return category.get('read_restricted');
});
}
});

View File

@ -67,17 +67,16 @@ const Composer = RestModel.extend({
creatingPrivateMessage: Em.computed.equal('action', PRIVATE_MESSAGE),
notCreatingPrivateMessage: Em.computed.not('creatingPrivateMessage'),
@computed("privateMessage", "archetype.hasOptions", "categoryId")
showCategoryChooser(isPrivateMessage, hasOptions, categoryId) {
@computed("privateMessage", "archetype.hasOptions")
showCategoryChooser(isPrivateMessage, hasOptions) {
const manyCategories = Discourse.Category.list().length > 1;
const category = Discourse.Category.findById(categoryId);
const containsMessages = category && category.get("contains_messages");
return !isPrivateMessage && !containsMessages && (hasOptions || manyCategories);
return !isPrivateMessage && (hasOptions || manyCategories);
},
privateMessage: function(){
return this.get('creatingPrivateMessage') || this.get('topic.archetype') === 'private_message';
}.property('creatingPrivateMessage', 'topic'),
@computed("creatingPrivateMessage", "topic")
privateMessage(creatingPrivateMessage, topic) {
return creatingPrivateMessage || (topic && topic.get('archetype') === 'private_message');
},
topicFirstPost: Em.computed.or('creatingTopic', 'editingFirstPost'),

View File

@ -424,8 +424,12 @@ const Topic = RestModel.extend({
this.set("archiving", true);
var promise = Discourse.ajax(`/t/${this.get('id')}/archive-message`, {type: 'PUT'});
promise.then(()=>this.set('message_archived', true))
.finally(()=>this.set('archiving', false));
promise.then((msg)=> {
this.set('message_archived', true);
if (msg && msg.group_name) {
this.set('inboxGroupName', msg.group_name);
}
}).finally(()=>this.set('archiving', false));
return promise;
},
@ -434,8 +438,12 @@ const Topic = RestModel.extend({
this.set("archiving", true);
var promise = Discourse.ajax(`/t/${this.get('id')}/move-to-inbox`, {type: 'PUT'});
promise.then(()=>this.set('message_archived', false))
.finally(()=>this.set('archiving', false));
promise.then((msg)=> {
this.set('message_archived', false);
if (msg && msg.group_name) {
this.set('inboxGroupName', msg.group_name);
}
}).finally(()=>this.set('archiving', false));
return promise;
}

View File

@ -48,7 +48,7 @@ UserBadge.reopenClass({
if ("user_badge" in json) {
userBadges = [json.user_badge];
} else {
userBadges = json.user_badges;
userBadges = (json.user_badge_info && json.user_badge_info.user_badges) || json.user_badges;
}
userBadges = userBadges.map(function(userBadgeJson) {
@ -73,6 +73,10 @@ UserBadge.reopenClass({
if ("user_badge" in json) {
return userBadges[0];
} else {
if (json.user_badge_info) {
userBadges.grant_count = json.user_badge_info.grant_count;
userBadges.username = json.user_badge_info.username;
}
return userBadges;
}
},

View File

@ -10,6 +10,7 @@ import UserBadge from 'discourse/models/user-badge';
import UserActionStat from 'discourse/models/user-action-stat';
import UserAction from 'discourse/models/user-action';
import Group from 'discourse/models/group';
import Topic from 'discourse/models/topic';
const User = RestModel.extend({
@ -355,6 +356,38 @@ const User = RestModel.extend({
});
}
});
},
summary() {
return Discourse.ajax(`/users/${this.get("username_lower")}/summary.json`)
.then(json => {
const topicMap = {};
json.topics.forEach(t => {
topicMap[t.id] = Topic.create(t);
});
const badgeMap = {};
Badge.createFromJson(json).forEach(b => {
badgeMap[b.id] = b;
});
const summary = json["user_summary"];
summary.replies.forEach(r => {
r.topic = topicMap[r.topic_id];
r.url = r.topic.urlForPostNumber(r.post_number);
r.createdAt = new Date(r.created_at);
});
summary.topics = summary.topic_ids.map(id => topicMap[id]);
summary.badges = summary.badges.map(ub => {
const badge = badgeMap[ub.badge_id];
badge.count = ub.count;
return badge;
});
return summary;
});
}
});

View File

@ -42,6 +42,7 @@ export default function() {
this.route('parentCategory', { path: '/c/:slug' });
this.route('categoryNone', { path: '/c/:slug/none' });
this.route('category', { path: '/c/:parentSlug/:slug' });
this.route('categoryWithID', { path: '/c/:parentSlug/:slug/:id' });
// homepage
this.route(Discourse.Utilities.defaultHomepage(), { path: '/' });
@ -57,11 +58,13 @@ export default function() {
// User routes
this.resource('users');
this.resource('user', { path: '/users/:username' }, function() {
this.route('summary');
this.resource('userActivity', { path: '/activity' }, function() {
this.route('topics');
this.route('replies');
this.route('likesGiven', {path: 'likes-given'});
this.route('bookmarks');
this.route('pending');
});
this.resource('userNotifications', {path: '/notifications'}, function(){

View File

@ -2,6 +2,11 @@ import UserBadge from 'discourse/models/user-badge';
import Badge from 'discourse/models/badge';
export default Discourse.Route.extend({
queryParams: {
username: {
refreshModel: true
}
},
actions: {
didTransition() {
this.controllerFor("badges/show")._showFooter();
@ -24,10 +29,13 @@ export default Discourse.Route.extend({
}
},
afterModel(model) {
return UserBadge.findByBadgeId(model.get("id")).then(userBadges => {
afterModel(model,transition) {
const username = transition.queryParams && transition.queryParams.username;
return UserBadge.findByBadgeId(model.get("id"), {username}).then(userBadges => {
this.userBadges = userBadges;
});
},
titleToken() {

View File

@ -0,0 +1,11 @@
import Category from 'discourse/models/category';
export default Discourse.Route.extend({
model: function(params) {
return Category.findById(params.id);
},
redirect: function(model) {
this.transitionTo(`/c/${Category.slugFor(model)}`);
}
});

View File

@ -1,2 +0,0 @@
export default Discourse.Route.extend({
});

View File

@ -0,0 +1,5 @@
export default Discourse.Route.extend({
model() {
return this.modelFor("User").summary();
}
});

View File

@ -9,7 +9,9 @@
<div class='row'>
<div class='badge'>{{user-badge badge=model}}</div>
<div class='description'>{{{model.displayDescriptionHtml}}}</div>
<div class='grant-count'>{{i18n 'badges.granted' count=model.grant_count}}</div>
{{#unless user}}
<div class='grant-count'>{{i18n 'badges.granted' count=grantCount}}</div>
{{/unless}}
<div class='info'>{{i18n 'badges.allow_title'}} {{{view.allowTitle}}}<br>{{i18n 'badges.multiple_grant'}} {{{view.multipleGrant}}}
</div>
</div>
@ -22,23 +24,48 @@
</div>
{{/if}}
{{#if user}}
<div class='badge-user-info'>
{{#link-to 'user' user}}
{{avatar user imageSize="extra_large"}}
<div class="details clearfix">
{{poster-name post=user}}
</div>
{{/link-to}}
<div class='earned'>
{{i18n 'badges.earned_n_times' count=grantCount}}
</div>
</div>
{{/if}}
{{#if userBadges}}
<div class={{unbound layoutClass}}>
{{#each ub in userBadges}}
<div class="badge-user">
{{#link-to 'user' ub.user classNames="badge-info"}}
{{avatar ub.user imageSize="large"}}
<div class="details">
<span class="username">{{ub.user.username}}</span>
{{format-date ub.granted_at}}
</div>
{{/link-to}}
{{#if user}}
{{format-date ub.granted_at}}
{{else}}
{{#link-to 'user' ub.user classNames="badge-info"}}
{{avatar ub.user imageSize="large"}}
<div class="details">
<span class="username">{{ub.user.username}}</span>
{{format-date ub.granted_at}}
</div>
{{/link-to}}
{{/if}}
{{#if ub.post_number}}
<a class="post-link" href="{{unbound ub.topic.url}}/{{unbound ub.post_number}}">{{{ub.topic.fancyTitle}}}</a>
{{/if}}
</div>
{{/each}}
{{#unless canLoadMore}}
{{#if user}}
<a class='load-more' href='{{model.url}}'>{{i18n 'badges.more_with_badge'}}</a>
{{/if}}
{{/unless}}
</div>
{{conditional-loading-spinner condition=canLoadMore}}

View File

@ -0,0 +1 @@
<input class="date-picker">

View File

@ -20,18 +20,13 @@
</section>
{{#if emailInEnabled}}
<section class='field'>
<label>
{{input type="checkbox" checked=category.contains_messages}}
{{i18n 'category.contains_messages'}}
</label>
</section>
<section class='field'>
<label>
{{input type="checkbox" checked=category.email_in_allow_strangers}}
{{i18n 'category.email_in_allow_strangers'}}
</label>
</section>
<section class='field'>
<label>
{{fa-icon "envelope-o"}}

View File

@ -4,7 +4,7 @@
<div id='period-popup' {{bind-attr class="showPeriods::hidden :period-popup"}}>
<ul>
{{#each p in site.periods}}
<li><a href {{action "changePeriod" p}}>{{period-title p}}</a></li>
<li><a href {{action "changePeriod" p}}>{{period-title p showDateRange=true}}</a></li>
{{/each}}
</ul>
</div>

View File

@ -1,7 +1,7 @@
{{#link-to 'badges.show' badge}}
<a href="{{badgeUrl}}">
{{#badge-button badge=badge}}
{{#if showGrantCount}}
<span class="count">(&times;&nbsp;{{count}})</span>
{{/if}}
{{/badge-button}}
{{/link-to}}
</a>

View File

@ -1,4 +1,5 @@
<div class="container">
<div class="container user-table">
<div class="wrapper">
<section class='user-navigation'>
<ul class='action-list nav-stacked'>
{{#each tabs as |tab|}}
@ -22,4 +23,5 @@
{{outlet}}
</section>
</section>
</div>
</div>

View File

@ -28,7 +28,7 @@
{{#if model.finished}}
{{d-button class="btn-primary" action="closeModal" label="close"}}
{{else}}
{{d-button icon="envelope" action="createInvite" class="btn-primary" disabled=disabled label=buttonTitle}}
{{d-button icon=inviteIcon action="createInvite" class="btn-primary" disabled=disabled label=buttonTitle}}
{{#if showCopyInviteButton}}
{{d-button icon="link" action="generateInvitelink" class="btn-primary" disabled=disabledCopyLink label='user.invited.generate_link'}}
{{/if}}

View File

@ -68,7 +68,7 @@
{{#if showBadges}}
<div class="badge-section">
{{#each ub in user.featured_user_badges}}
{{user-badge badge=ub.badge}}
{{user-badge badge=ub.badge user=user}}
{{/each}}
{{#if showMoreBadges}}
{{#link-to 'user.badges' user class="btn more-user-badges"}}

View File

@ -1,5 +1,5 @@
<section class='user-navigation'>
<ul class='action-list nav-stacked'>
<ul class='action-list activity-list nav-stacked'>
<li class='no-glyph'>
{{#link-to 'userActivity.index'}}{{i18n 'user.filters.all'}}{{/link-to}}

View File

@ -1,5 +1,5 @@
<section class='user-content user-badges-list'>
{{#each ub in controller}}
{{user-badge badge=ub.badge count=ub.count}}
{{user-badge badge=ub.badge count=ub.count user=user}}
{{/each}}
</section>

View File

@ -1,4 +1,9 @@
<section class='user-navigation'>
{{#unless mobileView}}
{{#if showNewPM}}
{{d-button class="btn-primary new-private-message" action="composePrivateMessage" icon="envelope" label="user.new_private_message"}}
{{/if}}
{{/unless}}
<ul class='action-list nav-stacked'>
<li class="noGlyph">
{{#link-to 'userPrivateMessages.index' model}}
@ -23,7 +28,7 @@
{{capitalize group.name}}
{{/link-to}}
</li>
<li>
<li class='archive'>
{{#link-to 'userPrivateMessages.groupArchive' group.name}}
{{i18n 'user.messages.archive'}}
{{/link-to}}
@ -32,7 +37,6 @@
{{/each}}
</ul>
{{d-button class="btn-primary new-private-message" action="composePrivateMessage" icon="envelope" label="user.new_private_message"}}
</section>
<section class='user-right messages'>
@ -42,6 +46,12 @@
<i class="fa fa-list"></i>
</button>
{{#if mobileView}}
{{#if showNewPM}}
{{d-button class="btn-primary new-private-message" action="composePrivateMessage" icon="envelope" label="user.new_private_message"}}
{{/if}}
{{/if}}
{{#if canArchive}}
<button {{action "archive"}} class="btn btn-archive">
{{i18n "user.messages.archive"}}

View File

@ -1,6 +1,6 @@
<section class='user-navigation'>
<ul class='action-list nav-stacked'>
<ul class='notification-list action-list nav-stacked'>
{{#if model}}
<li class='no-glyph'>
{{#link-to 'userNotifications.index'}}{{i18n 'user.filters.all'}}{{/link-to}}

View File

@ -178,7 +178,7 @@
{{/if}}
{{preference-checkbox labelKey="user.email_private_messages" checked=model.email_private_messages}}
{{preference-checkbox labelKey="user.email_direct" checked=model.email_direct}}
{{preference-checkbox labelKey="user.mailing_list_mode" checked=model.mailing_list_mode}}
<span class="pref-mailing-list-mode">{{preference-checkbox labelKey="user.mailing_list_mode" checked=model.mailing_list_mode}}</span>
{{preference-checkbox labelKey="user.email_always" checked=model.email_always}}
<div class='instructions'>

View File

@ -0,0 +1,62 @@
{{#if model.replies.length}}
<div class='top-section'>
<h3>{{i18n "user.summary.top_replies"}}</h3>
{{#each reply in model.replies}}
<ul>
<li>
<a href="{{reply.url}}">{{reply.topic.title}}</a> {{#if reply.like_count}}<span class='like-count'>{{reply.like_count}}<i class='fa fa-heart'></i></span>{{/if}} {{format-date reply.createdAt format="tiny" noTitle="true"}}
</li>
</ul>
{{/each}}
{{#if moreReplies}}
{{#link-to "userActivity.replies" user class="more"}}{{i18n "user.summary.more_replies"}}{{/link-to}}
{{/if}}
</div>
{{/if}}
{{#if model.topics.length}}
<div class='top-section'>
<h3>{{i18n "user.summary.top_topics"}}</h3>
{{#each topic in model.topics}}
<ul>
<li>
<a href="{{topic.url}}">{{topic.title}}</a> {{#if topic.like_count}}<span class='like-count'>{{topic.like_count}}<i class='fa fa-heart'></i></span>{{/if}} {{format-date topic.createdAt format="tiny" noTitle="true"}}
</li>
</ul>
{{/each}}
{{#if moreTopics}}
{{#link-to "userActivity.topics" user class="more"}}{{i18n "user.summary.more_topics"}}{{/link-to}}
{{/if}}
</div>
{{/if}}
<div class='top-section stats-section'>
<h3>{{i18n "user.summary.stats"}}</h3>
<dl>
<dt>{{i18n "user.summary.topic_count"}}</dt>
<dd>{{model.topic_count}}</dd>
<dt>{{i18n "user.summary.post_count"}}</dt>
<dd>{{model.post_count}}</dd>
<dt>{{i18n "user.summary.likes_given"}}</dt>
<dd>{{model.likes_given}}</dd>
<dt>{{i18n "user.summary.likes_received"}}</dt>
<dd>{{model.likes_received}}</dd>
<dt>{{i18n "user.summary.days_visited"}}</dt>
<dd>{{model.days_visited}}</dd>
<dt>{{i18n "user.summary.posts_read_count"}}</dt>
<dd>{{model.posts_read_count}}</dd>
</dl>
</div>
{{#if model.badges.length}}
<div class='top-section badges-section'>
<h3>{{i18n "user.summary.top_badges"}}</h3>
{{#each badge in model.badges}}
{{user-badge badge=badge count=badge.count user=user}}
{{/each}}
{{#if moreBadges}}
{{#link-to "user.badges" user class="more"}}{{i18n "user.summary.more_badges"}}{{/link-to}}
{{/if}}
</div>
{{/if}}

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