Merge branch 'master' of github.com:discourse/discourse

This commit is contained in:
Sam 2015-01-05 09:01:54 +11:00
commit 9aba6ab265
43 changed files with 344 additions and 256 deletions

View File

@ -1,8 +1,19 @@
export default Ember.Component.extend({ export default Ember.Component.extend({
classNameBindings: ['containerClass'], classNameBindings: ['containerClass'],
layoutName: 'components/conditional-loading-spinner',
containerClass: function() { containerClass: function() {
return (this.get('size') === 'small') ? 'inline-spinner' : undefined; return (this.get('size') === 'small') ? 'inline-spinner' : undefined;
}.property('size') }.property('size'),
render: function(buffer) {
if (this.get('condition')) {
buffer.push('<div class="spinner ' + this.get('size') + '"}}></div>');
} else {
return this._super();
}
},
_conditionChanged: function() {
this.rerender();
}.observes('condition')
}); });

View File

@ -146,9 +146,9 @@ export default ObjectController.extend(ModalFunctionality, {
}).catch(function(error) { }).catch(function(error) {
if (error && error.responseText) { if (error && error.responseText) {
self.flash($.parseJSON(error.responseText).errors[0]); self.flash($.parseJSON(error.responseText).errors[0], 'error');
} else { } else {
self.flash(I18n.t('generic_error')); self.flash(I18n.t('generic_error'), 'error');
} }
self.set('saving', false); self.set('saving', false);
}); });

View File

@ -1,7 +1,8 @@
import ObjectController from 'discourse/controllers/object'; import ObjectController from 'discourse/controllers/object';
import BufferedContent from 'discourse/mixins/buffered-content';
import { spinnerHTML } from 'discourse/helpers/loading-spinner'; import { spinnerHTML } from 'discourse/helpers/loading-spinner';
export default ObjectController.extend(Discourse.SelectedPostsCount, { export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedContent, {
multiSelect: false, multiSelect: false,
needs: ['header', 'modal', 'composer', 'quote-button', 'search', 'topic-progress', 'application'], needs: ['header', 'modal', 'composer', 'quote-button', 'search', 'topic-progress', 'application'],
allPostsSelected: false, allPostsSelected: false,
@ -235,11 +236,6 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, {
this.set('allPostsSelected', false); this.set('allPostsSelected', false);
}, },
/**
Toggle a participant for filtering
@method toggleParticipant
**/
toggleParticipant: function(user) { toggleParticipant: function(user) {
this.get('postStream').toggleParticipant(Em.get(user, 'username')); this.get('postStream').toggleParticipant(Em.get(user, 'username'));
}, },
@ -247,17 +243,13 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, {
editTopic: function() { editTopic: function() {
if (!this.get('details.can_edit')) return false; if (!this.get('details.can_edit')) return false;
this.setProperties({ this.set('editingTopic', true);
editingTopic: true,
newTitle: this.get('title'),
newCategoryId: this.get('category_id')
});
return false; return false;
}, },
// close editing mode
cancelEditingTopic: function() { cancelEditingTopic: function() {
this.set('editingTopic', false); this.set('editingTopic', false);
this.rollbackBuffer();
}, },
toggleMultiSelect: function() { toggleMultiSelect: function() {
@ -265,39 +257,24 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, {
}, },
finishedEditingTopic: function() { finishedEditingTopic: function() {
if (this.get('editingTopic')) { if (!this.get('editingTopic')) { return; }
var topic = this.get('model'); // save the modifications
var self = this,
props = this.get('buffered.buffer');
// Topic title hasn't been sanitized yet, so the template shouldn't trust it. Discourse.Topic.update(this.get('model'), props).then(function() {
this.set('topicSaving', true); // Note we roll back on success here because `update` saves
// the properties to the topic.
// manually update the titles & category self.rollbackBuffer();
var backup = topic.setPropertiesBackup({
title: this.get('newTitle'),
category_id: parseInt(this.get('newCategoryId'), 10),
fancy_title: this.get('newTitle')
});
// save the modifications
var self = this;
topic.save().then(function(result){
// update the title if it has been changed (cleaned up) server-side
topic.setProperties(Em.getProperties(result.basic_topic, 'title', 'fancy_title'));
self.set('topicSaving', false);
}, function(error) {
self.setProperties({ editingTopic: true, topicSaving: false });
topic.setProperties(backup);
if (error && error.responseText) {
bootbox.alert($.parseJSON(error.responseText).errors[0]);
} else {
bootbox.alert(I18n.t('generic_error'));
}
});
// close editing mode
self.set('editingTopic', false); self.set('editingTopic', false);
} }).catch(function(error) {
if (error && error.responseText) {
bootbox.alert($.parseJSON(error.responseText).errors[0]);
} else {
bootbox.alert(I18n.t('generic_error'));
}
});
}, },
toggledSelectedPost: function(post) { toggledSelectedPost: function(post) {

View File

@ -1,5 +1,6 @@
Handlebars.registerHelper('link-domain', function(property, options) { import registerUnbound from 'discourse/helpers/register-unbound';
var link = Em.get(this, property, options);
registerUnbound('link-domain', function(link) {
if (link) { if (link) {
var internal = Em.get(link, 'internal'), var internal = Em.get(link, 'internal'),
hasTitle = (!Em.isEmpty(Em.get(link, 'title'))); hasTitle = (!Em.isEmpty(Em.get(link, 'title')));

View File

@ -1,46 +0,0 @@
// TODO: Make this a proper ES6 import
var ComposerView = require('discourse/views/composer').default;
ComposerView.on("initWmdEditor", function(){
if (!Discourse.SiteSettings.enable_emoji) { return; }
var template = Handlebars.compile(
"<div class='autocomplete'>" +
"<ul>" +
"{{#each options}}" +
"<li>" +
"<a href='#'><img src='{{src}}' class='emoji'> {{code}}</a>" +
"</li>" +
"{{/each}}" +
"</ul>" +
"</div>"
);
$('#wmd-input').autocomplete({
template: template,
key: ":",
transformComplete: function(v){ return v.code + ":"; },
dataSource: function(term){
return new Ember.RSVP.Promise(function(resolve) {
var full = ":" + term;
term = term.toLowerCase();
if (term === "") {
return resolve(["smile", "smiley", "wink", "sunny", "blush"]);
}
if (Discourse.Emoji.translations[full]) {
return resolve([Discourse.Emoji.translations[full]]);
}
var options = Discourse.Emoji.search(term, {maxResults: 5});
return resolve(options);
}).then(function(list) {
return list.map(function(i) {
return {code: i, src: Discourse.Emoji.urlFor(i)};
});
});
}
});
});

View File

@ -58,6 +58,7 @@ Discourse.Category = Discourse.Model.extend({
return Discourse.ajax(url, { return Discourse.ajax(url, {
data: { data: {
name: this.get('name'), name: this.get('name'),
slug: this.get('slug'),
color: this.get('color'), color: this.get('color'),
text_color: this.get('text_color'), text_color: this.get('text_color'),
secure: this.get('secure'), secure: this.get('secure'),

View File

@ -463,13 +463,10 @@ Discourse.Composer = Discourse.Model.extend({
// Update the title if we've changed it // Update the title if we've changed it
if (this.get('title') && post.get('post_number') === 1) { if (this.get('title') && post.get('post_number') === 1) {
var topic = this.get('topic'); Discourse.Topic.update(this.get('topic'), {
topic.setProperties({
title: this.get('title'), title: this.get('title'),
fancy_title: Handlebars.Utils.escapeExpression(this.get('title')), category_id: this.get('categoryId')
category_id: parseInt(this.get('categoryId'), 10)
}); });
topic.save();
} }
post.setProperties({ post.setProperties({

View File

@ -30,7 +30,7 @@ Discourse.ExportCsv.reopenClass({
@method export_user_list @method export_user_list
**/ **/
exportUserList: function() { exportUserList: function() {
return Discourse.ajax("/export_csv/export_entity.json", {data: {entity_type: 'admin', entity: 'user'}}); return Discourse.ajax("/export_csv/export_entity.json", {data: {entity_type: 'admin', entity: 'user_list'}});
}, },
/** /**

View File

@ -1,12 +1,4 @@
Discourse.Model = Ember.Object.extend(Discourse.Presence, { Discourse.Model = Ember.Object.extend(Discourse.Presence);
// Like `setProperties` but returns the original values in case
// we want to roll back
setPropertiesBackup: function(obj) {
var backup = this.getProperties(Ember.keys(obj));
this.setProperties(obj);
return backup;
}
});
Discourse.Model.reopenClass({ Discourse.Model.reopenClass({
extractByKey: function(collection, klass) { extractByKey: function(collection, klass) {

View File

@ -202,23 +202,6 @@ Discourse.Topic = Discourse.Model.extend({
}); });
}, },
// Save any changes we've made to the model
save: function() {
// Don't save unless we can
if (!this.get('details.can_edit')) return;
var data = { title: this.get('title') };
if(this.get('category')){
data.category_id = this.get('category.id');
}
return Discourse.ajax(this.get('url'), {
type: 'PUT',
data: data
});
},
/** /**
Invite a user to this topic Invite a user to this topic
@ -373,6 +356,29 @@ Discourse.Topic.reopenClass({
} }
}, },
update: function(topic, props) {
props = JSON.parse(JSON.stringify(props)) || {};
// Annoyingly, empty arrays are not sent across the wire. This
// allows us to make a distinction between arrays that were not
// sent and arrays that we specifically want to be empty.
Object.keys(props).forEach(function(k) {
var v = props[k];
if (v instanceof Array && v.length === 0) {
props[k + '_empty_array'] = true;
}
});
return Discourse.ajax(topic.get('url'), { type: 'PUT', data: props }).then(function(result) {
// The title can be cleaned up server side
props.title = result.basic_topic.title;
props.fancy_title = result.basic_topic.fancy_title;
topic.setProperties(props);
});
},
create: function() { create: function() {
var result = this._super.apply(this, arguments); var result = this._super.apply(this, arguments);
this.createActionSummary(result); this.createActionSummary(result);

View File

@ -1,5 +0,0 @@
{{#if condition}}
<div {{bind-attr class=":spinner size"}}></div>
{{else}}
{{yield}}
{{/if}}

View File

@ -82,7 +82,7 @@
{{#if showAllLinksControls}} {{#if showAllLinksControls}}
<div class='link-summary'> <div class='link-summary'>
<a href='#' {{action "showAllLinks"}}>{{i18n 'topic_map.links_shown' totalLinks=details.links.length}}</a> <a href {{action "showAllLinks"}}>{{i18n 'topic_map.links_shown' totalLinks=details.links.length}}</a>
</div> </div>
{{/if}} {{/if}}

View File

@ -0,0 +1,9 @@
<div class='autocomplete'>
<ul>
{{#each options}}
<li>
<a href><img src='{{src}}' class='emoji'> {{code}}</a>
</li>
{{/each}}
</ul>
</div>

View File

@ -1,7 +1,13 @@
<form> <form>
<section class='field'> <section class='field'>
<label>{{i18n 'category.name'}}</label> <section class="field-item">
{{text-field value=name placeholderKey="category.name_placeholder" maxlength="50"}} <label>{{i18n 'category.name'}}</label>
{{text-field value=name placeholderKey="category.name_placeholder" maxlength="50"}}
</section>
<section class="field-item">
<label>{{i18n 'category.slug'}}</label>
{{text-field value=slug placeholderKey="category.slug_placeholder" maxlength="255"}}
</section>
</section> </section>
{{#if canSelectParentCategory}} {{#if canSelectParentCategory}}

View File

@ -16,11 +16,11 @@
{{#if editingTopic}} {{#if editingTopic}}
{{#if isPrivateMessage}} {{#if isPrivateMessage}}
<span class="private-message-glyph">{{fa-icon envelope}}</span> <span class="private-message-glyph">{{fa-icon envelope}}</span>
{{autofocus-text-field id='edit-title' value=newTitle maxLength=maxTitleLength}} {{autofocus-text-field id='edit-title' value=buffered.title maxLength=maxTitleLength}}
{{else}} {{else}}
{{autofocus-text-field id='edit-title' value=newTitle maxLength=maxTitleLength}} {{autofocus-text-field id='edit-title' value=buffered.title maxLength=maxTitleLength}}
</br> </br>
{{category-chooser valueAttribute="id" value=newCategoryId source=category_id}} {{category-chooser valueAttribute="id" value=buffered.category_id source=buffered.category_id}}
{{/if}} {{/if}}
<button class='btn btn-primary btn-small no-text' {{action "finishedEditingTopic"}}>{{fa-icon check}}</button> <button class='btn btn-primary btn-small no-text' {{action "finishedEditingTopic"}}>{{fa-icon check}}</button>
@ -34,11 +34,7 @@
{{#if details.loaded}} {{#if details.loaded}}
{{topic-status topic=model}} {{topic-status topic=model}}
<a href='{{unbound url}}' {{action "jumpTop"}}> <a href='{{unbound url}}' {{action "jumpTop"}}>
{{#if topicSaving}} {{{fancy_title}}}
{{fancy_title}}
{{else}}
{{{fancy_title}}}
{{/if}}
</a> </a>
{{/if}} {{/if}}

View File

@ -7,6 +7,7 @@ export default ComboboxView.extend({
overrideWidths: true, overrideWidths: true,
dataAttributes: ['id', 'description_text'], dataAttributes: ['id', 'description_text'],
valueBinding: Ember.Binding.oneWay('source'), valueBinding: Ember.Binding.oneWay('source'),
castInteger: true,
content: function() { content: function() {
var scopedCategoryId = this.get('scopedCategoryId'); var scopedCategoryId = this.get('scopedCategoryId');

View File

@ -63,7 +63,7 @@ export default Discourse.View.extend({
this.rerender(); this.rerender();
}.observes('content.@each'), }.observes('content.@each'),
didInsertElement: function() { _initializeCombo: function() {
var $elem = this.$(), var $elem = this.$(),
self = this; self = this;
@ -75,10 +75,15 @@ export default Discourse.View.extend({
$elem.select2({formatResult: this.template, minimumResultsForSearch: 5, width: 'resolve'}); $elem.select2({formatResult: this.template, minimumResultsForSearch: 5, width: 'resolve'});
var castInteger = this.get('castInteger');
$elem.on("change", function (e) { $elem.on("change", function (e) {
self.set('value', $(e.target).val()); var val = $(e.target).val();
if (val.length && castInteger) {
val = parseInt(val, 10);
}
self.set('value', val);
}); });
}, }.on('didInsertElement'),
willClearRender: function() { willClearRender: function() {
var elementId = "s2id_" + this.$().attr('id'); var elementId = "s2id_" + this.$().attr('id');

View File

@ -163,6 +163,39 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
this.trigger('previewRefreshed', $wmdPreview); this.trigger('previewRefreshed', $wmdPreview);
}, },
_applyEmojiAutocomplete: function() {
if (!this.siteSettings.enable_emoji) { return; }
var template = this.container.lookup('template:emoji-selector-autocomplete.raw');
$('#wmd-input').autocomplete({
template: template,
key: ":",
transformComplete: function(v){ return v.code + ":"; },
dataSource: function(term){
return new Ember.RSVP.Promise(function(resolve) {
var full = ":" + term;
term = term.toLowerCase();
if (term === "") {
return resolve(["smile", "smiley", "wink", "sunny", "blush"]);
}
if (Discourse.Emoji.translations[full]) {
return resolve([Discourse.Emoji.translations[full]]);
}
var options = Discourse.Emoji.search(term, {maxResults: 5});
return resolve(options);
}).then(function(list) {
return list.map(function(i) {
return {code: i, src: Discourse.Emoji.urlFor(i)};
});
});
}
});
},
initEditor: function() { initEditor: function() {
// not quite right, need a callback to pass in, meaning this gets called once, // not quite right, need a callback to pass in, meaning this gets called once,
// but if you start replying to another topic it will get the avatars wrong // but if you start replying to another topic it will get the avatars wrong
@ -172,6 +205,7 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
$LAB.script(assetPath('defer/html-sanitizer-bundle')); $LAB.script(assetPath('defer/html-sanitizer-bundle'));
ComposerView.trigger("initWmdEditor"); ComposerView.trigger("initWmdEditor");
this._applyEmojiAutocomplete();
var template = this.container.lookup('template:user-selector-autocomplete.raw'); var template = this.container.lookup('template:user-selector-autocomplete.raw');
$wmdInput.data('init', true); $wmdInput.data('init', true);

View File

@ -130,6 +130,10 @@
section.field { section.field {
margin-bottom: 20px; margin-bottom: 20px;
} }
section.field .field-item {
display: inline-block;
margin-right: 10px;
}
} }
.reply-where-modal { .reply-where-modal {

View File

@ -320,12 +320,15 @@ class Admin::UsersController < Admin::AdminController
user.email_tokens.update_all confirmed: true user.email_tokens.update_all confirmed: true
email_token = user.email_tokens.create(email: user.email) email_token = user.email_tokens.create(email: user.email)
Jobs.enqueue(:user_email,
unless params[:send_email] == '0' || params[:send_email] == 'false'
Jobs.enqueue( :user_email,
type: :account_created, type: :account_created,
user_id: user.id, user_id: user.id,
email_token: email_token.token) email_token: email_token.token)
end
render json: success_json render json: success_json.merge!(password_url: "#{Discourse.base_url}/users/password-reset/#{email_token.token}")
end end

View File

@ -95,6 +95,19 @@ class CategoriesController < ApplicationController
end end
end end
def update_slug
@category = Category.find(params[:category_id].to_i)
guardian.ensure_can_edit!(@category)
custom_slug = params[:slug].to_s
if custom_slug.present? && @category.update_attributes(slug: custom_slug)
render json: success_json
else
render_json_error(@category)
end
end
def set_notifications def set_notifications
category_id = params[:category_id].to_i category_id = params[:category_id].to_i
notification_level = params[:notification_level].to_i notification_level = params[:notification_level].to_i

View File

@ -15,7 +15,7 @@ class ExportCsvController < ApplicationController
def show def show
params.require(:id) params.require(:id)
filename = params.fetch(:id) filename = params.fetch(:id)
export_id = filename.split('_')[1].split('.')[0] export_id = filename.split('-')[2].split('.')[0]
export_initiated_by_user_id = 0 export_initiated_by_user_id = 0
export_initiated_by_user_id = UserExport.where(id: export_id)[0].user_id unless UserExport.where(id: export_id).empty? export_initiated_by_user_id = UserExport.where(id: export_id)[0].user_id unless UserExport.where(id: export_id).empty?
export_csv_path = UserExport.get_download_path(filename) export_csv_path = UserExport.get_download_path(filename)

View File

@ -2,6 +2,7 @@ require_dependency 'topic_view'
require_dependency 'promotion' require_dependency 'promotion'
require_dependency 'url_helper' require_dependency 'url_helper'
require_dependency 'topics_bulk_action' require_dependency 'topics_bulk_action'
require_dependency 'discourse_event'
class TopicsController < ApplicationController class TopicsController < ApplicationController
include UrlHelper include UrlHelper
@ -134,6 +135,8 @@ class TopicsController < ApplicationController
success = PostRevisor.new(first_post, topic).revise!(current_user, changes, validate_post: false) success = PostRevisor.new(first_post, topic).revise!(current_user, changes, validate_post: false)
end end
DiscourseEvent.trigger(:topic_saved, topic, params)
# this is used to return the title to the client as it may have been changed by "TextCleaner" # this is used to return the title to the client as it may have been changed by "TextCleaner"
success ? render_serialized(topic, BasicTopicSerializer) : render_json_error(topic) success ? render_serialized(topic, BasicTopicSerializer) : render_json_error(topic)
end end

View File

@ -24,6 +24,7 @@ module Jobs
def execute(args) def execute(args)
entity = args[:entity] entity = args[:entity]
@file_name = entity
if entity == "user_archive" if entity == "user_archive"
@entity_type = "user" @entity_type = "user"
@ -56,19 +57,25 @@ module Jobs
end end
end end
def user_export def user_list_export
query = ::AdminUserIndexQuery.new query = ::AdminUserIndexQuery.new
user_data = query.find_users_query.to_a user_data = query.find_users_query.to_a
user_data.map do |user| user_data.map do |user|
group_names = get_group_names(user).join(';') group_names = get_group_names(user).join(';')
user_array = get_user_fields(user) user_array = get_user_list_fields(user)
user_array.push(group_names) if group_names != '' user_array.push(group_names) if group_names != ''
user_array user_array
end end
end end
def staff_action_export def staff_action_export
staff_action_data = UserHistory.order('id DESC').to_a if @current_user.admin?
staff_action_data = UserHistory.only_staff_actions.order('id DESC').to_a
else
# moderator
staff_action_data = UserHistory.where(admin_only: false).only_staff_actions.order('id DESC').to_a
end
staff_action_data.map do |staff_action| staff_action_data.map do |staff_action|
get_staff_action_fields(staff_action) get_staff_action_fields(staff_action)
end end
@ -162,7 +169,7 @@ module Jobs
user_archive_array user_archive_array
end end
def get_user_fields(user) def get_user_list_fields(user)
user_array = [] user_array = []
HEADER_ATTRS_FOR['user'].each do |attr| HEADER_ATTRS_FOR['user'].each do |attr|
@ -265,7 +272,8 @@ module Jobs
def set_file_path def set_file_path
@file = UserExport.create(export_type: @entity_type, user_id: @current_user.id) @file = UserExport.create(export_type: @entity_type, user_id: @current_user.id)
@file_name = "export_#{@file.id}.csv" file_name_prefix = @file_name.split('_').join('-')
@file_name = "#{file_name_prefix}-#{@file.id}.csv"
# ensure directory exists # ensure directory exists
dir = File.dirname("#{UserExport.base_directory}/#{@file_name}") dir = File.dirname("#{UserExport.base_directory}/#{@file_name}")

View File

@ -201,18 +201,19 @@ SQL
end end
def ensure_slug def ensure_slug
if name.present? return unless name.present?
self.name.strip!
if slug.present? self.name.strip!
# custom slug
errors.add(:slug, "is already in use") if duplicate_slug? if slug.present?
else # santized custom slug
# auto slug self.slug = Slug.for(slug)
self.slug = Slug.for(name) errors.add(:slug, 'is already in use') if duplicate_slug?
return if self.slug.blank? else
self.slug = '' if duplicate_slug? # auto slug
end self.slug = Slug.for(name)
return if self.slug.blank?
self.slug = '' if duplicate_slug?
end end
end end

View File

@ -20,7 +20,7 @@ class UserHistory < ActiveRecord::Base
:change_site_setting, :change_site_setting,
:change_site_customization, :change_site_customization,
:delete_site_customization, :delete_site_customization,
:checked_for_custom_avatar, :checked_for_custom_avatar, # not used anymore
:notified_about_avatar, :notified_about_avatar,
:notified_about_sequential_replies, :notified_about_sequential_replies,
:notified_about_dominating_topic, :notified_about_dominating_topic,

View File

@ -1,14 +1,13 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="author" content=""> <meta name="author" content="">
<meta name="generator" content="Discourse <%= Discourse::VERSION::STRING %> - https://github.com/discourse/discourse version <%= Discourse.git_version %>"> <meta name="generator" content="Discourse <%= Discourse::VERSION::STRING %> - https://github.com/discourse/discourse version <%= Discourse.git_version %>">
<link rel="icon" type="image/png" href="<%=SiteSetting.favicon_url%>"> <link rel="icon" type="image/png" href="<%=SiteSetting.favicon_url%>">
<link rel="apple-touch-icon" type="image/png" href="<%=SiteSetting.apple_touch_icon_url%>"> <link rel="apple-touch-icon" type="image/png" href="<%=SiteSetting.apple_touch_icon_url%>">
<%= canonical_link_tag %> <%= canonical_link_tag %>
<%= render partial: "common/special_font_face" %> <%= render partial: "common/special_font_face" %>
<%= render partial: "common/discourse_stylesheet" %> <%= render partial: "common/discourse_stylesheet" %>
<%= discourse_csrf_tags %> <%= discourse_csrf_tags %>

View File

@ -7,11 +7,11 @@
<%= render partial: "layouts/head" %> <%= render partial: "layouts/head" %>
<%- if SiteSetting.enable_escaped_fragments? %> <%- if SiteSetting.enable_escaped_fragments? %>
<meta name="fragment" content="!"> <meta name="fragment" content="!">
<%- end %> <%- end %>
<%- if shared_session_key %> <%- if shared_session_key %>
<meta name="shared_session_key" content="<%= shared_session_key %>"> <meta name="shared_session_key" content="<%= shared_session_key %>">
<%- end %> <%- end %>
<%= script "preload_store" %> <%= script "preload_store" %>

View File

@ -20,13 +20,13 @@
<% if @rss %> <% if @rss %>
<% content_for :head do %> <% content_for :head do %>
<%= auto_discovery_link_tag(:rss, {action: "#{@rss}_feed"}, title: I18n.t("rss_description.#{@rss}")) %> <%= auto_discovery_link_tag(:rss, { action: "#{@rss}_feed" }, title: I18n.t("rss_description.#{@rss}")) %>
<% end %> <% end %>
<% end %> <% end %>
<% if @category %> <% if @category %>
<% content_for :head do %> <% content_for :head do %>
<%= auto_discovery_link_tag(@category, {action: :category_feed, format: :rss}, title: t('rss_topics_in_category', category: @category.name), type: 'application/rss+xml') %> <%= auto_discovery_link_tag(:rss, { action: :category_feed }, title: t('rss_topics_in_category', category: @category.name)) %>
<% end %> <% end %>
<% end %> <% end %>

View File

@ -1281,6 +1281,8 @@ en:
delete: 'Delete Category' delete: 'Delete Category'
create: 'New Category' create: 'New Category'
save: 'Save Category' save: 'Save Category'
slug: 'Category Slug'
slug_placeholder: '(Optional) dashed-words for url'
creation_error: There has been an error during the creation of the category. creation_error: There has been an error during the creation of the category.
save_error: There was an error saving the category. save_error: There was an error saving the category.
name: "Category Name" name: "Category Name"

View File

@ -598,14 +598,14 @@ en:
s3_backup_config_warning: 'The server is configured to upload backups to s3, but at least one the following setting is not set: s3_access_key_id, s3_secret_access_key or s3_backup_bucket. Go to <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="http://meta.discourse.org/t/how-to-set-up-image-uploads-to-s3/7229" target="_blank">See "How to set up image uploads to S3?" to learn more</a>.' s3_backup_config_warning: 'The server is configured to upload backups to s3, but at least one the following setting is not set: s3_access_key_id, s3_secret_access_key or s3_backup_bucket. Go to <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="http://meta.discourse.org/t/how-to-set-up-image-uploads-to-s3/7229" target="_blank">See "How to set up image uploads to S3?" to learn more</a>.'
image_magick_warning: 'The server is configured to create thumbnails of large images, but ImageMagick is not installed. Install ImageMagick using your favorite package manager or <a href="http://www.imagemagick.org/script/binary-releases.php" target="_blank">download the latest release</a>.' image_magick_warning: 'The server is configured to create thumbnails of large images, but ImageMagick is not installed. Install ImageMagick using your favorite package manager or <a href="http://www.imagemagick.org/script/binary-releases.php" target="_blank">download the latest release</a>.'
failing_emails_warning: 'There are %{num_failed_jobs} email jobs that failed. Check your config/discourse.conf file and ensure that the mail server settings are correct. <a href="/sidekiq/retries" target="_blank">See the failed jobs in Sidekiq</a>.' failing_emails_warning: 'There are %{num_failed_jobs} email jobs that failed. Check your config/discourse.conf file and ensure that the mail server settings are correct. <a href="/sidekiq/retries" target="_blank">See the failed jobs in Sidekiq</a>.'
default_logo_warning: "You haven't customized the logo images for your site. Update logo_url, logo_small_url, and favicon_url in the <a href='/admin/site_settings'>Site Settings</a>." default_logo_warning: "Set the graphic logos for your site. Update logo_url, logo_small_url, and favicon_url in <a href='/admin/site_settings'>Site Settings</a>."
contact_email_missing: "You haven't provided a contact email for your site. Please update contact_email in the <a href='/admin/site_settings'>Site Settings</a>." contact_email_missing: "Enter a site contact email address so new users or users who can't log in, as well as other webmasters and system administrators, can reach you for urgent matters. Update it in <a href='/admin/site_settings'>Site Settings</a>."
contact_email_invalid: "The site contact email is invalid. Please update contact_email in the <a href='/admin/site_settings'>Site Settings</a>." contact_email_invalid: "The site contact email is invalid. Update it in <a href='/admin/site_settings'>Site Settings</a>."
title_nag: "The title Site Setting is still set to the default value. Please update it with your site's title in the <a href='/admin/site_settings'>Site Settings</a>." title_nag: "Enter the name of your site. Update title in <a href='/admin/site_settings'>Site Settings</a>."
site_description_missing: "The site_description setting is blank. Write a brief description of this forum in the <a href='/admin/site_settings'>Site Settings</a>." site_description_missing: "Enter a one sentence description of your site that will appear in search results. Update site_description in <a href='/admin/site_settings'>Site Settings</a>."
consumer_email_warning: "Your site is configured to use Gmail (or another consumer email service) to send email. <a href='http://support.google.com/a/bin/answer.py?hl=en&answer=166852' target='_blank'>Gmail limits how many emails you can send</a>. Consider using an email service provider like mandrill.com to ensure email deliverability." consumer_email_warning: "Your site is configured to use Gmail (or another consumer email service) to send email. <a href='http://support.google.com/a/bin/answer.py?hl=en&answer=166852' target='_blank'>Gmail limits how many emails you can send</a>. Consider using an email service provider like mandrill.com to ensure email deliverability."
access_password_removal: "Your site was using the access_password setting, which has been removed. The login_required and must_approve_users settings have been enabled, which should be used instead. You can change them in the <a href='/admin/site_settings'>Site Settings</a>. Be sure to <a href='/admin/users/list/pending'>approve users in the Pending Users list</a>. (This message will go away after 2 days.)" access_password_removal: "Your site was using the access_password setting, which has been removed. The login_required and must_approve_users settings have been enabled, which should be used instead. You can change them in the <a href='/admin/site_settings'>Site Settings</a>. Be sure to <a href='/admin/users/list/pending'>approve users in the Pending Users list</a>. (This message will go away after 2 days.)"
site_contact_username_warning: "The site_contact_username setting is blank. Please update it in the <a href='/admin/site_settings'>Site Settings</a>. Set it to the username of an admin user who should be the sender of system messages." site_contact_username_warning: "Enter the name of a friendly staff user account to send important automated private messages from, such as the new user welcome, flag warnings, etc. Update site_contact_username in <a href='/admin/site_settings'>Site Settings</a>."
notification_email_warning: "The notification_email setting is blank. Please update it in the <a href='/admin/site_settings'>Site Settings</a>." notification_email_warning: "The notification_email setting is blank. Please update it in the <a href='/admin/site_settings'>Site Settings</a>."
content_types: content_types:
@ -657,9 +657,9 @@ en:
allow_duplicate_topic_titles: "Allow topics with identical, duplicate titles." allow_duplicate_topic_titles: "Allow topics with identical, duplicate titles."
unique_posts_mins: "How many minutes before a user can make a post with the same content again" unique_posts_mins: "How many minutes before a user can make a post with the same content again"
educate_until_posts: "When the user starts typing their first (n) new posts, show the pop-up new user education panel in the composer." educate_until_posts: "When the user starts typing their first (n) new posts, show the pop-up new user education panel in the composer."
title: "Brief title of this site, used in the title tag." title: "The name of this site, as used in the title tag."
site_description: "Describe this site in one sentence, used in the meta description tag." site_description: "Describe this site in one sentence, as used in the meta description tag."
contact_email: "Email address of key contact for site. Important notices from discourse.org regarding critical updates may be sent to this address." contact_email: "Email address of key contact responsible for this site. Used for critical notifications only, as well as on the /about contact form for urgent matters."
queue_jobs: "DEVELOPER ONLY! WARNING! By default, queue jobs in sidekiq. If disabled, your site will be broken." queue_jobs: "DEVELOPER ONLY! WARNING! By default, queue jobs in sidekiq. If disabled, your site will be broken."
crawl_images: "Retrieve images from remote URLs to insert the correct width and height dimensions." crawl_images: "Retrieve images from remote URLs to insert the correct width and height dimensions."
download_remote_images_to_local: "Convert remote images to local images by downloading them; this prevents broken images." download_remote_images_to_local: "Convert remote images to local images by downloading them; this prevents broken images."
@ -737,7 +737,7 @@ en:
post_menu_hidden_items: "The menu items to hide by default in the post menu unless an expansion ellipsis is clicked on." post_menu_hidden_items: "The menu items to hide by default in the post menu unless an expansion ellipsis is clicked on."
share_links: "Determine which items appear on the share dialog, and in what order." share_links: "Determine which items appear on the share dialog, and in what order."
track_external_right_clicks: "Track external links that are right clicked (eg: open in new tab) disabled by default because it rewrites URLs" track_external_right_clicks: "Track external links that are right clicked (eg: open in new tab) disabled by default because it rewrites URLs"
site_contact_username: "All automated private messages will be from this user; if left blank the default System account will be used." site_contact_username: "A valid staff username to send all automated private messages from. If left blank the default System account will be used."
send_welcome_message: "Send all new users a welcome private message with a quick start guide." send_welcome_message: "Send all new users a welcome private message with a quick start guide."
suppress_reply_directly_below: "Don't show the expandable reply count on a post when there is only a single reply directly below this post." suppress_reply_directly_below: "Don't show the expandable reply count on a post when there is only a single reply directly below this post."
suppress_reply_directly_above: "Don't show the expandable in-reply-to on a post when there is only a single reply directly above this post." suppress_reply_directly_above: "Don't show the expandable in-reply-to on a post when there is only a single reply directly above this post."

View File

@ -325,6 +325,7 @@ Discourse::Application.routes.draw do
post "category/uploads" => "categories#upload" post "category/uploads" => "categories#upload"
post "category/:category_id/move" => "categories#move" post "category/:category_id/move" => "categories#move"
post "category/:category_id/notifications" => "categories#set_notifications" post "category/:category_id/notifications" => "categories#set_notifications"
put "category/:category_id/slug" => "categories#update_slug"
get "c/:id/show" => "categories#show" get "c/:id/show" => "categories#show"
get "c/:category.rss" => "list#category_feed", format: :rss get "c/:category.rss" => "list#category_feed", format: :rss

View File

@ -0,0 +1,10 @@
class CleanUpUserHistory < ActiveRecord::Migration
def up
# 'checked_for_custom_avatar' is not used anymore
# was removed in https://github.com/discourse/discourse/commit/6c1c8be79433f87bef9d768da7b8fa4ec9bb18d7
UserHistory.where(action: UserHistory.actions[:checked_for_custom_avatar]).delete_all
end
def down
end
end

View File

@ -26,9 +26,11 @@ class ComposerMessagesFinder
if count < SiteSetting.educate_until_posts if count < SiteSetting.educate_until_posts
education_posts_text = I18n.t('education.until_posts', count: SiteSetting.educate_until_posts) education_posts_text = I18n.t('education.until_posts', count: SiteSetting.educate_until_posts)
return {templateName: 'composer/education', return {
wait_for_typing: true, templateName: 'composer/education',
body: PrettyText.cook(SiteText.text_for(education_key, education_posts_text: education_posts_text)) } wait_for_typing: true,
body: PrettyText.cook(SiteText.text_for(education_key, education_posts_text: education_posts_text))
}
end end
nil nil
@ -37,7 +39,11 @@ class ComposerMessagesFinder
# New users have a limited number of replies in a topic # New users have a limited number of replies in a topic
def check_new_user_many_replies def check_new_user_many_replies
return unless replying? && @user.posted_too_much_in_topic?(@details[:topic_id]) return unless replying? && @user.posted_too_much_in_topic?(@details[:topic_id])
{templateName: 'composer/education', body: PrettyText.cook(I18n.t('education.too_many_replies', newuser_max_replies_per_topic: SiteSetting.newuser_max_replies_per_topic)) }
{
templateName: 'composer/education',
body: PrettyText.cook(I18n.t('education.too_many_replies', newuser_max_replies_per_topic: SiteSetting.newuser_max_replies_per_topic))
}
end end
# Should a user be contacted to update their avatar? # Should a user be contacted to update their avatar?
@ -49,14 +55,14 @@ class ComposerMessagesFinder
# We don't notify users who have avatars or who have been notified already. # We don't notify users who have avatars or who have been notified already.
return if @user.uploaded_avatar_id || UserHistory.exists_for_user?(@user, :notified_about_avatar) return if @user.uploaded_avatar_id || UserHistory.exists_for_user?(@user, :notified_about_avatar)
# Finally, we don't check users whose avatars haven't been examined
return unless UserHistory.exists_for_user?(@user, :checked_for_custom_avatar)
# If we got this far, log that we've nagged them about the avatar # If we got this far, log that we've nagged them about the avatar
UserHistory.create!(action: UserHistory.actions[:notified_about_avatar], target_user_id: @user.id ) UserHistory.create!(action: UserHistory.actions[:notified_about_avatar], target_user_id: @user.id )
# Return the message # Return the message
{templateName: 'composer/education', body: PrettyText.cook(I18n.t('education.avatar', profile_path: "/users/#{@user.username_lower}")) } {
templateName: 'composer/education',
body: PrettyText.cook(I18n.t('education.avatar', profile_path: "/users/#{@user.username_lower}"))
}
end end
# Is a user replying too much in succession? # Is a user replying too much in succession?
@ -87,10 +93,12 @@ class ComposerMessagesFinder
target_user_id: @user.id, target_user_id: @user.id,
topic_id: @details[:topic_id] ) topic_id: @details[:topic_id] )
{templateName: 'composer/education', {
wait_for_typing: true, templateName: 'composer/education',
extraClass: 'urgent', wait_for_typing: true,
body: PrettyText.cook(I18n.t('education.sequential_replies')) } extraClass: 'urgent',
body: PrettyText.cook(I18n.t('education.sequential_replies'))
}
end end
def check_dominating_topic def check_dominating_topic
@ -102,6 +110,7 @@ class ComposerMessagesFinder
!UserHistory.exists_for_user?(@user, :notified_about_dominating_topic, topic_id: @details[:topic_id]) !UserHistory.exists_for_user?(@user, :notified_about_dominating_topic, topic_id: @details[:topic_id])
topic = Topic.find_by(id: @details[:topic_id]) topic = Topic.find_by(id: @details[:topic_id])
return if topic.blank? || return if topic.blank? ||
topic.user_id == @user.id || topic.user_id == @user.id ||
topic.posts_count < SiteSetting.summary_posts_required || topic.posts_count < SiteSetting.summary_posts_required ||
@ -117,11 +126,12 @@ class ComposerMessagesFinder
target_user_id: @user.id, target_user_id: @user.id,
topic_id: @details[:topic_id]) topic_id: @details[:topic_id])
{
{templateName: 'composer/education', templateName: 'composer/education',
wait_for_typing: true, wait_for_typing: true,
extraClass: 'urgent', extraClass: 'urgent',
body: PrettyText.cook(I18n.t('education.dominating_topic', percent: (ratio * 100).round)) } body: PrettyText.cook(I18n.t('education.dominating_topic', percent: (ratio * 100).round))
}
end end
def check_reviving_old_topic def check_reviving_old_topic
@ -136,20 +146,22 @@ class ComposerMessagesFinder
topic.last_posted_at.nil? || topic.last_posted_at.nil? ||
topic.last_posted_at > SiteSetting.warn_reviving_old_topic_age.days.ago topic.last_posted_at > SiteSetting.warn_reviving_old_topic_age.days.ago
{templateName: 'composer/education', {
wait_for_typing: false, templateName: 'composer/education',
extraClass: 'urgent', wait_for_typing: false,
body: PrettyText.cook(I18n.t('education.reviving_old_topic', days: (Time.zone.now - topic.last_posted_at).round / 1.day)) } extraClass: 'urgent',
body: PrettyText.cook(I18n.t('education.reviving_old_topic', days: (Time.zone.now - topic.last_posted_at).round / 1.day))
}
end end
private private
def creating_topic? def creating_topic?
return @details[:composerAction] == "createTopic" @details[:composerAction] == "createTopic"
end end
def replying? def replying?
return @details[:composerAction] == "reply" @details[:composerAction] == "reply"
end end
end end

View File

@ -83,44 +83,31 @@ describe ComposerMessagesFinder do
let(:finder) { ComposerMessagesFinder.new(user, composerAction: 'createTopic') } let(:finder) { ComposerMessagesFinder.new(user, composerAction: 'createTopic') }
let(:user) { Fabricate(:user) } let(:user) { Fabricate(:user) }
context "a user who we haven't checked for an avatar yet" do context "success" do
it "returns no avatar message" do let!(:message) { finder.check_avatar_notification }
finder.check_avatar_notification.should be_blank
it "returns an avatar upgrade message" do
message.should be_present
end
it "creates a notified_about_avatar log" do
UserHistory.exists_for_user?(user, :notified_about_avatar).should == true
end end
end end
context "a user who has been checked for a custom avatar" do it "doesn't return notifications for new users" do
before do user.trust_level = TrustLevel[0]
UserHistory.create!(action: UserHistory.actions[:checked_for_custom_avatar], target_user_id: user.id ) finder.check_avatar_notification.should be_blank
end end
context "success" do it "doesn't return notifications for users who have custom avatars" do
let!(:message) { finder.check_avatar_notification } user.uploaded_avatar_id = 1
finder.check_avatar_notification.should be_blank
it "returns an avatar upgrade message" do end
message.should be_present
end
it "creates a notified_about_avatar log" do
UserHistory.exists_for_user?(user, :notified_about_avatar).should == true
end
end
it "doesn't return notifications for new users" do
user.trust_level = TrustLevel[0]
finder.check_avatar_notification.should be_blank
end
it "doesn't return notifications for users who have custom avatars" do
user.uploaded_avatar_id = 1
finder.check_avatar_notification.should be_blank
end
it "doesn't notify users who have been notified already" do
UserHistory.create!(action: UserHistory.actions[:notified_about_avatar], target_user_id: user.id )
finder.check_avatar_notification.should be_blank
end
it "doesn't notify users who have been notified already" do
UserHistory.create!(action: UserHistory.actions[:notified_about_avatar], target_user_id: user.id )
finder.check_avatar_notification.should be_blank
end end
end end

View File

@ -111,10 +111,13 @@ describe HasCustomFields do
db_item = CustomFieldsTestItem.find(test_item.id) db_item = CustomFieldsTestItem.find(test_item.id)
db_item.custom_fields.should == {"a" => ["b", "c", "d"]} db_item.custom_fields.should == {"a" => ["b", "c", "d"]}
db_item.custom_fields["a"] = ["c", "d"] db_item.custom_fields.update('a' => ['c', 'd'])
db_item.save db_item.save
db_item.custom_fields.should == {"a" => ["c", "d"]} db_item.custom_fields.should == {"a" => ["c", "d"]}
db_item.custom_fields.delete('a')
db_item.custom_fields.should == {}
end end
it "casts integers in arrays properly without error" do it "casts integers in arrays properly without error" do

View File

@ -460,6 +460,7 @@ describe Admin::UsersController do
context ".invite_admin" do context ".invite_admin" do
it 'should invite admin' do it 'should invite admin' do
Jobs.expects(:enqueue).with(:user_email, anything).returns(true)
xhr :post, :invite_admin, name: 'Bill', username: 'bill22', email: 'bill@bill.com' xhr :post, :invite_admin, name: 'Bill', username: 'bill22', email: 'bill@bill.com'
response.should be_success response.should be_success
@ -468,6 +469,14 @@ describe Admin::UsersController do
u.username.should == "bill22" u.username.should == "bill22"
u.admin.should == true u.admin.should == true
end end
it "doesn't send the email with send_email falsy" do
Jobs.expects(:enqueue).with(:user_email, anything).never
xhr :post, :invite_admin, name: 'Bill', username: 'bill22', email: 'bill@bill.com', send_email: '0'
response.should be_success
json = ::JSON.parse(response.body)
json["password_url"].should be_present
end
end end
end end

View File

@ -203,4 +203,42 @@ describe CategoriesController do
end end
describe 'update_slug' do
it 'requires the user to be logged in' do
lambda { xhr :put, :update_slug, category_id: 'category'}.should raise_error(Discourse::NotLoggedIn)
end
describe 'logged in' do
let(:valid_attrs) { {id: @category.id, slug: 'fff'} }
before do
@user = log_in(:admin)
@category = Fabricate(:happy_category, user: @user)
end
it 'rejects blank' do
xhr :put, :update_slug, category_id: @category.id, slug: nil
response.status.should == 422
end
it 'accepts valid custom slug' do
xhr :put, :update_slug, category_id: @category.id, slug: 'valid-slug'
response.should be_success
category = Category.find(@category.id)
category.slug.should == 'valid-slug'
end
it 'accepts not well formed custom slug' do
xhr :put, :update_slug, category_id: @category.id, slug: ' valid slug'
response.should be_success
category = Category.find(@category.id)
category.slug.should == 'valid-slug'
end
it 'rejects invalid custom slug' do
xhr :put, :update_slug, category_id: @category.id, slug: ' '
response.status.should == 422
end
end
end
end end

View File

@ -1,7 +1,7 @@
require "spec_helper" require "spec_helper"
describe ExportCsvController do describe ExportCsvController do
let(:export_filename) { "export_999.csv" } let(:export_filename) { "user-archive-999.csv" }
context "while logged in as normal user" do context "while logged in as normal user" do
@ -30,7 +30,7 @@ describe ExportCsvController do
describe ".download" do describe ".download" do
it "uses send_file to transmit the export file" do it "uses send_file to transmit the export file" do
file = UserExport.create(export_type: "user", user_id: @user.id) file = UserExport.create(export_type: "user", user_id: @user.id)
file_name = "export_#{file.id}.csv" file_name = "user-archive-#{file.id}.csv"
controller.stubs(:render) controller.stubs(:render)
export = UserExport.new() export = UserExport.new()
UserExport.expects(:get_download_path).with(file_name).returns(export) UserExport.expects(:get_download_path).with(file_name).returns(export)
@ -74,7 +74,7 @@ describe ExportCsvController do
describe ".download" do describe ".download" do
it "uses send_file to transmit the export file" do it "uses send_file to transmit the export file" do
file = UserExport.create(export_type: "admin", user_id: @admin.id) file = UserExport.create(export_type: "admin", user_id: @admin.id)
file_name = "export_#{file.id}.csv" file_name = "screened-email-#{file.id}.csv"
controller.stubs(:render) controller.stubs(:render)
export = UserExport.new() export = UserExport.new()
UserExport.expects(:get_download_path).with(file_name).returns(export) UserExport.expects(:get_download_path).with(file_name).returns(export)

View File

@ -7,3 +7,9 @@ Fabricator(:diff_category, from: :category) do
name "Different Category" name "Different Category"
user user
end end
Fabricator(:happy_category, from: :category) do
name 'Happy Category'
slug 'happy'
user
end

View File

@ -8,16 +8,16 @@ describe Jobs::ExportCsvFile do
end end
end end
let :user_header do let :user_list_header do
Jobs::ExportCsvFile.new.get_header('user') Jobs::ExportCsvFile.new.get_header('user')
end end
let :user_export do let :user_list_export do
Jobs::ExportCsvFile.new.user_export Jobs::ExportCsvFile.new.user_list_export
end end
def to_hash(row) def to_hash(row)
Hash[*user_header.zip(row).flatten] Hash[*user_list_header.zip(row).flatten]
end end
it 'exports sso data' do it 'exports sso data' do
@ -25,10 +25,9 @@ describe Jobs::ExportCsvFile do
user = Fabricate(:user) user = Fabricate(:user)
user.create_single_sign_on_record(external_id: "123", last_payload: "xxx", external_email: 'test@test.com') user.create_single_sign_on_record(external_id: "123", last_payload: "xxx", external_email: 'test@test.com')
user = to_hash(user_export.find{|u| u[0] == user.id}) user = to_hash(user_list_export.find{|u| u[0] == user.id})
user["external_id"].should == "123" user["external_id"].should == "123"
user["external_email"].should == "test@test.com" user["external_email"].should == "test@test.com"
end end
end end

View File

@ -198,6 +198,11 @@ describe Category do
c.slug.should eq("cats-category") c.slug.should eq("cats-category")
end end
it 'and be sanitized' do
c = Fabricate(:category, name: 'Cats', slug: ' invalid slug')
c.slug.should == 'invalid-slug'
end
it 'fails if custom slug is duplicate with existing' do it 'fails if custom slug is duplicate with existing' do
c1 = Fabricate(:category, name: "Cats", slug: "cats") c1 = Fabricate(:category, name: "Cats", slug: "cats")
c2 = Fabricate.build(:category, name: "More Cats", slug: "cats") c2 = Fabricate.build(:category, name: "More Cats", slug: "cats")

View File

@ -28,8 +28,8 @@ test("editingMode", function() {
topicController.set('model.details.can_edit', true); topicController.set('model.details.can_edit', true);
topicController.send('editTopic'); topicController.send('editTopic');
ok(topicController.get('editingTopic'), "calling editTopic enables editing if the user can edit"); ok(topicController.get('editingTopic'), "calling editTopic enables editing if the user can edit");
equal(topicController.get('newTitle'), topic.get('title')); equal(topicController.get('buffered.title'), topic.get('title'));
equal(topicController.get('newCategoryId'), topic.get('category_id')); equal(topicController.get('buffered.category_id'), topic.get('category_id'));
topicController.send('cancelEditingTopic'); topicController.send('cancelEditingTopic');
ok(!topicController.get('editingTopic'), "cancelling edit mode reverts the property value"); ok(!topicController.get('editingTopic'), "cancelling edit mode reverts the property value");