Can edit category descriptions, they show up in a title attribute

This commit is contained in:
Robin Ward 2013-02-21 18:09:56 -05:00
parent 2d9942ceef
commit 532b1f5450
21 changed files with 268 additions and 103 deletions

View file

@ -40,16 +40,22 @@
*/
categoryLink: function(category) {
var color, name;
if (!category) {
return "";
}
var color, name, description, result;
if (!category) return "";
color = Em.get(category, 'color');
name = Em.get(category, 'name');
return "<a href=\"/category/" +
(this.categoryUrlId(category)) +
"\" class=\"badge-category excerptable\" data-excerpt-size=\"medium\" style=\"background-color: #" + color + "\">" +
name + "</a>";
description = Em.get(category, 'description');
// Build the HTML link
result = "<a href=\"/category/" +
this.categoryUrlId(category) +
"\" class=\"badge-category excerptable\" data-excerpt-size=\"medium\" ";
// Add description if we have it
if (description) result += "title=\"" + description + "\" ";
return result + "style=\"background-color: #" + color + "\">" + name + "</a>";
},
avatarUrl: function(username, size, template) {
var rawSize;

View file

@ -312,6 +312,7 @@
post.set('cooked', jQuery('#wmd-preview').html());
this.set('composeState', CLOSED);
post.save(function(savedPost) {
var idx, postNumber, posts;
posts = _this.get('topic.posts');
/* perhaps our post came from elsewhere eg. draft

View file

@ -144,22 +144,25 @@
url: "/posts/" + (this.get('id')),
type: 'PUT',
data: {
post: {
raw: this.get('raw')
},
post: { raw: this.get('raw') },
image_sizes: this.get('imageSizes')
},
success: function(result) {
return typeof complete === "function" ? complete(Discourse.Post.create(result)) : void 0;
console.log(result)
// If we received a category update, update it
if (result.category) Discourse.get('site').updateCategory(result.category);
return typeof complete === "function" ? complete(Discourse.Post.create(result.post)) : void 0;
},
error: function(result) {
return typeof error === "function" ? error(result) : void 0;
}
});
} else {
/* We're saving a post
*/
// We're saving a post
data = {
post: this.getProperties('raw', 'topic_id', 'reply_to_post_number', 'category'),
archetype: this.get('archetype'),

View file

@ -1,6 +1,7 @@
(function() {
window.Discourse.Site = Discourse.Model.extend({
notificationLookup: (function() {
var result;
result = [];
@ -9,6 +10,7 @@
});
return result;
}).property('notification_types'),
flagTypes: (function() {
var postActionTypes;
postActionTypes = this.get('post_action_types');
@ -17,8 +19,14 @@
}
return postActionTypes.filterProperty('is_flag', true);
}).property('post_action_types.@each'),
postActionTypeById: function(id) {
return this.get("postActionByIdLookup.action" + id);
},
updateCategory: function(newCategory) {
var existingCategory = this.get('categories').findProperty('id', Em.get(newCategory, 'id'));
if (existingCategory) existingCategory.mergeAttributes(newCategory);
}
});

View file

@ -3,10 +3,17 @@
<label>{{i18n category.name}}</label>
{{view Discourse.TextField valueBinding="view.category.name" placeholderKey="category.name_placeholder"}}
{{#if view.category.excerpt}}
<div class='description-controls' style="margin-bottom: 20px">
<label>{{i18n category.description}}</label>
<p>{{view.category.excerpt}} <a href="{{unbound view.category.topic_url}}">{{i18n category.topic}}</a></p>
{{/if}}
{{#if view.category.description}}
{{view.category.description}}
<a href="#" {{action showCategoryTopic target="view"}}>{{i18n category.change_in_category_topic}}</a>
{{else}}
{{i18n category.no_description}} <a {{action showCategoryTopic target="view"}} href="#">{{i18n category.change_in_category_topic}}</a>
{{/if}}
</div>
<label>{{i18n category.color}}</label>

View file

@ -1,64 +1,72 @@
(function() {
window.Discourse.EditCategoryView = window.Discourse.ModalBodyView.extend({
/**
A modal view for editing the basic aspects of a category
@class EditCategoryView
@extends Discourse.ModalBodyView
@namespace Discourse
@module Discourse
**/
Discourse.EditCategoryView = Discourse.ModalBodyView.extend({
templateName: 'modal/edit_category',
appControllerBinding: 'Discourse.appController',
disabled: (function() {
if (this.get('saving')) {
return true;
}
if (!this.get('category.name')) {
return true;
}
if (!this.get('category.color')) {
return true;
}
if (this.get('saving')) return true;
if (!this.get('category.name')) return true;
if (!this.get('category.color')) return true;
return false;
}).property('category.name', 'category.color'),
colorStyle: (function() {
return "background-color: #" + (this.get('category.color')) + ";";
}).property('category.color'),
title: (function() {
if (this.get('category.id')) {
return "Edit Category";
} else {
return "Create Category";
}
if (this.get('category.id')) return Em.String.i18n("category.edit_long");
return "Create Category";
}).property('category.id'),
buttonTitle: (function() {
if (this.get('saving')) {
return "Saving...";
} else {
return this.get('title');
}
if (this.get('saving')) return Em.String.i18n("saving");
return this.get('title');
}).property('title', 'saving'),
didInsertElement: function() {
this._super();
if (this.get('category')) {
return this.set('id', this.get('category.slug'));
this.set('id', this.get('category.slug'));
} else {
return this.set('category', Discourse.Category.create({
color: 'AB9364'
}));
this.set('category', Discourse.Category.create({ color: 'AB9364' }));
}
},
showCategoryTopic: function() {
jQuery('#discourse-modal').modal('hide');
Discourse.routeTo(this.get('category.topic_url'));
return false;
},
saveSuccess: function(result) {
jQuery('#discourse-modal').modal('hide');
window.location = "/category/" + (Discourse.Utilities.categoryUrlId(result.category));
},
saveCategory: function() {
var _this = this;
this.set('saving', true);
return this.get('category').save({
success: function(result) {
return _this.saveSuccess(result);
_this.saveSuccess(result);
},
error: function(errors) {
_this.displayErrors(errors);
return _this.set('saving', false);
_this.set('saving', false);
}
});
}
});
}).call(this);

View file

@ -35,23 +35,32 @@ class PostsController < ApplicationController
def update
requires_parameter(:post)
@post = Post.where(id: params[:id]).first
@post.image_sizes = params[:image_sizes] if params[:image_sizes].present?
guardian.ensure_can_edit!(@post)
if @post.revise(current_user, params[:post][:raw])
TopicLink.extract_from(@post)
post = Post.where(id: params[:id]).first
post.image_sizes = params[:image_sizes] if params[:image_sizes].present?
guardian.ensure_can_edit!(post)
revisor = PostRevisor.new(post)
if revisor.revise!(current_user, params[:post][:raw])
TopicLink.extract_from(post)
end
if @post.errors.present?
render_json_error(@post)
if post.errors.present?
render_json_error(post)
return
end
post_serializer = PostSerializer.new(@post, scope: guardian, root: false)
post_serializer.draft_sequence = DraftSequence.current(current_user, @post.topic.draft_key)
link_counts = TopicLinkClick.counts_for(@post.topic, [@post])
post_serializer.single_post_link_counts = link_counts[@post.id] if link_counts.present?
render_json_dump(post_serializer)
post_serializer = PostSerializer.new(post, scope: guardian, root: false)
post_serializer.draft_sequence = DraftSequence.current(current_user, post.topic.draft_key)
link_counts = TopicLinkClick.counts_for(post.topic, [post])
post_serializer.single_post_link_counts = link_counts[post.id] if link_counts.present?
result = {post: post_serializer.as_json}
if revisor.category_changed.present?
result[:category] = CategorySerializer.new(revisor.category_changed, scope: guardian, root: false).as_json
end
render_json_dump(result)
end
def by_number

View file

@ -44,19 +44,6 @@ class Category < ActiveRecord::Base
topics_week = (#{topics_week})")
end
# Use the first paragraph of the topic's first post as the excerpt
def excerpt
if topic.present?
first_post = topic.posts.order(:post_number).first
body = first_post.cooked
matches = body.scan(/\<p\>(.*)\<\/p\>/)
if matches and matches[0] and matches[0][0]
return matches[0][0]
end
end
nil
end
def topic_url
topic.try(:relative_url)
end
@ -67,11 +54,17 @@ class Category < ActiveRecord::Base
after_create do
topic = Topic.create!(title: I18n.t("category.topic_prefix", category: name), user: user, visible: false)
topic.posts.create!(raw: SiteSetting.category_post_template, user: user)
post_contents = I18n.t("category.post_template", replace_paragraph: I18n.t("category.replace_paragraph"))
topic.posts.create!(raw: post_contents, user: user)
update_column(:topic_id, topic.id)
topic.update_column(:category_id, self.id)
end
def self.post_template
I18n.t("category.post_template", replace_paragraph: I18n.t("category.replace_paragraph"))
end
# We cache the categories in the site json, so we need to invalidate it when they change
def invalidate_site_cache
Site.invalidate_cache

View file

@ -100,8 +100,6 @@ class SiteSetting < ActiveRecord::Base
setting(:best_of_score_threshold, 15)
setting(:best_of_posts_required, 50)
setting(:best_of_likes_required, 1)
setting(:category_post_template,
"[Replace this first paragraph with a short description of your new category. Try to keep it below 200 characters.]\n\nUse this space below for a longer description, as well as to establish any rules or discussion!")
# we need to think of a way to force users to enter certain settings, this is a minimal config thing
setting(:notification_email, 'info@discourse.org')

View file

@ -1,3 +1,11 @@
class CategorySerializer < ApplicationSerializer
attributes :id, :name, :color, :slug, :topic_count
attributes :id,
:name,
:color,
:slug,
:topic_count,
:description,
:topic_url
end

View file

@ -519,6 +519,7 @@ en:
category:
none: '(no category)'
edit: 'edit'
edit_long: "Edit Category"
view: 'View Topics in Category'
delete: 'Delete Category'
create: 'Create Category'
@ -531,6 +532,8 @@ en:
color_placeholder: "Any web color"
delete_confirm: "Are you sure you want to delete that category?"
list: "List Categories"
no_description: "There is no description for this category."
change_in_category_topic: "visit category topic to edit the description"
flagging:
title: 'Why are you flagging this post?'

View file

@ -228,7 +228,6 @@ nl:
exclude_rel_nofollow_domains: "Een commagescheiden lijst van domeinen waar 'nofollow' niet is toegevoegd. (voorbeelddomein.com zal automatisch sub.voorbeelddomein.com toestaan)"
post_excerpt_maxlength: "Maximale lengte in karakters van een post-uittreksel."
post_onebox_maxlength: "Maximale lengte van een 'oneboxed' discourse post."
category_post_template: "De post-template die verschijnt wanneer je een categorie aanmaakt"
new_topics_rollup: "Hoeveel topics er aan een topic-lijst kunnen worden toegevoegd voordat de lijst oprolt."
onebox_max_chars: "Het maximaal aantal karakters dat een 'onebox' zal importeren in een lap text."
logo_url: "Het logo van je site bijv: http://xyz.com/x.png"

View file

@ -62,6 +62,8 @@ en:
category:
topic_prefix: "Category definition for %{category}"
replace_paragraph: "[Replace this first paragraph with a short description of your new category. Try to keep it below 200 characters.]"
post_template: "%{replace_paragraph}\n\nUse this space below for a longer description, as well as to establish any rules or discussion!"
trust_levels:
new:
@ -254,7 +256,6 @@ en:
exclude_rel_nofollow_domains: "A comma delimited list of domains where nofollow is not added (tld.com will automatically allow sub.tld.com as well)"
post_excerpt_maxlength: "Maximum length in chars of a post's excerpt."
post_onebox_maxlength: "Maximum length of a oneboxed discourse post."
category_post_template: "The post template that appears once you create a category"
new_topics_rollup: "How many topics can be inserted on the topic list before rolling up."
onebox_max_chars: "The maximum amount of characters a onebox will import in a text blob."
logo_url: "The logo for your site eg: http://xyz.com/x.png"

View file

@ -261,7 +261,6 @@ fr:
exclude_rel_nofollow_domains: "Une liste séparée par des virgules contenant les noms de domaines de premier niveau pour lesquels il faut ajouter un attribut nofollow (exemple.com va automatiquement fonctionner aussi avec sous.domaine.exemple.com)"
post_excerpt_maxlength: "Longueur maximale d'un extrait de message."
post_onebox_maxlength: "Longueur maximale d'un message emboîté."
category_post_template: "Le modèle de message qui va apparaitre quand vous créez une catégorie"
new_topics_rollup: "Combien de discussions peuvent être insérées dans la liste des discussions avant de remonter."
onebox_max_chars: "Nombre maximal de caractères qu'une boîte peut importer en blob de texte."
logo_url: "Le logo de votre site, par exemple: http://xyz.com/x.png"

View file

@ -255,7 +255,6 @@ nl:
exclude_rel_nofollow_domains: "Een commagescheiden lijst van domeinen waar 'nofollow' niet is toegevoegd. (voorbeelddomein.com zal automatisch sub.voorbeelddomein.com toestaan)"
post_excerpt_maxlength: "Maximale lengte in karakters van een post-uittreksel."
post_onebox_maxlength: "Maximale lengte van een 'oneboxed' discourse post."
category_post_template: "De post-template die verschijnt wanneer je een categorie aanmaakt"
new_topics_rollup: "Hoeveel topics er aan een topic-lijst kunnen worden toegevoegd voordat de lijst oprolt."
onebox_max_chars: "Het maximaal aantal karakters dat een 'onebox' zal importeren in een lap tekst."
logo_url: "Het logo van je site bijv: http://xyz.com/x.png"

View file

@ -0,0 +1,25 @@
class AddDescriptionToCategories < ActiveRecord::Migration
def up
add_column :categories, :description, :text, null: true
# While we're at it, remove unused columns
remove_column :categories, :top1_topic_id
remove_column :categories, :top2_topic_id
remove_column :categories, :top1_user_id
remove_column :categories, :top2_user_id
# Migrate excerpts over
Category.all.each do |c|
excerpt = c.excerpt
unless excerpt == I18n.t("category.replace_paragraph")
c.update_column(:description, c.excerpt)
end
end
end
def down
remove_column :categories, :description
end
end

View file

@ -1222,10 +1222,6 @@ CREATE TABLE categories (
name character varying(50) NOT NULL,
color character varying(6) DEFAULT 'AB9364'::character varying NOT NULL,
topic_id integer,
top1_topic_id integer,
top2_topic_id integer,
top1_user_id integer,
top2_user_id integer,
topic_count integer DEFAULT 0 NOT NULL,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL,
@ -1233,7 +1229,8 @@ CREATE TABLE categories (
topics_year integer,
topics_month integer,
topics_week integer,
slug character varying(255) NOT NULL
slug character varying(255) NOT NULL,
description text
);
@ -1242,7 +1239,7 @@ CREATE TABLE categories (
--
CREATE SEQUENCE categories_id_seq
START WITH 5
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
@ -1327,7 +1324,7 @@ CREATE TABLE draft_sequences (
--
CREATE SEQUENCE draft_sequences_id_seq
START WITH 20
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
@ -1361,7 +1358,7 @@ CREATE TABLE drafts (
--
CREATE SEQUENCE drafts_id_seq
START WITH 2
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
@ -1394,7 +1391,7 @@ CREATE TABLE email_logs (
--
CREATE SEQUENCE email_logs_id_seq
START WITH 3
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
@ -1429,7 +1426,7 @@ CREATE TABLE email_tokens (
--
CREATE SEQUENCE email_tokens_id_seq
START WITH 3
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
@ -1642,7 +1639,7 @@ CREATE TABLE onebox_renders (
--
CREATE SEQUENCE onebox_renders_id_seq
START WITH 2
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
@ -1676,7 +1673,7 @@ CREATE TABLE post_action_types (
--
CREATE SEQUENCE post_action_types_id_seq
START WITH 6
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
@ -1807,7 +1804,7 @@ CREATE TABLE posts (
--
CREATE SEQUENCE posts_id_seq
START WITH 16
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
@ -1898,7 +1895,7 @@ CREATE TABLE site_settings (
--
CREATE SEQUENCE site_settings_id_seq
START WITH 4
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
@ -1930,7 +1927,7 @@ CREATE TABLE topic_allowed_users (
--
CREATE SEQUENCE topic_allowed_users_id_seq
START WITH 3
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
@ -2122,7 +2119,7 @@ CREATE TABLE topics (
--
CREATE SEQUENCE topics_id_seq
START WITH 16
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
@ -2228,7 +2225,7 @@ CREATE TABLE user_actions (
--
CREATE SEQUENCE user_actions_id_seq
START WITH 40
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
@ -2292,7 +2289,7 @@ CREATE TABLE user_visits (
--
CREATE SEQUENCE user_visits_id_seq
START WITH 4
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
@ -2359,7 +2356,7 @@ CREATE TABLE users (
--
CREATE SEQUENCE users_id_seq
START WITH 3
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
@ -4578,4 +4575,6 @@ INSERT INTO schema_migrations (version) VALUES ('20130208220635');
INSERT INTO schema_migrations (version) VALUES ('20130213021450');
INSERT INTO schema_migrations (version) VALUES ('20130213203300');
INSERT INTO schema_migrations (version) VALUES ('20130213203300');
INSERT INTO schema_migrations (version) VALUES ('20130221215017');

View file

@ -1,5 +1,8 @@
require 'edit_rate_limiter'
class PostRevisor
attr_reader :category_changed
def initialize(post)
@post = post
end
@ -8,6 +11,7 @@ class PostRevisor
@user, @new_raw, @opts = user, new_raw, opts
return false if not should_revise?
revise_post
update_category_description
post_process_post
true
end
@ -76,6 +80,25 @@ class PostRevisor
@post.save
end
def update_category_description
# If we're revising the first post, we might have to update the category description
return unless @post.post_number == 1
# Is there a category with our topic id?
category = Category.where(topic_id: @post.topic_id).first
return unless category.present?
# If found, update its description
body = @post.cooked
matches = body.scan(/\<p\>(.*)\<\/p\>/)
if matches and matches[0] and matches[0][0]
new_description = matches[0][0]
new_description = nil if new_description == I18n.t("category.replace_paragraph")
category.update_column(:description, new_description)
@category_changed = category
end
end
def post_process_post
@post.invalidate_oneboxes = true
@post.trigger_post_process

View file

@ -43,6 +43,11 @@ describe PostRevisor do
it "doesn't change the last_version_at" do
post.last_version_at.should == first_version_at
end
it "doesn't update a category" do
subject.category_changed.should be_blank
end
end
describe 'revision much later' do
@ -55,6 +60,10 @@ describe PostRevisor do
post.reload
end
it "doesn't update a category" do
subject.category_changed.should be_blank
end
it 'updates the cached_version' do
post.cached_version.should == 2
end
@ -82,6 +91,10 @@ describe PostRevisor do
post.last_version_at.to_i.should == revised_at.to_i
end
it "doesn't update a category" do
subject.category_changed.should be_blank
end
context "after second window" do
let!(:new_revised_at) {revised_at + 2.minutes}
@ -102,6 +115,68 @@ describe PostRevisor do
end
end
describe 'category topic' do
let!(:category) do
category = Fabricate(:category)
category.update_column(:topic_id, topic.id)
category
end
let(:new_description) { "this is my new description." }
it "should have to description by default" do
category.description.should be_blank
end
context "one paragraph description" do
before do
subject.revise!(post.user, new_description)
category.reload
end
it "returns true for category_changed" do
subject.category_changed.should be_true
end
it "updates the description of the category" do
category.description.should == new_description
end
end
context "multiple paragraph description" do
before do
subject.revise!(post.user, "#{new_description}\n\nOther content goes here.")
category.reload
end
it "returns the changed category info" do
subject.category_changed.should == category
end
it "updates the description of the category" do
category.description.should == new_description
end
end
context 'when updating back to the original paragraph' do
before do
category.update_column(:description, 'this is my description')
subject.revise!(post.user, Category.post_template)
category.reload
end
it "puts the description back to nothing" do
category.description.should be_blank
end
it "returns true for category_changed" do
subject.category_changed.should == category
end
end
end
describe 'rate limiter' do
let(:changed_by) { Fabricate(:coding_horror) }

View file

@ -192,7 +192,7 @@ describe PostsController do
end
it "calls revise with valid parameters" do
Post.any_instance.expects(:revise).with(post.user, 'edited body')
PostRevisor.any_instance.expects(:revise!).with(post.user, 'edited body')
xhr :put, :update, update_params
end

View file

@ -83,6 +83,10 @@ describe Category do
@category.slug.should == 'amazing-category'
end
it 'has a default description' do
@category.description.should be_blank
end
it 'has one topic' do
Topic.where(category_id: @category).count.should == 1
end
@ -107,14 +111,12 @@ describe Category do
@topic.posts.count.should == 1
end
it 'should have an excerpt' do
@category.excerpt.should be_present
end
it 'should have a topic url' do
@category.topic_url.should be_present
end
describe "trying to change the category topic's category" do
before do
@ -166,8 +168,7 @@ describe Category do
context 'with regular topics' do
before do
@category.topics << Fabricate(:topic,
user: @category.user)
@category.topics << Fabricate(:topic, user: @category.user)
Category.update_stats
@category.reload
end