Add fixed_category_positions site setting to handle whether categories are ordered by specified positions or by activity.

This commit is contained in:
Neil Lalonde 2014-05-16 11:33:44 -04:00
parent 417fdeaad8
commit 27cbc06563
17 changed files with 91 additions and 101 deletions

View File

@ -32,6 +32,10 @@ export default Discourse.DiscoveryController.extend({
return Discourse.User.currentProp('staff');
}.property(),
canOrder: function() {
return this.get('canEdit') && Discourse.SiteSettings.fixed_category_positions;
}.property('Discourse.SiteSettings.fixed_category_positions'),
moveCategory: function(categoryId, position){
this.get('model.categories').moveCategory(categoryId, position);
},

View File

@ -12,7 +12,6 @@ export default Discourse.ObjectController.extend(Discourse.ModalFunctionality, {
securitySelected: Ember.computed.equal('selectedTab', 'security'),
settingsSelected: Ember.computed.equal('selectedTab', 'settings'),
foregroundColors: ['FFFFFF', '000000'],
defaultPosition: false,
parentCategories: function() {
return Discourse.Category.list().filter(function (c) {
@ -29,7 +28,6 @@ export default Discourse.ObjectController.extend(Discourse.ModalFunctionality, {
onShow: function() {
this.changeSize();
this.titleChanged();
this.set('defaultPosition', this.get('position') === null);
},
changeSize: function() {
@ -116,6 +114,10 @@ export default Discourse.ObjectController.extend(Discourse.ModalFunctionality, {
return !this.get('isUncategorized') && this.get('id');
}.property('isUncategorized', 'id'),
showPositionInput: function() {
return Discourse.SiteSettings.fixed_category_positions;
}.property('Discourse.SiteSettings.fixed_category_positions'),
actions: {
selectGeneral: function() {
@ -148,17 +150,6 @@ export default Discourse.ObjectController.extend(Discourse.ModalFunctionality, {
this.get('model').removePermission(permission);
},
toggleDefaultPosition: function() {
this.toggleProperty('defaultPosition');
},
disableDefaultPosition: function() {
this.set('defaultPosition', false);
Em.run.schedule('afterRender', function() {
this.$('.position-input').focus();
});
},
saveCategory: function() {
var self = this,
model = this.get('model'),
@ -166,7 +157,6 @@ export default Discourse.ObjectController.extend(Discourse.ModalFunctionality, {
this.set('saving', true);
model.set('parentCategory', parentCategory);
if (this.get('defaultPosition')) { model.set('position', 'default'); }
self.set('saving', false);
this.get('model').save().then(function(result) {

View File

@ -7,7 +7,7 @@
<th class='latest'>{{i18n categories.latest}}</th>
<th class='stats topics'>{{i18n categories.topics}}</th>
<th class='stats posts'>{{i18n categories.posts}}
{{#if canEdit}}<button title='{{i18n categories.toggle_ordering}}' class='btn toggle-admin no-text' {{action toggleOrdering}}><i class='fa fa-wrench'></i></button>{{/if}}
{{#if canOrder}}<button title='{{i18n categories.toggle_ordering}}' class='btn toggle-admin no-text' {{action toggleOrdering}}><i class='fa fa-wrench'></i></button>{{/if}}
</th>
</tr>
</thead>

View File

@ -123,15 +123,12 @@
</section>
{{/if}}
<section class='field'>
<label>{{i18n category.position}}</label>
<span {{action disableDefaultPosition}}>{{textField value=position disabled=defaultPosition class="position-input"}}</span>
&nbsp;{{i18n or}}&nbsp;
<button {{bind-attr class=":btn defaultPosition:btn-primary"}} {{action toggleDefaultPosition}}>
<span {{bind-attr class="defaultPosition::hidden"}}><i class="fa fa-check"></i></span>
{{i18n category.default_position}}
</button>
</section>
{{#if showPositionInput}}
<section class='field'>
<label>{{i18n category.position}}</label>
{{textField value=position class="position-input"}}
</section>
{{/if}}
</div>
{{/unless}}
</div>

View File

@ -50,19 +50,19 @@ class CategoriesController < ApplicationController
def create
guardian.ensure_can_create!(Category)
position = category_params.delete(:position)
@category = Category.create(category_params.merge(user: current_user))
return render_json_error(@category) unless @category.save
@category.move_to(category_params[:position].to_i) if category_params[:position]
@category.move_to(position.to_i) if position
render_serialized(@category, CategorySerializer)
end
def update
guardian.ensure_can_edit!(@category)
json_result(@category, serializer: CategorySerializer) { |cat|
if category_params[:position]
category_params[:position] == 'default' ? cat.use_default_position : cat.move_to(category_params[:position].to_i)
end
cat.move_to(category_params[:position].to_i) if category_params[:position]
if category_params.key? :email_in and category_params[:email_in].length == 0
# properly null the value so the database constrain doesn't catch us
category_params[:email_in] = nil

View File

@ -57,16 +57,19 @@ class CategoryList
@categories = Category
.includes(:featured_users, subcategories: [:topic_only_relative_url])
.secured(@guardian)
.order('position asc')
.order('COALESCE(categories.posts_week, 0) DESC')
.order('COALESCE(categories.posts_month, 0) DESC')
.order('COALESCE(categories.posts_year, 0) DESC')
.to_a
if SiteSetting.fixed_category_positions
@categories = @categories.order('position ASC').order('id ASC')
else
@categories = @categories.order('COALESCE(categories.posts_week, 0) DESC')
.order('COALESCE(categories.posts_month, 0) DESC')
.order('COALESCE(categories.posts_year, 0) DESC')
end
if latest_post_only?
@categories = @categories.includes(:latest_post => {:topic => :last_poster} )
end
@categories = @categories.to_a
subcategories = {}
to_delete = Set.new
@categories.each do |c|

View File

@ -1,6 +1,12 @@
module Positionable
extend ActiveSupport::Concern
included do
before_save do
self.position ||= self.class.count
end
end
def move_to(position_arg)
position = [[position_arg, 0].max, self.class.count - 1].min
@ -27,11 +33,4 @@ module Positionable
SET position = :position
WHERE id = :id", {id: id, position: position}
end
def use_default_position
self.exec_sql "
UPDATE #{self.class.table_name}
SET POSITION = null
WHERE id = :id", {id: id}
end
end

View File

@ -620,6 +620,7 @@ en:
max_image_width: "Maximum allowed width of images in a post"
max_image_height: "Maximum allowed height of images in a post"
category_featured_topics: "Number of topics displayed per category on the /categories page. After changing this value, it takes up to 15 minutes for the categories page to update."
fixed_category_positions: "If checked, you will be able to arrange categories into a fixed order. If unchecked, categories are listed in order of activity."
add_rel_nofollow_to_user_content: "Add rel nofollow to all submitted user content, except for internal links (including parent domains) changing this requires you update all your baked markdown with: \"rake posts:rebake\""
exclude_rel_nofollow_domains: "A pipe-delimited list of domains where nofollow is not added (tld.com will automatically allow sub.tld.com as well)"

View File

@ -78,6 +78,9 @@ basic:
category_featured_topics:
client: true
default: 3
fixed_category_positions:
client: true
default: false
topics_per_page: 30
posts_per_page:
client: true

View File

@ -9,7 +9,7 @@ class AddUncategorizedCategory < ActiveRecord::Migration
result = execute "INSERT INTO categories
(name,color,slug,description,text_color, user_id, created_at, updated_at, position)
VALUES ('#{name}', 'AB9364', 'uncategorized', '', 'FFFFFF', -1, now(), now(), 1 )
VALUES ('#{name}', 'AB9364', 'uncategorized', '', 'FFFFFF', -1, now(), now(), 0 )
RETURNING id
"
category_id = result[0]["id"].to_i

View File

@ -13,8 +13,8 @@ class AddLoungeCategory < ActiveRecord::Migration
end
result = execute "INSERT INTO categories
(name, color, text_color, created_at, updated_at, user_id, slug, description, read_restricted)
VALUES ('#{name}', 'EEEEEE', '652D90', now(), now(), -1, '#{Slug.for(name)}', '#{description}', true)
(name, color, text_color, created_at, updated_at, user_id, slug, description, read_restricted, position)
VALUES ('#{name}', 'EEEEEE', '652D90', now(), now(), -1, '#{Slug.for(name)}', '#{description}', true, 3)
RETURNING id"
category_id = result[0]["id"].to_i

View File

@ -8,8 +8,8 @@ class AddMetaCategory < ActiveRecord::Migration
name = I18n.t('meta_category_name')
if Category.exec_sql("SELECT 1 FROM categories where name ilike '#{name}'").count == 0
result = execute "INSERT INTO categories
(name, color, text_color, created_at, updated_at, user_id, slug, description, read_restricted)
VALUES ('#{name}', '808281', 'FFFFFF', now(), now(), -1, '#{Slug.for(name)}', '#{description}', true)
(name, color, text_color, created_at, updated_at, user_id, slug, description, read_restricted, position)
VALUES ('#{name}', '808281', 'FFFFFF', now(), now(), -1, '#{Slug.for(name)}', '#{description}', true, 1)
RETURNING id"
category_id = result[0]["id"].to_i

View File

@ -7,8 +7,8 @@ class AddStaffCategory < ActiveRecord::Migration
name = I18n.t('staff_category_name')
if Category.exec_sql("SELECT 1 FROM categories where name ilike '#{name}'").count == 0
result = execute "INSERT INTO categories
(name, color, text_color, created_at, updated_at, user_id, slug, description, read_restricted)
VALUES ('#{name}', '283890', 'FFFFFF', now(), now(), -1, '#{Slug.for(name)}', '#{description}', true)
(name, color, text_color, created_at, updated_at, user_id, slug, description, read_restricted, position)
VALUES ('#{name}', '283890', 'FFFFFF', now(), now(), -1, '#{Slug.for(name)}', '#{description}', true, 2)
RETURNING id"
category_id = result[0]["id"].to_i

View File

@ -0,0 +1,15 @@
class InitFixedCategoryPositionsValue < ActiveRecord::Migration
def up
# Look at existing categories to determine if positions have been specified
result = Category.exec_sql("SELECT count(*) FROM categories WHERE position IS NOT NULL")
# Greater than 4 because uncategorized, meta, staff, lounge all have positions by default
if result[0]['count'].to_i > 4
execute "INSERT INTO site_settings (name, data_type, value, created_at, updated_at) VALUES ('fixed_category_positions', 5, 't', now(), now())"
end
end
def down
execute "DELETE FROM site_settings WHERE name = 'fixed_category_positions'"
end
end

View File

@ -89,29 +89,41 @@ describe CategoryList do
uncategorized.save
end
it "returns topics in specified order" do
cat1, cat2 = Fabricate(:category, position: 1), Fabricate(:category, position: 0)
category_ids.should == [cat2.id, cat1.id]
context 'fixed_category_positions is enabled' do
before do
SiteSetting.stubs(:fixed_category_positions).returns(true)
end
it "returns categories in specified order" do
cat1, cat2 = Fabricate(:category, position: 1), Fabricate(:category, position: 0)
category_ids.should == [cat2.id, cat1.id]
end
it "handles duplicate position values" do
cat1, cat2, cat3, cat4 = Fabricate(:category, position: 0), Fabricate(:category, position: 0), Fabricate(:category, position: nil), Fabricate(:category, position: 0)
first_three = category_ids[0,3] # The order is not deterministic
first_three.should include(cat1.id)
first_three.should include(cat2.id)
first_three.should include(cat4.id)
category_ids[-1].should == cat3.id
end
end
it "returns default order categories" do
cat1, cat2 = Fabricate(:category, position: nil), Fabricate(:category, position: nil)
category_ids.should include(cat1.id)
category_ids.should include(cat2.id)
end
context 'fixed_category_positions is disabled' do
before do
SiteSetting.stubs(:fixed_category_positions).returns(false)
end
it "default always at the end" do
cat1, cat2, cat3 = Fabricate(:category, position: 0), Fabricate(:category, position: 2), Fabricate(:category, position: nil)
category_ids.should == [cat1.id, cat2.id, cat3.id]
end
it "returns categories in order of activity" do
cat1 = Fabricate(:category, position: 0, posts_week: 1, posts_month: 1, posts_year: 1)
cat2 = Fabricate(:category, position: 1, posts_week: 2, posts_month: 1, posts_year: 1)
category_ids.should == [cat2.id, cat1.id]
end
it "handles duplicate position values" do
cat1, cat2, cat3, cat4 = Fabricate(:category, position: 0), Fabricate(:category, position: 0), Fabricate(:category, position: nil), Fabricate(:category, position: 0)
first_three = category_ids[0,3] # The order is not deterministic
first_three.should include(cat1.id)
first_three.should include(cat2.id)
first_three.should include(cat4.id)
category_ids[-1].should == cat3.id
it "returns categories in order of id when there's no activity" do
cat1, cat2 = Fabricate(:category, position: 1), Fabricate(:category, position: 0)
category_ids.should == [cat1.id, cat2.id]
end
end
end

View File

@ -45,34 +45,7 @@ describe Positionable do
item = TestItem.new
item.id = 7
item.save
item.position.should be_nil
end
it "can set records to have null position" do
5.times do |i|
Topic.exec_sql("insert into test_items(id,position) values(#{i}, #{i})")
end
TestItem.find(2).use_default_position
TestItem.find(2).position.should be_nil
TestItem.find(1).move_to(4)
TestItem.order('id ASC').pluck(:position).should == [0,4,nil,2,3]
end
it "can maintain null positions when moving things around" do
5.times do |i|
Topic.exec_sql("insert into test_items(id,position) values(#{i}, null)")
end
TestItem.find(2).move_to(0)
TestItem.order('id asc').pluck(:position).should == [nil,nil,0,nil,nil]
TestItem.find(0).move_to(4)
TestItem.order('id asc').pluck(:position).should == [4,nil,0,nil,nil]
TestItem.find(2).move_to(1)
TestItem.order('id asc').pluck(:position).should == [4,nil,1,nil,nil]
TestItem.find(0).move_to(1)
TestItem.order('id asc').pluck(:position).should == [1,nil,2,nil,nil]
item.position.should == 5
end
end
end

View File

@ -165,13 +165,6 @@ describe CategoriesController do
@category.color.should == "ff0"
@category.auto_close_hours.should == 72
end
it "can set category to use default position" do
xhr :put, :update, valid_attrs.merge(position: 'default')
response.should be_success
@category.reload
@category.position.should be_nil
end
end
end