mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-23 23:58:31 -05:00
Merge branch 'master' of github.com:discourse/discourse
This commit is contained in:
commit
9aba6ab265
43 changed files with 344 additions and 256 deletions
|
@ -1,8 +1,19 @@
|
|||
export default Ember.Component.extend({
|
||||
classNameBindings: ['containerClass'],
|
||||
layoutName: 'components/conditional-loading-spinner',
|
||||
|
||||
containerClass: function() {
|
||||
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')
|
||||
});
|
||||
|
|
|
@ -146,9 +146,9 @@ export default ObjectController.extend(ModalFunctionality, {
|
|||
|
||||
}).catch(function(error) {
|
||||
if (error && error.responseText) {
|
||||
self.flash($.parseJSON(error.responseText).errors[0]);
|
||||
self.flash($.parseJSON(error.responseText).errors[0], 'error');
|
||||
} else {
|
||||
self.flash(I18n.t('generic_error'));
|
||||
self.flash(I18n.t('generic_error'), 'error');
|
||||
}
|
||||
self.set('saving', false);
|
||||
});
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import ObjectController from 'discourse/controllers/object';
|
||||
import BufferedContent from 'discourse/mixins/buffered-content';
|
||||
import { spinnerHTML } from 'discourse/helpers/loading-spinner';
|
||||
|
||||
export default ObjectController.extend(Discourse.SelectedPostsCount, {
|
||||
export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedContent, {
|
||||
multiSelect: false,
|
||||
needs: ['header', 'modal', 'composer', 'quote-button', 'search', 'topic-progress', 'application'],
|
||||
allPostsSelected: false,
|
||||
|
@ -235,11 +236,6 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, {
|
|||
this.set('allPostsSelected', false);
|
||||
},
|
||||
|
||||
/**
|
||||
Toggle a participant for filtering
|
||||
|
||||
@method toggleParticipant
|
||||
**/
|
||||
toggleParticipant: function(user) {
|
||||
this.get('postStream').toggleParticipant(Em.get(user, 'username'));
|
||||
},
|
||||
|
@ -247,17 +243,13 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, {
|
|||
editTopic: function() {
|
||||
if (!this.get('details.can_edit')) return false;
|
||||
|
||||
this.setProperties({
|
||||
editingTopic: true,
|
||||
newTitle: this.get('title'),
|
||||
newCategoryId: this.get('category_id')
|
||||
});
|
||||
this.set('editingTopic', true);
|
||||
return false;
|
||||
},
|
||||
|
||||
// close editing mode
|
||||
cancelEditingTopic: function() {
|
||||
this.set('editingTopic', false);
|
||||
this.rollbackBuffer();
|
||||
},
|
||||
|
||||
toggleMultiSelect: function() {
|
||||
|
@ -265,39 +257,24 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, {
|
|||
},
|
||||
|
||||
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.
|
||||
this.set('topicSaving', true);
|
||||
|
||||
// manually update the titles & category
|
||||
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
|
||||
Discourse.Topic.update(this.get('model'), props).then(function() {
|
||||
// Note we roll back on success here because `update` saves
|
||||
// the properties to the topic.
|
||||
self.rollbackBuffer();
|
||||
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) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
Handlebars.registerHelper('link-domain', function(property, options) {
|
||||
var link = Em.get(this, property, options);
|
||||
import registerUnbound from 'discourse/helpers/register-unbound';
|
||||
|
||||
registerUnbound('link-domain', function(link) {
|
||||
if (link) {
|
||||
var internal = Em.get(link, 'internal'),
|
||||
hasTitle = (!Em.isEmpty(Em.get(link, 'title')));
|
||||
|
|
|
@ -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)};
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
|
@ -58,6 +58,7 @@ Discourse.Category = Discourse.Model.extend({
|
|||
return Discourse.ajax(url, {
|
||||
data: {
|
||||
name: this.get('name'),
|
||||
slug: this.get('slug'),
|
||||
color: this.get('color'),
|
||||
text_color: this.get('text_color'),
|
||||
secure: this.get('secure'),
|
||||
|
|
|
@ -463,13 +463,10 @@ Discourse.Composer = Discourse.Model.extend({
|
|||
|
||||
// Update the title if we've changed it
|
||||
if (this.get('title') && post.get('post_number') === 1) {
|
||||
var topic = this.get('topic');
|
||||
topic.setProperties({
|
||||
Discourse.Topic.update(this.get('topic'), {
|
||||
title: this.get('title'),
|
||||
fancy_title: Handlebars.Utils.escapeExpression(this.get('title')),
|
||||
category_id: parseInt(this.get('categoryId'), 10)
|
||||
category_id: this.get('categoryId')
|
||||
});
|
||||
topic.save();
|
||||
}
|
||||
|
||||
post.setProperties({
|
||||
|
|
|
@ -30,7 +30,7 @@ Discourse.ExportCsv.reopenClass({
|
|||
@method export_user_list
|
||||
**/
|
||||
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'}});
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,12 +1,4 @@
|
|||
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 = Ember.Object.extend(Discourse.Presence);
|
||||
|
||||
Discourse.Model.reopenClass({
|
||||
extractByKey: function(collection, klass) {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -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() {
|
||||
var result = this._super.apply(this, arguments);
|
||||
this.createActionSummary(result);
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
{{#if condition}}
|
||||
<div {{bind-attr class=":spinner size"}}></div>
|
||||
{{else}}
|
||||
{{yield}}
|
||||
{{/if}}
|
|
@ -82,7 +82,7 @@
|
|||
|
||||
{{#if showAllLinksControls}}
|
||||
<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>
|
||||
{{/if}}
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<div class='autocomplete'>
|
||||
<ul>
|
||||
{{#each options}}
|
||||
<li>
|
||||
<a href><img src='{{src}}' class='emoji'> {{code}}</a>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
|
@ -1,7 +1,13 @@
|
|||
<form>
|
||||
<section class='field'>
|
||||
<label>{{i18n 'category.name'}}</label>
|
||||
{{text-field value=name placeholderKey="category.name_placeholder" maxlength="50"}}
|
||||
<section class="field-item">
|
||||
<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>
|
||||
|
||||
{{#if canSelectParentCategory}}
|
||||
|
|
|
@ -16,11 +16,11 @@
|
|||
{{#if editingTopic}}
|
||||
{{#if isPrivateMessage}}
|
||||
<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}}
|
||||
{{autofocus-text-field id='edit-title' value=newTitle maxLength=maxTitleLength}}
|
||||
{{autofocus-text-field id='edit-title' value=buffered.title maxLength=maxTitleLength}}
|
||||
</br>
|
||||
{{category-chooser valueAttribute="id" value=newCategoryId source=category_id}}
|
||||
{{category-chooser valueAttribute="id" value=buffered.category_id source=buffered.category_id}}
|
||||
{{/if}}
|
||||
|
||||
<button class='btn btn-primary btn-small no-text' {{action "finishedEditingTopic"}}>{{fa-icon check}}</button>
|
||||
|
@ -34,11 +34,7 @@
|
|||
{{#if details.loaded}}
|
||||
{{topic-status topic=model}}
|
||||
<a href='{{unbound url}}' {{action "jumpTop"}}>
|
||||
{{#if topicSaving}}
|
||||
{{fancy_title}}
|
||||
{{else}}
|
||||
{{{fancy_title}}}
|
||||
{{/if}}
|
||||
{{{fancy_title}}}
|
||||
</a>
|
||||
{{/if}}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ export default ComboboxView.extend({
|
|||
overrideWidths: true,
|
||||
dataAttributes: ['id', 'description_text'],
|
||||
valueBinding: Ember.Binding.oneWay('source'),
|
||||
castInteger: true,
|
||||
|
||||
content: function() {
|
||||
var scopedCategoryId = this.get('scopedCategoryId');
|
||||
|
|
|
@ -63,7 +63,7 @@ export default Discourse.View.extend({
|
|||
this.rerender();
|
||||
}.observes('content.@each'),
|
||||
|
||||
didInsertElement: function() {
|
||||
_initializeCombo: function() {
|
||||
var $elem = this.$(),
|
||||
self = this;
|
||||
|
||||
|
@ -75,10 +75,15 @@ export default Discourse.View.extend({
|
|||
|
||||
$elem.select2({formatResult: this.template, minimumResultsForSearch: 5, width: 'resolve'});
|
||||
|
||||
var castInteger = this.get('castInteger');
|
||||
$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() {
|
||||
var elementId = "s2id_" + this.$().attr('id');
|
||||
|
|
|
@ -163,6 +163,39 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
|
|||
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() {
|
||||
// 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
|
||||
|
@ -172,6 +205,7 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
|
|||
|
||||
$LAB.script(assetPath('defer/html-sanitizer-bundle'));
|
||||
ComposerView.trigger("initWmdEditor");
|
||||
this._applyEmojiAutocomplete();
|
||||
|
||||
var template = this.container.lookup('template:user-selector-autocomplete.raw');
|
||||
$wmdInput.data('init', true);
|
||||
|
|
|
@ -130,6 +130,10 @@
|
|||
section.field {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
section.field .field-item {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.reply-where-modal {
|
||||
|
|
|
@ -320,12 +320,15 @@ class Admin::UsersController < Admin::AdminController
|
|||
user.email_tokens.update_all confirmed: true
|
||||
|
||||
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,
|
||||
user_id: user.id,
|
||||
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
|
||||
|
||||
|
|
|
@ -95,6 +95,19 @@ class CategoriesController < ApplicationController
|
|||
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
|
||||
category_id = params[:category_id].to_i
|
||||
notification_level = params[:notification_level].to_i
|
||||
|
|
|
@ -15,7 +15,7 @@ class ExportCsvController < ApplicationController
|
|||
def show
|
||||
params.require(: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 = UserExport.where(id: export_id)[0].user_id unless UserExport.where(id: export_id).empty?
|
||||
export_csv_path = UserExport.get_download_path(filename)
|
||||
|
|
|
@ -2,6 +2,7 @@ require_dependency 'topic_view'
|
|||
require_dependency 'promotion'
|
||||
require_dependency 'url_helper'
|
||||
require_dependency 'topics_bulk_action'
|
||||
require_dependency 'discourse_event'
|
||||
|
||||
class TopicsController < ApplicationController
|
||||
include UrlHelper
|
||||
|
@ -134,6 +135,8 @@ class TopicsController < ApplicationController
|
|||
success = PostRevisor.new(first_post, topic).revise!(current_user, changes, validate_post: false)
|
||||
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"
|
||||
success ? render_serialized(topic, BasicTopicSerializer) : render_json_error(topic)
|
||||
end
|
||||
|
|
|
@ -24,6 +24,7 @@ module Jobs
|
|||
|
||||
def execute(args)
|
||||
entity = args[:entity]
|
||||
@file_name = entity
|
||||
|
||||
if entity == "user_archive"
|
||||
@entity_type = "user"
|
||||
|
@ -56,19 +57,25 @@ module Jobs
|
|||
end
|
||||
end
|
||||
|
||||
def user_export
|
||||
def user_list_export
|
||||
query = ::AdminUserIndexQuery.new
|
||||
user_data = query.find_users_query.to_a
|
||||
user_data.map do |user|
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
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|
|
||||
get_staff_action_fields(staff_action)
|
||||
end
|
||||
|
@ -162,7 +169,7 @@ module Jobs
|
|||
user_archive_array
|
||||
end
|
||||
|
||||
def get_user_fields(user)
|
||||
def get_user_list_fields(user)
|
||||
user_array = []
|
||||
|
||||
HEADER_ATTRS_FOR['user'].each do |attr|
|
||||
|
@ -265,7 +272,8 @@ module Jobs
|
|||
|
||||
def set_file_path
|
||||
@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
|
||||
dir = File.dirname("#{UserExport.base_directory}/#{@file_name}")
|
||||
|
|
|
@ -201,18 +201,19 @@ SQL
|
|||
end
|
||||
|
||||
def ensure_slug
|
||||
if name.present?
|
||||
self.name.strip!
|
||||
return unless name.present?
|
||||
|
||||
if slug.present?
|
||||
# custom slug
|
||||
errors.add(:slug, "is already in use") if duplicate_slug?
|
||||
else
|
||||
# auto slug
|
||||
self.slug = Slug.for(name)
|
||||
return if self.slug.blank?
|
||||
self.slug = '' if duplicate_slug?
|
||||
end
|
||||
self.name.strip!
|
||||
|
||||
if slug.present?
|
||||
# santized custom slug
|
||||
self.slug = Slug.for(slug)
|
||||
errors.add(:slug, 'is already in use') if duplicate_slug?
|
||||
else
|
||||
# auto slug
|
||||
self.slug = Slug.for(name)
|
||||
return if self.slug.blank?
|
||||
self.slug = '' if duplicate_slug?
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ class UserHistory < ActiveRecord::Base
|
|||
:change_site_setting,
|
||||
:change_site_customization,
|
||||
:delete_site_customization,
|
||||
:checked_for_custom_avatar,
|
||||
:checked_for_custom_avatar, # not used anymore
|
||||
:notified_about_avatar,
|
||||
:notified_about_sequential_replies,
|
||||
:notified_about_dominating_topic,
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
|
||||
<meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<meta name="author" content="">
|
||||
<meta name="generator" content="Discourse <%= Discourse::VERSION::STRING %> - https://github.com/discourse/discourse version <%= Discourse.git_version %>">
|
||||
<meta name="author" content="">
|
||||
<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="apple-touch-icon" type="image/png" href="<%=SiteSetting.apple_touch_icon_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%>">
|
||||
|
||||
<%= canonical_link_tag %>
|
||||
<%= canonical_link_tag %>
|
||||
|
||||
<%= render partial: "common/special_font_face" %>
|
||||
<%= render partial: "common/discourse_stylesheet" %>
|
||||
<%= render partial: "common/special_font_face" %>
|
||||
<%= render partial: "common/discourse_stylesheet" %>
|
||||
|
||||
<%= discourse_csrf_tags %>
|
||||
<%= discourse_csrf_tags %>
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
<%= render partial: "layouts/head" %>
|
||||
|
||||
<%- if SiteSetting.enable_escaped_fragments? %>
|
||||
<meta name="fragment" content="!">
|
||||
<meta name="fragment" content="!">
|
||||
<%- end %>
|
||||
|
||||
<%- if shared_session_key %>
|
||||
<meta name="shared_session_key" content="<%= shared_session_key %>">
|
||||
<meta name="shared_session_key" content="<%= shared_session_key %>">
|
||||
<%- end %>
|
||||
|
||||
<%= script "preload_store" %>
|
||||
|
|
|
@ -20,13 +20,13 @@
|
|||
|
||||
<% if @rss %>
|
||||
<% 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 %>
|
||||
|
||||
<% if @category %>
|
||||
<% 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 %>
|
||||
|
||||
|
|
|
@ -1281,6 +1281,8 @@ en:
|
|||
delete: 'Delete Category'
|
||||
create: 'New 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.
|
||||
save_error: There was an error saving the category.
|
||||
name: "Category Name"
|
||||
|
|
|
@ -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>.'
|
||||
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>.'
|
||||
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>."
|
||||
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_invalid: "The site contact email is invalid. Please update contact_email in the <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>."
|
||||
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>."
|
||||
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: "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. Update it in <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: "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."
|
||||
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>."
|
||||
|
||||
content_types:
|
||||
|
@ -657,9 +657,9 @@ en:
|
|||
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"
|
||||
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."
|
||||
site_description: "Describe this site in one sentence, 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."
|
||||
title: "The name of this site, as used in the title tag."
|
||||
site_description: "Describe this site in one sentence, as used in the meta description tag."
|
||||
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."
|
||||
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."
|
||||
|
@ -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."
|
||||
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"
|
||||
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."
|
||||
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."
|
||||
|
|
|
@ -325,6 +325,7 @@ Discourse::Application.routes.draw do
|
|||
post "category/uploads" => "categories#upload"
|
||||
post "category/:category_id/move" => "categories#move"
|
||||
post "category/:category_id/notifications" => "categories#set_notifications"
|
||||
put "category/:category_id/slug" => "categories#update_slug"
|
||||
|
||||
get "c/:id/show" => "categories#show"
|
||||
get "c/:category.rss" => "list#category_feed", format: :rss
|
||||
|
|
10
db/migrate/20150102113309_clean_up_user_history.rb
Normal file
10
db/migrate/20150102113309_clean_up_user_history.rb
Normal 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
|
|
@ -26,9 +26,11 @@ class ComposerMessagesFinder
|
|||
|
||||
if count < SiteSetting.educate_until_posts
|
||||
education_posts_text = I18n.t('education.until_posts', count: SiteSetting.educate_until_posts)
|
||||
return {templateName: 'composer/education',
|
||||
wait_for_typing: true,
|
||||
body: PrettyText.cook(SiteText.text_for(education_key, education_posts_text: education_posts_text)) }
|
||||
return {
|
||||
templateName: 'composer/education',
|
||||
wait_for_typing: true,
|
||||
body: PrettyText.cook(SiteText.text_for(education_key, education_posts_text: education_posts_text))
|
||||
}
|
||||
end
|
||||
|
||||
nil
|
||||
|
@ -37,7 +39,11 @@ class ComposerMessagesFinder
|
|||
# New users have a limited number of replies in a topic
|
||||
def check_new_user_many_replies
|
||||
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
|
||||
|
||||
# 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.
|
||||
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
|
||||
UserHistory.create!(action: UserHistory.actions[:notified_about_avatar], target_user_id: @user.id )
|
||||
|
||||
# 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
|
||||
|
||||
# Is a user replying too much in succession?
|
||||
|
@ -87,10 +93,12 @@ class ComposerMessagesFinder
|
|||
target_user_id: @user.id,
|
||||
topic_id: @details[:topic_id] )
|
||||
|
||||
{templateName: 'composer/education',
|
||||
wait_for_typing: true,
|
||||
extraClass: 'urgent',
|
||||
body: PrettyText.cook(I18n.t('education.sequential_replies')) }
|
||||
{
|
||||
templateName: 'composer/education',
|
||||
wait_for_typing: true,
|
||||
extraClass: 'urgent',
|
||||
body: PrettyText.cook(I18n.t('education.sequential_replies'))
|
||||
}
|
||||
end
|
||||
|
||||
def check_dominating_topic
|
||||
|
@ -102,6 +110,7 @@ class ComposerMessagesFinder
|
|||
!UserHistory.exists_for_user?(@user, :notified_about_dominating_topic, topic_id: @details[:topic_id])
|
||||
|
||||
topic = Topic.find_by(id: @details[:topic_id])
|
||||
|
||||
return if topic.blank? ||
|
||||
topic.user_id == @user.id ||
|
||||
topic.posts_count < SiteSetting.summary_posts_required ||
|
||||
|
@ -117,11 +126,12 @@ class ComposerMessagesFinder
|
|||
target_user_id: @user.id,
|
||||
topic_id: @details[:topic_id])
|
||||
|
||||
|
||||
{templateName: 'composer/education',
|
||||
wait_for_typing: true,
|
||||
extraClass: 'urgent',
|
||||
body: PrettyText.cook(I18n.t('education.dominating_topic', percent: (ratio * 100).round)) }
|
||||
{
|
||||
templateName: 'composer/education',
|
||||
wait_for_typing: true,
|
||||
extraClass: 'urgent',
|
||||
body: PrettyText.cook(I18n.t('education.dominating_topic', percent: (ratio * 100).round))
|
||||
}
|
||||
end
|
||||
|
||||
def check_reviving_old_topic
|
||||
|
@ -136,20 +146,22 @@ class ComposerMessagesFinder
|
|||
topic.last_posted_at.nil? ||
|
||||
topic.last_posted_at > SiteSetting.warn_reviving_old_topic_age.days.ago
|
||||
|
||||
{templateName: 'composer/education',
|
||||
wait_for_typing: false,
|
||||
extraClass: 'urgent',
|
||||
body: PrettyText.cook(I18n.t('education.reviving_old_topic', days: (Time.zone.now - topic.last_posted_at).round / 1.day)) }
|
||||
{
|
||||
templateName: 'composer/education',
|
||||
wait_for_typing: false,
|
||||
extraClass: 'urgent',
|
||||
body: PrettyText.cook(I18n.t('education.reviving_old_topic', days: (Time.zone.now - topic.last_posted_at).round / 1.day))
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def creating_topic?
|
||||
return @details[:composerAction] == "createTopic"
|
||||
@details[:composerAction] == "createTopic"
|
||||
end
|
||||
|
||||
def replying?
|
||||
return @details[:composerAction] == "reply"
|
||||
@details[:composerAction] == "reply"
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -83,44 +83,31 @@ describe ComposerMessagesFinder do
|
|||
let(:finder) { ComposerMessagesFinder.new(user, composerAction: 'createTopic') }
|
||||
let(:user) { Fabricate(:user) }
|
||||
|
||||
context "a user who we haven't checked for an avatar yet" do
|
||||
it "returns no avatar message" do
|
||||
finder.check_avatar_notification.should be_blank
|
||||
context "success" do
|
||||
let!(:message) { finder.check_avatar_notification }
|
||||
|
||||
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
|
||||
|
||||
context "a user who has been checked for a custom avatar" do
|
||||
before do
|
||||
UserHistory.create!(action: UserHistory.actions[:checked_for_custom_avatar], target_user_id: user.id )
|
||||
end
|
||||
it "doesn't return notifications for new users" do
|
||||
user.trust_level = TrustLevel[0]
|
||||
finder.check_avatar_notification.should be_blank
|
||||
end
|
||||
|
||||
context "success" do
|
||||
let!(:message) { finder.check_avatar_notification }
|
||||
|
||||
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
|
||||
|
||||
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 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
|
||||
end
|
||||
|
||||
|
|
|
@ -111,10 +111,13 @@ describe HasCustomFields do
|
|||
db_item = CustomFieldsTestItem.find(test_item.id)
|
||||
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.custom_fields.should == {"a" => ["c", "d"]}
|
||||
|
||||
db_item.custom_fields.delete('a')
|
||||
db_item.custom_fields.should == {}
|
||||
|
||||
end
|
||||
|
||||
it "casts integers in arrays properly without error" do
|
||||
|
|
|
@ -460,6 +460,7 @@ describe Admin::UsersController do
|
|||
|
||||
context ".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'
|
||||
response.should be_success
|
||||
|
||||
|
@ -468,6 +469,14 @@ describe Admin::UsersController do
|
|||
u.username.should == "bill22"
|
||||
u.admin.should == true
|
||||
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
|
||||
|
|
|
@ -203,4 +203,42 @@ describe CategoriesController do
|
|||
|
||||
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
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
require "spec_helper"
|
||||
|
||||
describe ExportCsvController do
|
||||
let(:export_filename) { "export_999.csv" }
|
||||
let(:export_filename) { "user-archive-999.csv" }
|
||||
|
||||
|
||||
context "while logged in as normal user" do
|
||||
|
@ -30,7 +30,7 @@ describe ExportCsvController do
|
|||
describe ".download" do
|
||||
it "uses send_file to transmit the export file" do
|
||||
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)
|
||||
export = UserExport.new()
|
||||
UserExport.expects(:get_download_path).with(file_name).returns(export)
|
||||
|
@ -74,7 +74,7 @@ describe ExportCsvController do
|
|||
describe ".download" do
|
||||
it "uses send_file to transmit the export file" do
|
||||
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)
|
||||
export = UserExport.new()
|
||||
UserExport.expects(:get_download_path).with(file_name).returns(export)
|
||||
|
|
|
@ -7,3 +7,9 @@ Fabricator(:diff_category, from: :category) do
|
|||
name "Different Category"
|
||||
user
|
||||
end
|
||||
|
||||
Fabricator(:happy_category, from: :category) do
|
||||
name 'Happy Category'
|
||||
slug 'happy'
|
||||
user
|
||||
end
|
||||
|
|
|
@ -8,16 +8,16 @@ describe Jobs::ExportCsvFile do
|
|||
end
|
||||
end
|
||||
|
||||
let :user_header do
|
||||
let :user_list_header do
|
||||
Jobs::ExportCsvFile.new.get_header('user')
|
||||
end
|
||||
|
||||
let :user_export do
|
||||
Jobs::ExportCsvFile.new.user_export
|
||||
let :user_list_export do
|
||||
Jobs::ExportCsvFile.new.user_list_export
|
||||
end
|
||||
|
||||
def to_hash(row)
|
||||
Hash[*user_header.zip(row).flatten]
|
||||
Hash[*user_list_header.zip(row).flatten]
|
||||
end
|
||||
|
||||
it 'exports sso data' do
|
||||
|
@ -25,10 +25,9 @@ describe Jobs::ExportCsvFile do
|
|||
user = Fabricate(:user)
|
||||
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_email"].should == "test@test.com"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -198,6 +198,11 @@ describe Category do
|
|||
c.slug.should eq("cats-category")
|
||||
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
|
||||
c1 = Fabricate(:category, name: "Cats", slug: "cats")
|
||||
c2 = Fabricate.build(:category, name: "More Cats", slug: "cats")
|
||||
|
|
|
@ -28,8 +28,8 @@ test("editingMode", function() {
|
|||
topicController.set('model.details.can_edit', true);
|
||||
topicController.send('editTopic');
|
||||
ok(topicController.get('editingTopic'), "calling editTopic enables editing if the user can edit");
|
||||
equal(topicController.get('newTitle'), topic.get('title'));
|
||||
equal(topicController.get('newCategoryId'), topic.get('category_id'));
|
||||
equal(topicController.get('buffered.title'), topic.get('title'));
|
||||
equal(topicController.get('buffered.category_id'), topic.get('category_id'));
|
||||
|
||||
topicController.send('cancelEditingTopic');
|
||||
ok(!topicController.get('editingTopic'), "cancelling edit mode reverts the property value");
|
||||
|
|
Loading…
Reference in a new issue