Version bump

This commit is contained in:
Neil Lalonde 2016-01-25 13:41:18 -05:00
commit f42d1c8f63
350 changed files with 8519 additions and 5133 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
lang_map = es_ES: es, fr_FR: fr, ko_KR: ko, pt_PT: pt, sk_SK: sk
[discourse-org.clientenyml]
file_filter = config/locales/client.<lang>.yml

View File

@ -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,3 +1,14 @@
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:
@ -38,17 +49,17 @@ GEM
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)
@ -94,10 +105,10 @@ GEM
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,7 +166,7 @@ GEM
lru_redux (1.1.0)
mail (2.6.3)
mime-types (>= 1.16, < 3)
memory_profiler (0.9.4)
memory_profiler (0.9.6)
message_bus (2.0.0.beta.2)
rack (>= 1.1.3)
redis
@ -166,9 +177,9 @@ GEM
minitest (5.8.3)
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)
@ -176,7 +187,7 @@ GEM
netrc (0.11.0)
nokogiri (1.6.7.1)
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)
@ -270,7 +278,7 @@ GEM
rake (10.4.2)
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)
@ -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

@ -3,6 +3,7 @@ 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).
@ -42,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'});
@ -255,7 +256,7 @@ export default Ember.Component.extend({
template: template,
key: '#',
transformComplete(category) {
return category.get('slug');
return Category.slugFor(category, categoryHashtagSeparator);
},
dataSource(term) {
return Category.search(term);

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

@ -7,6 +7,12 @@ export default Ember.Controller.extend(ModalFunctionality, {
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

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

@ -4,7 +4,7 @@
**/
Discourse.Dialect.inlineRegexp({
start: '#',
matcher: /^#([\w-]{1,50})/i,
matcher: /^#([\w-:]{1,50})/i,
spaceOrTagBoundary: true,
emitter: function(matches) {
@ -15,7 +15,7 @@ Discourse.Dialect.inlineRegexp({
result = categoryHashtagLookup && categoryHashtagLookup(slug);
if (result && result[0] === "category") {
return ['a', { class: attributeClass, href: result[1] }, hashtag];
return ['a', { class: attributeClass, href: result[1] }, '#', ["span", {}, slug]];
} else {
return ['span', { class: attributeClass }, hashtag];
}

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

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

@ -1,12 +1,10 @@
import { replaceSpan } from 'discourse/lib/category-hashtags';
const validCategoryHashtags = {};
const checkedCategoryHashtags = [];
const testedKey = 'tested';
const testedClass = `hashtag-${testedKey}`;
function replaceSpan($elem, categorySlug, categoryLink) {
$elem.replaceWith(`<a href="${categoryLink}" class="hashtag">#${categorySlug}</a>`);
}
function updateFound($hashtags, categorySlugs) {
Ember.run.schedule('afterRender', () => {
$hashtags.each((index, hashtag) => {

View File

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

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'),
@ -299,11 +298,18 @@ Category.reopenClass({
}
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 = [];
term = term.toLowerCase();
const done = () => {
return data.length === limit;
@ -312,7 +318,10 @@ Category.reopenClass({
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)) {
(!emptyTerm &&
(category.get('name').toLowerCase().indexOf(term) === 0 ||
category.get('slug').toLowerCase().indexOf(slugTerm) === 0))) {
data.push(category);
}
}
@ -321,7 +330,10 @@ Category.reopenClass({
for (i = 0; i < length && !done(); i++) {
const category = categories[i];
if ((!emptyTerm && category.get('name').toLowerCase().indexOf(term) > 0)) {
if (!emptyTerm &&
(category.get('name').toLowerCase().indexOf(term) > 0 ||
category.get('slug').toLowerCase().indexOf(slugTerm) > 0)) {
if (data.indexOf(category) === -1) data.push(category);
}
}

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

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

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

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

View File

@ -149,7 +149,7 @@
</section>
<ul class="nav nav-pills user-nav">
<li class='selected'>{{#link-to 'userActivity'}}{{i18n 'user.activity_stream'}}{{/link-to}}</li>
<li>{{#link-to 'userActivity'}}{{i18n 'user.activity_stream'}}{{/link-to}}</li>
{{#if showNotificationsTab}}
<li>
{{#link-to 'userNotifications'}}
@ -167,12 +167,17 @@
{{#if showBadges}}
<li>{{#link-to 'user.badges'}}{{fa-icon "certificate"}}{{i18n 'badges.title'}}{{/link-to}}</li>
{{/if}}
<li>{{#link-to 'user.summary'}}{{i18n 'user.summary.title'}}{{/link-to}}</li>
{{#if model.can_edit}}
<li>{{#link-to 'preferences'}}{{fa-icon "cog"}}{{i18n 'user.preferences'}}{{/link-to}}</li>
{{/if}}
</ul>
{{outlet}}
<div class='user-table'>
<div class='wrapper'>
{{outlet}}
</div>
</div>
</section>
</div>

View File

@ -6,9 +6,14 @@ export default ModalBodyView.extend({
classNames: ['create-account'],
_setup: function() {
// allows the submission the form when pressing 'ENTER' on *any* text input field
// but only when the submit button is enabled
// Allows submitting the form when pressing 'ENTER' on *any* text input field
// but only when the submit button is enabled.
const createAccountController = this.get('controller');
if ($.cookie('email')) {
createAccountController.set('accountEmail', $.cookie('email'));
}
Em.run.schedule('afterRender', function() {
$("input[type='text'], input[type='password']").keydown(function(e) {
if (createAccountController.get('submitDisabled') === false && e.keyCode === 13) {

View File

@ -15,8 +15,13 @@ export default ModalBodyView.extend({
// Get username and password from the browser's password manager,
// if it filled the hidden static login form:
loginController.set('loginName', $('#hidden-login-form input[name=username]').val());
loginController.set('loginPassword', $('#hidden-login-form input[name=password]').val());
var prefillUsername = $('#hidden-login-form input[name=username]').val();
if (prefillUsername) {
loginController.set('loginName', prefillUsername);
loginController.set('loginPassword', $('#hidden-login-form input[name=password]').val());
} else if ($.cookie('email')) {
loginController.set('loginName', $.cookie('email'));
}
Em.run.schedule('afterRender', function() {
$('#login-account-password, #login-account-name').keydown(function(e) {

View File

@ -3,6 +3,7 @@ import { number } from 'discourse/lib/formatter';
import DiscourseURL from 'discourse/lib/url';
import { default as computed, on } from 'ember-addons/ember-computed-decorators';
import { fmt } from 'discourse/lib/computed';
import { isValidLink } from 'discourse/lib/click-track';
const DAY = 60 * 50 * 1000;
@ -192,8 +193,7 @@ const PostView = Discourse.GroupedView.extend(Ember.Evented, {
if (valid) {
// don't display badge counts on category badge & oneboxes (unless when explicitely stated)
if ($link.hasClass("track-link") ||
$link.closest('.badge-category,.onebox-result,.onebox-body').length === 0) {
if (isValidLink($link)) {
$link.append("<span class='badge badge-notification clicks' title='" + I18n.t("topic_map.clicks", {count: lc.clicks}) + "'>" + number(lc.clicks) + "</span>");
}
}

View File

@ -0,0 +1,9 @@
//= depend_on 'client.sk.yml'
//= require locales/i18n
<%= JsLocaleHelper.output_locale(:sk) %>
I18n.pluralizationRules['sk'] = function (n) {
if (n == 1) return "one";
if (n >= 2 && n <= 4) return "few";
return "other";
};

View File

@ -1532,7 +1532,7 @@ button.ru {
@media all
and (max-width : 850px) {
html:not(.mobile-view) .nav-stacked {
html:not(.mobile-view) .admin-content .nav-stacked {
.glyph {width: auto; position: relative;}
> li > a {padding: 13px}
}
@ -1606,10 +1606,9 @@ and (max-width : 500px) {
border-bottom: 1px solid #dfdfdf;
}
.actions {
font-size: 1.214em;
float: right;
a {
margin-left: 5px;
.btn {
padding: 3px 6px;
}
}
}
@ -1769,6 +1768,30 @@ table#user-badges {
}
}
// Emails
.email-list {
.filters input {
width: 100%;
}
.time {
width: 50px;
}
.username div {
max-width: 180px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.addresses p {
margin: 2px 0;
max-width: 200px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
// Mobile specific styles
// Mobile view text-inputs need some padding
.mobile-view .admin-contents {

View File

@ -241,15 +241,17 @@ ol.category-breadcrumb {
h2 {
float: left;
margin: 5px 0 10px;
.top-date-string {
color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%));
font-weight: normal;
font-size: 0.7em;
text-transform: uppercase;
}
}
.top-date-string {
color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%));
font-weight: normal;
text-transform: uppercase;
}
button {
outline: 0;
background: transparent;
@ -262,6 +264,7 @@ ol.category-breadcrumb {
@include unselectable;
font-size: 1.2em;
border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
padding: 5px;
background: $secondary;
@ -277,9 +280,15 @@ ol.category-breadcrumb {
li {
margin: 0;
padding: 0;
a {
font-weight: bold;
a, a:visited {
display: block;
padding: 5px;
color: $primary;
}
.top-date-string {
font-weight: normal;
font-size: 0.8em;
}
&:hover {
background-color: dark-light-diff($highlight, $secondary, 50%, -70%);

View File

@ -33,6 +33,15 @@
}
}
.date-picker-wrapper {
display: inline-block;
position: relative;
}
.pika-single {
position: absolute !important;
}
.modal-body.feature-topic {
padding: 5px;
max-height: 500px;

View File

@ -156,9 +156,6 @@ aside.quote {
background-color: dark-light-diff($tertiary, $secondary, 85%, -65%);
}
}
.wiki .topic-body {
background-color: dark-light-diff($wiki, $secondary, 95%, -50%);
}
// this ensures consistent top margin on topic posts even if the first line of a post
// is a top-margin-less element like a list or image.

View File

@ -187,6 +187,58 @@
}
}
.show-badge .badge-user-info {
margin-left: 2%;
.earned {
margin-top: 15px;
font-size: 1.3em;
}
.username {
margin-top: 5px;
display: block;
color: dark-light-choose(scale-color($primary, $lightness: 30%), scale-color($secondary, $lightness: 70%));
}
}
.show-badge .single-user {
margin-left: 2%;
padding-bottom: 20px;
.load-more {
padding-top: 30px;
display: block;
font-size: 1.2em;
}
}
.show-badge .single-user .badge-user {
padding-left: 0;
text-align: left;
display: block;
margin: 20px 0;
.badge-info {
display: none;
}
.date {
display: inline-block;
font-size: 1.1em;
margin-left: 10px;
}
.post-link {
font-size: 1.3em;
width: 500px;
margin: 0;
padding: 0;
}
width: 800px;
&:after {
content: "";
clear: both;
display: table;
}
}
.long-description.banner {
width: 88%;
margin-bottom: 20px;

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