Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Kris Aubuchon
2014-02-11 00:44:10 -05:00
56 changed files with 727 additions and 390 deletions
@@ -24,6 +24,8 @@ Discourse.AdminUserIndexController = Discourse.ObjectController.extend({
return Discourse.SiteSettings.must_approve_users;
}.property(),
primaryGroupDirty: Discourse.computed.propertyNotEqual('originalPrimaryGroupId', 'primary_group_id'),
actions: {
toggleTitleEdit: function() {
this.toggleProperty('editingTitle');
@@ -44,6 +46,22 @@ Discourse.AdminUserIndexController = Discourse.ObjectController.extend({
this.get('model').generateApiKey();
},
savePrimaryGroup: function() {
var self = this;
Discourse.ajax("/admin/users/" + this.get('id') + "/primary_group", {
type: 'PUT',
data: {primary_group_id: this.get('primary_group_id')}
}).then(function () {
self.set('originalPrimaryGroupId', self.get('primary_group_id'));
}).catch(function() {
bootbox.alert(I18n.t('generic_error'));
});
},
resetPrimaryGroup: function() {
this.set('primary_group_id', this.get('originalPrimaryGroupId'));
},
regenerateApiKey: function() {
var self = this;
bootbox.confirm(I18n.t("admin.api.confirm_regen"), I18n.t("no_value"), I18n.t("yes_value"), function(result) {
@@ -19,7 +19,7 @@ Discourse.AdminDashboard.reopenClass({
@return {jqXHR} a jQuery Promise object
**/
find: function() {
return Discourse.ajax("/admin/dashboard").then(function(json) {
return Discourse.ajax("/admin/dashboard.json").then(function(json) {
var model = Discourse.AdminDashboard.create(json);
model.set('loaded', true);
return model;
@@ -34,7 +34,7 @@ Discourse.AdminDashboard.reopenClass({
@return {jqXHR} a jQuery Promise object
**/
fetchProblems: function() {
return Discourse.ajax("/admin/dashboard/problems", {
return Discourse.ajax("/admin/dashboard/problems.json", {
type: 'GET',
dataType: 'json'
}).then(function(json) {
@@ -388,7 +388,7 @@ Discourse.AdminUser.reopenClass({
},
find: function(username) {
return Discourse.ajax("/admin/users/" + username).then(function (result) {
return Discourse.ajax("/admin/users/" + username + ".json").then(function (result) {
result.loadedDetails = true;
return Discourse.AdminUser.create(result);
});
@@ -23,10 +23,16 @@ Discourse.AdminUserRoute = Discourse.Route.extend({
afterModel: function(adminUser) {
var controller = this.controllerFor('adminUser');
adminUser.loadDetails().then(function () {
return adminUser.loadDetails().then(function () {
adminUser.setOriginalTrustLevel();
controller.set('model', adminUser);
window.scrollTo(0, 0);
});
},
setupController: function(controller, model) {
controller.setProperties({
originalPrimaryGroupId: model.get('primary_group_id'),
model: model
});
},
@@ -46,6 +46,25 @@
</div>
</div>
<div class='display-row'>
<div class='field'>{{i18n admin.groups.primary}}</div>
<div class='value'>
{{#if custom_groups}}
{{combobox content=custom_groups value=primary_group_id nameProperty="name" none="admin.groups.no_primary"}}
{{else}}
&mdash;
{{/if}}
</div>
<div class='controls'>
{{#if primaryGroupDirty}}
<div>
<button class='btn ok' {{action savePrimaryGroup}}><i class='fa fa-check'></i></button>
<button class='btn cancel' {{action resetPrimaryGroup}}><i class='fa fa-times'></i></button>
</div>
{{/if}}
</div>
</div>
<div class='display-row'>
<div class='field'>{{i18n user.ip_address.title}}</div>
<div class='value'>{{ip_address}}</div>
@@ -317,7 +336,7 @@
<section>
<hr/>
<button {{bind-attr class=":btn :btn-danger :pull-right deleteForbidden:hidden"}} {{action destroy target="content"}} {{bind-attr disabled="deleteForbidden"}} {{bind-attr}}>
<button {{bind-attr class=":btn :btn-danger :pull-right deleteForbidden:hidden"}} {{action destroy target="content"}} {{bind-attr disabled="deleteForbidden"}}>
<i class="fa fa-exclamation-triangle"></i>
{{i18n admin.user.delete}}
</button>
@@ -0,0 +1,10 @@
/**
The view class for an Admin User
@class AdminUserView
@extends Discourse.View
@namespace Discourse
@module Discourse
**/
Discourse.AdminUserView = Discourse.View.extend(Discourse.ScrollTop);
@@ -0,0 +1,12 @@
/**
Displays a list of groups that a user belongs to.
@class Discourse.GroupsListComponent
@extends Ember.Component
@namespace Discourse
@module Discourse
**/
Discourse.GroupsListComponent = Em.Component.extend({
classNames: ['groups']
});
@@ -25,7 +25,10 @@ Discourse.PostGapComponent = Ember.Component.extend({
if (this.get('loading')) {
buffer.push(I18n.t('loading'));
} else {
buffer.push(I18n.t('post.gap', {count: this.get('gap.length')}));
var gapLength = this.get('gap.length');
if (gapLength) {
buffer.push(I18n.t('post.gap', {count: gapLength}));
}
}
},
@@ -29,7 +29,7 @@ Discourse.StaticController = Discourse.Controller.extend({
text = text.match(/<!-- preload-content: -->((?:.|[\n\r])*)<!-- :preload-content -->/)[1];
this.set('content', text);
} else {
return Discourse.ajax(path, {dataType: 'html'}).then(function (result) {
return Discourse.ajax(path + ".html", {dataType: 'html'}).then(function (result) {
self.set('content', result);
});
}
@@ -37,6 +37,11 @@ Discourse.ScreenTrack = Ember.Object.extend({
},
stop: function() {
if(!this.get('topicId')) {
// already stopped no need to "extra stop"
return;
}
this.tick();
this.flush();
this.reset();
@@ -105,9 +110,10 @@ Discourse.ScreenTrack = Ember.Object.extend({
var highestSeenByTopic = Discourse.Session.currentProp('highestSeenByTopic');
if ((highestSeenByTopic[topicId] || 0) < highestSeen) {
highestSeenByTopic[topicId] = highestSeen;
Discourse.TopicTrackingState.current().updateSeen(topicId, highestSeen);
}
Discourse.TopicTrackingState.current().updateSeen(topicId, highestSeen);
if (!$.isEmptyObject(newTimings)) {
Discourse.ajax('/topics/timings', {
data: {
@@ -0,0 +1,18 @@
/**
This mixin will cause a view to scroll the viewport to the top once it has been inserted
@class Discourse.ScrollTop
@extends Ember.Mixin
@namespace Discourse
@module Discourse
**/
Discourse.ScrollTop = Em.Mixin.create({
_scrollTop: function() {
Em.run.schedule('afterRender', function() {
$(document).scrollTop(0);
});
}.on('didInsertElement'),
});
@@ -39,8 +39,9 @@ Discourse.TopicTrackingState = Discourse.Model.extend({
},
updateSeen: function(topicId, highestSeen) {
if(!topicId || !highestSeen) { return; }
var state = this.states["t" + topicId];
if(state && state.last_read_post_number < highestSeen) {
if(state && (!state.last_read_post_number || state.last_read_post_number < highestSeen)) {
state.last_read_post_number = highestSeen;
this.incrementMessageCount();
}
@@ -84,9 +85,24 @@ Discourse.TopicTrackingState = Discourse.Model.extend({
sync: function(list, filter){
var tracker = this;
var states = this.states;
if(!list || !list.topics) { return; }
// compensate for delayed "new" topics
// client side we know they are not new, server side we think they are
for(var i=list.topics.length-1; i>=0; i--){
var state = states["t"+ list.topics[i].id];
if(state && state.last_read_post_number > 0){
if(filter === "new"){
list.topics.splice(i, 1);
} else {
list.topics[i].unseen = false;
list.topics[i].dont_sync = true;
}
}
}
if(filter === "new" && !list.more_topics_url){
// scrub all new rows and reload from list
_.each(this.states, function(state){
@@ -112,10 +128,11 @@ Discourse.TopicTrackingState = Discourse.Model.extend({
if(topic.unseen) {
row.last_read_post_number = null;
} else if (topic.unread || topic.new_posts){
// subtle issue here
row.last_read_post_number = topic.highest_post_number - ((topic.unread||0) + (topic.new_posts||0));
} else {
delete tracker.states["t" + topic.id];
if(!topic.dont_sync) {
delete tracker.states["t" + topic.id];
}
return;
}
@@ -11,6 +11,9 @@ function buildTopicRoute(filter) {
},
model: function() {
// attempt to stop early cause we need this to be called before .sync
Discourse.ScreenTrack.current().stop();
return Discourse.TopicList.list(filter).then(function(list) {
var tracking = Discourse.TopicTrackingState.current();
if (tracking) {
@@ -0,0 +1,6 @@
{{#if groups}}
{{i18n groups.title count=groups.length}}:
{{#each groups}}
{{#link-to 'group' this class="group-link"}}{{name}}{{/link-to}}
{{/each}}
{{/if}}
@@ -29,7 +29,7 @@
{{/each}}
{{else}}
<label>{{i18n category.parent}}</label>
{{categoryChooser valueAttribute="id" value=parent_category_id categories=parentCategories}}
{{categoryChooser valueAttribute="id" value=parent_category_id categories=parentCategories rootNone=true}}
{{/if}}
</section>
@@ -29,6 +29,7 @@
{{/if}}
{{#if user_title}}<div class="user-title" {{action showPosterExpansion this}}>{{user_title}}</div>{{/if}}
{{#if primary_group_name}}<div><a href='/groups/{{unbound primary_group_name}}' class='user-group'>{{unbound primary_group_name}}</a></div>{{/if}}
</div>
{{else}}
<div class="contents">
@@ -10,6 +10,8 @@
<h3>{{i18n last_post}} {{unboundDate path="user.last_posted_at" leaveAgo="true"}}</h3>
<h3>{{i18n joined}} {{unboundDate path="user.created_at" leaveAgo="true"}}</h3>
{{groups-list groups=user.custom_groups}}
<div class='bottom'>
{{#if user.bio_cooked}}<div class='bio'>{{{user.bio_cooked}}}</div>{{/if}}
@@ -43,14 +43,7 @@
<div class='bio'>{{{bio_cooked}}}</div>
{{#if custom_groups}}
<div class='groups'>
{{i18n groups.title count=custom_groups.length}}:
{{#each custom_groups}}
{{#link-to 'group' this class="group-link"}}{{name}}{{/link-to}}
{{/each}}
</div>
{{/if}}
{{groups-list groups=custom_groups}}
{{#if isSuspended}}
<div class='suspended'>
@@ -26,7 +26,11 @@ Discourse.CategoryChooserView = Discourse.ComboboxView.extend({
none: function() {
if (Discourse.User.currentProp('staff') || Discourse.SiteSettings.allow_uncategorized_topics) {
return 'category.none';
if (this.get('rootNone')) {
return "category.none";
} else {
return Discourse.Category.list().findBy('id', Discourse.Site.currentProp('uncategorized_category_id'));
}
} else {
return 'category.choose';
}
@@ -11,41 +11,45 @@ Discourse.ComboboxView = Discourse.View.extend({
classNames: ['combobox'],
valueAttribute: 'id',
render: function(buffer) {
buildData: function(o) {
var data = "";
if (this.dataAttributes) {
this.dataAttributes.forEach(function(a) {
data += "data-" + a + "=\"" + o.get(a) + "\" ";
});
}
return data;
},
var nameProperty = this.get('nameProperty') || 'name';
render: function(buffer) {
var nameProperty = this.get('nameProperty') || 'name',
none = this.get('none');
// Add none option if required
if (this.get('none')) {
buffer.push('<option value="">' + (I18n.t(this.get('none'))) + "</option>");
if (typeof none === "string") {
buffer.push('<option value="">' + I18n.t(none) + "</option>");
} else if (typeof none === "object") {
buffer.push("<option value=\"\" " + this.buildData(none) + ">" + Em.get(none, nameProperty) + "</option>");
}
var selected = this.get('value');
if (selected) { selected = selected.toString(); }
if (this.get('content')) {
var comboboxView = this;
_.each(this.get('content'),function(o) {
var val = o[comboboxView.get('valueAttribute')];
var self = this;
this.get('content').forEach(function(o) {
var val = o[self.get('valueAttribute')];
if (val) { val = val.toString(); }
var selectedText = (val === selected) ? "selected" : "";
var data = "";
if (comboboxView.dataAttributes) {
comboboxView.dataAttributes.forEach(function(a) {
data += "data-" + a + "=\"" + o.get(a) + "\" ";
});
}
buffer.push("<option " + selectedText + " value=\"" + val + "\" " + data + ">" + Em.get(o, nameProperty) + "</option>");
buffer.push("<option " + selectedText + " value=\"" + val + "\" " + self.buildData(o) + ">" + Em.get(o, nameProperty) + "</option>");
});
}
},
valueChanged: function() {
var $combo = this.$();
var val = this.get('value');
var $combo = this.$(),
val = this.get('value');
if (val !== undefined && val !== null) {
$combo.val(val.toString());
} else {
@@ -55,8 +59,8 @@ Discourse.ComboboxView = Discourse.View.extend({
}.observes('value'),
didInsertElement: function() {
var $elem = this.$();
var comboboxView = this;
var $elem = this.$(),
self = this;
$elem.chosen({ template: this.template, disable_search_threshold: 5 });
if (this.overrideWidths) {
@@ -74,7 +78,7 @@ Discourse.ComboboxView = Discourse.View.extend({
}
$elem.chosen().change(function(e) {
comboboxView.set('value', $(e.target).val());
self.set('value', $(e.target).val());
});
}
@@ -7,15 +7,9 @@
@namespace Discourse
@module Discourse
**/
Discourse.DiscoveryTopicsView = Discourse.View.extend(Discourse.LoadMore, {
Discourse.DiscoveryTopicsView = Discourse.View.extend(Discourse.ScrollTop, Discourse.LoadMore, {
eyelineSelector: '.topic-list-item',
_scrollTop: function() {
Em.run.schedule('afterRender', function() {
$(document).scrollTop(0);
});
}.on('didInsertElement'),
actions: {
loadMore: function() {
var self = this;
@@ -1,4 +1,13 @@
Discourse.GroupIndexView = Discourse.View.extend(Discourse.LoadMore, {
/**
Displays all posts within a group
@class Discourse.GroupIndexView
@extends Ember.Mixin
@namespace Discourse
@module Discourse
**/
Discourse.GroupIndexView = Discourse.View.extend(Discourse.ScrollTop, Discourse.LoadMore, {
eyelineSelector: '.user-stream .item'
});
@@ -12,13 +12,21 @@ Discourse.PostView = Discourse.GroupedView.extend(Ember.Evented, {
classNameBindings: ['postTypeClass',
'selected',
'post.hidden:post-hidden',
'post.deleted'],
'post.deleted',
'groupNameClass'],
postBinding: 'content',
postTypeClass: function() {
return this.get('post.post_type') === Discourse.Site.currentProp('post_types.moderator_action') ? 'moderator' : 'regular';
}.property('post.post_type'),
groupNameClass: function() {
var primaryGroupName = this.get('post.primary_group_name');
if (primaryGroupName) {
return "group-" + primaryGroupName;
}
}.property('post.primary_group_name'),
// If the cooked content changed, add the quote controls
cookedChanged: function() {
var postView = this;
@@ -40,6 +40,16 @@
margin-top: 0px;
color: $primary_medium;
}
.groups {
font-size: 13px;
font-weight: normal;
margin-top: 0px;
color: $primary_medium;
.group-link {
color: $primary;
}
}
.bottom {
clear: both;
+14 -4
View File
@@ -461,17 +461,26 @@ iframe {
width: 45px;
height: 45px;
}
.contents {
.contents {
text-align: center;
a {
display: block;
a {
display: block;
margin: 0 auto;
width: 45px;
}
a.user-group {
margin: 4px 0 0 0;
padding: 0px;
color: $primary_light;
font-size: 80%;
width: 100%;
line-height: 13px;
}
h3 a {
display: inline;
width: auto;
@@ -614,6 +623,7 @@ position: relative;
}
}
.user-title {
margin-top: 8px;
color: $primary_light;
+6 -3
View File
@@ -2,6 +2,12 @@
@import "common/foundation/variables";
@import "common/foundation/mixins";
.groups {
.group-link {
color: $tertiary_lightest;
}
}
.user-preferences {
input.category-group {
width: 500px;
@@ -210,9 +216,6 @@
h1, h2 {margin-top: 10px;}
.group-link {
color: $tertiary_lightest;
}
.bio {
color: $primary_lighter;
@@ -442,6 +442,7 @@ iframe {
float: left;
}
.user-title {
color: #aaa;
padding-top: 2px;
@@ -17,6 +17,7 @@ class Admin::UsersController < Admin::AdminController
:block,
:unblock,
:trust_level,
:primary_group,
:generate_api_key,
:revoke_api_key]
@@ -94,6 +95,13 @@ class Admin::UsersController < Admin::AdminController
render_serialized(@user, AdminUserSerializer)
end
def primary_group
guardian.ensure_can_change_primary_group!(@user)
@user.primary_group_id = params[:primary_group_id]
@user.save!
render nothing: true
end
def trust_level
guardian.ensure_can_change_trust_level!(@user)
logger = StaffActionLogger.new(current_user)
+5
View File
@@ -40,6 +40,11 @@ class ListController < ApplicationController
list_opts = build_topic_list_options
list_opts.merge!(options) if options
user = list_target_user
if filter == :latest && params[:category].blank?
list_opts[:no_definitions] = true
end
list = TopicQuery.new(user, list_opts).public_send("list_#{filter}")
list.more_topics_url = construct_url_with(list_opts)
if Discourse.anonymous_filters.include?(filter)
@@ -30,7 +30,8 @@ module Jobs
)', post.topic.category_id, CategoryUser.notification_levels[:muted])
.each do |user|
if Guardian.new(user).can_see?(post)
UserNotifications.mailing_list_notify(user, post).deliver
message = UserNotifications.mailing_list_notify(user, post)
Email::Sender.new(message, :mailing_list, user).send
end
end
+1 -1
View File
@@ -1,7 +1,7 @@
module Jobs
# various consistency checks
class EnsureDbConsistency < Jobs::Scheduled
every 1.day
every 12.hours
def execute(args)
TopicUser.ensure_consistency!
+1
View File
@@ -220,6 +220,7 @@ class Group < ActiveRecord::Base
if @deletions
@deletions.each do |gu|
gu.destroy
User.update_all 'primary_group_id = NULL', ['id = ? AND primary_group_id = ?', gu.user_id, gu.group_id]
end
end
@deletions = nil
@@ -14,12 +14,14 @@ class AdminDetailedUserSerializer < AdminUserSerializer
:private_topics_count,
:can_delete_all_posts,
:can_be_deleted,
:suspend_reason
:suspend_reason,
:primary_group_id
has_one :approved_by, serializer: BasicUserSerializer, embed: :objects
has_one :api_key, serializer: ApiKeySerializer, embed: :objects
has_one :suspended_by, serializer: BasicUserSerializer, embed: :objects
has_one :leader_requirements, serializer: LeaderRequirementsSerializer, embed: :objects
has_many :custom_groups, embed: :object, serializer: BasicGroupSerializer
def can_revoke_admin
scope.can_revoke_admin?(object)
+14 -9
View File
@@ -29,31 +29,36 @@ class ListableTopicSerializer < BasicTopicSerializer
end
def seen
object.user_data.present?
return true if !scope || !scope.user
return true if object.user_data && !object.user_data.last_read_post_number.nil?
return true if object.created_at < scope.user.treat_as_new_topic_start_date
false
end
def unseen
return false if scope.blank?
return false if scope.user.blank?
return false if object.user_data.present?
return false if object.created_at < scope.user.treat_as_new_topic_start_date
true
!seen
end
def last_read_post_number
return nil unless object.user_data
object.user_data.last_read_post_number
end
alias :include_last_read_post_number? :seen
def has_user_data
!!object.user_data
end
alias :include_last_read_post_number? :has_user_data
def unread
unread_helper.unread_posts
end
alias :include_unread? :seen
alias :include_unread? :has_user_data
def new_posts
unread_helper.new_posts
end
alias :include_new_posts? :seen
alias :include_new_posts? :has_user_data
def include_excerpt?
pinned
+6
View File
@@ -23,6 +23,7 @@ class PostSerializer < BasicPostSerializer
:topic_slug,
:topic_id,
:display_username,
:primary_group_name,
:version,
:can_edit,
:can_delete,
@@ -75,6 +76,11 @@ class PostSerializer < BasicPostSerializer
object.user.try(:name)
end
def primary_group_name
return nil unless object.user && @topic_view
return @topic_view.primary_group_names[object.user.primary_group_id] if object.user.primary_group_id
end
def link_counts
return @single_post_link_counts if @single_post_link_counts.present?
@@ -13,7 +13,7 @@ class TopicListItemSerializer < ListableTopicSerializer
def starred
object.user_data.starred?
end
alias :include_starred? :seen
alias :include_starred? :has_user_data
def posters
object.posters || []