From 7ed309666bd4488d9cb6e3b3c35dc1db4adbbc49 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 22 Jun 2015 14:05:35 -0400 Subject: [PATCH] Refactor search results to be components instead of views for reuse --- .../components/search-result-category.js.es6 | 2 + .../components/search-result-post.js.es6 | 2 + .../components/search-result-topic.js.es6 | 2 + .../components/search-result-user.js.es6 | 2 + .../discourse/components/search-result.js.es6 | 11 + .../components/search-text-field.js.es6 | 7 + .../discourse/lib/search-for-term.js.es6 | 16 +- .../components/search-result-category.hbs | 7 + .../components/search-result-post.hbs | 12 + .../components/search-result-topic.hbs | 14 + .../components/search-result-user.hbs | 8 + .../discourse/templates/search.hbs | 7 +- .../templates/search/category_result.hbs | 3 - .../templates/search/post_result.hbs | 10 - .../templates/search/topic_result.hbs | 10 - .../templates/search/user_result.hbs | 4 - .../views/search-results-type.js.es6 | 15 - .../discourse/views/search-text-field.js.es6 | 27 - app/assets/javascripts/main_include.js | 1 + .../javascripts/acceptance/search-test.js.es6 | 18 + .../fixtures/search-fixtures.js.es6 | 667 +++++++++++++++++- 21 files changed, 764 insertions(+), 81 deletions(-) create mode 100644 app/assets/javascripts/discourse/components/search-result-category.js.es6 create mode 100644 app/assets/javascripts/discourse/components/search-result-post.js.es6 create mode 100644 app/assets/javascripts/discourse/components/search-result-topic.js.es6 create mode 100644 app/assets/javascripts/discourse/components/search-result-user.js.es6 create mode 100644 app/assets/javascripts/discourse/components/search-result.js.es6 create mode 100644 app/assets/javascripts/discourse/components/search-text-field.js.es6 create mode 100644 app/assets/javascripts/discourse/templates/components/search-result-category.hbs create mode 100644 app/assets/javascripts/discourse/templates/components/search-result-post.hbs create mode 100644 app/assets/javascripts/discourse/templates/components/search-result-topic.hbs create mode 100644 app/assets/javascripts/discourse/templates/components/search-result-user.hbs delete mode 100644 app/assets/javascripts/discourse/templates/search/category_result.hbs delete mode 100644 app/assets/javascripts/discourse/templates/search/post_result.hbs delete mode 100644 app/assets/javascripts/discourse/templates/search/topic_result.hbs delete mode 100644 app/assets/javascripts/discourse/templates/search/user_result.hbs delete mode 100644 app/assets/javascripts/discourse/views/search-results-type.js.es6 delete mode 100644 app/assets/javascripts/discourse/views/search-text-field.js.es6 create mode 100644 test/javascripts/acceptance/search-test.js.es6 diff --git a/app/assets/javascripts/discourse/components/search-result-category.js.es6 b/app/assets/javascripts/discourse/components/search-result-category.js.es6 new file mode 100644 index 00000000000..e23284ed164 --- /dev/null +++ b/app/assets/javascripts/discourse/components/search-result-category.js.es6 @@ -0,0 +1,2 @@ +import SearchResult from 'discourse/components/search-result'; +export default SearchResult.extend(); diff --git a/app/assets/javascripts/discourse/components/search-result-post.js.es6 b/app/assets/javascripts/discourse/components/search-result-post.js.es6 new file mode 100644 index 00000000000..e23284ed164 --- /dev/null +++ b/app/assets/javascripts/discourse/components/search-result-post.js.es6 @@ -0,0 +1,2 @@ +import SearchResult from 'discourse/components/search-result'; +export default SearchResult.extend(); diff --git a/app/assets/javascripts/discourse/components/search-result-topic.js.es6 b/app/assets/javascripts/discourse/components/search-result-topic.js.es6 new file mode 100644 index 00000000000..e23284ed164 --- /dev/null +++ b/app/assets/javascripts/discourse/components/search-result-topic.js.es6 @@ -0,0 +1,2 @@ +import SearchResult from 'discourse/components/search-result'; +export default SearchResult.extend(); diff --git a/app/assets/javascripts/discourse/components/search-result-user.js.es6 b/app/assets/javascripts/discourse/components/search-result-user.js.es6 new file mode 100644 index 00000000000..e23284ed164 --- /dev/null +++ b/app/assets/javascripts/discourse/components/search-result-user.js.es6 @@ -0,0 +1,2 @@ +import SearchResult from 'discourse/components/search-result'; +export default SearchResult.extend(); diff --git a/app/assets/javascripts/discourse/components/search-result.js.es6 b/app/assets/javascripts/discourse/components/search-result.js.es6 new file mode 100644 index 00000000000..dacf1f26965 --- /dev/null +++ b/app/assets/javascripts/discourse/components/search-result.js.es6 @@ -0,0 +1,11 @@ +export default Ember.Component.extend({ + tagName: 'ul', + + _highlightOnInsert: function() { + const term = this.get('controller.term'); + if(!_.isEmpty(term)) { + this.$('.blurb').highlight(term.split(/\s+/), {className: 'search-highlight'}); + this.$('.topic-title').highlight(term.split(/\s+/), {className: 'search-highlight'} ); + } + }.on('didInsertElement') +}); diff --git a/app/assets/javascripts/discourse/components/search-text-field.js.es6 b/app/assets/javascripts/discourse/components/search-text-field.js.es6 new file mode 100644 index 00000000000..e833f661101 --- /dev/null +++ b/app/assets/javascripts/discourse/components/search-text-field.js.es6 @@ -0,0 +1,7 @@ +import TextField from 'discourse/components/text-field'; + +export default TextField.extend({ + placeholder: function() { + return this.get('searchContextEnabled') ? "" : I18n.t('search.title'); + }.property('searchContextEnabled') +}); diff --git a/app/assets/javascripts/discourse/lib/search-for-term.js.es6 b/app/assets/javascripts/discourse/lib/search-for-term.js.es6 index 12e05eb739e..478b1bc6e99 100644 --- a/app/assets/javascripts/discourse/lib/search-for-term.js.es6 +++ b/app/assets/javascripts/discourse/lib/search-for-term.js.es6 @@ -4,7 +4,7 @@ function searchForTerm(term, opts) { if (!opts) opts = {}; // Only include the data we have - var data = { term: term, include_blurbs: 'true' }; + const data = { term: term, include_blurbs: 'true' }; if (opts.typeFilter) data.type_filter = opts.typeFilter; if (opts.searchForId) data.search_for_id = true; @@ -22,7 +22,7 @@ function searchForTerm(term, opts) { if (!results.posts) { results.posts = []; } if (!results.categories) { results.categories = []; } - var topicMap = {}; + const topicMap = {}; results.topics = results.topics.map(function(topic){ topic = Topic.create(topic); topicMap[topic.id] = topic; @@ -44,23 +44,23 @@ function searchForTerm(term, opts) { return Discourse.Category.list().findProperty('id', category.id); }).compact(); - var r = results.grouped_search_result; + const r = results.grouped_search_result; results.resultTypes = []; // TODO: consider refactoring front end to take a better structure [['topic','posts'],['user','users'],['category','categories']].forEach(function(pair){ - var type = pair[0], name = pair[1]; - if(results[name].length > 0) { + const type = pair[0], name = pair[1]; + if (results[name].length > 0) { results.resultTypes.push({ results: results[name], - displayType: (opts.searchContext && opts.searchContext.type === 'topic' && type === 'topic') ? 'post' : type, - type: type, + componentName: "search-result-" + ((opts.searchContext && opts.searchContext.type === 'topic' && type === 'topic') ? 'post' : type), + type, more: r['more_' + name] }); } }); - var noResults = !!(results.topics.length === 0 && + const noResults = !!(results.topics.length === 0 && results.posts.length === 0 && results.users.length === 0 && results.categories.length === 0); diff --git a/app/assets/javascripts/discourse/templates/components/search-result-category.hbs b/app/assets/javascripts/discourse/templates/components/search-result-category.hbs new file mode 100644 index 00000000000..071eba18417 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/search-result-category.hbs @@ -0,0 +1,7 @@ +{{#each results as |result|}} +
  • + + {{category-badge result}} + +
  • +{{/each}} diff --git a/app/assets/javascripts/discourse/templates/components/search-result-post.hbs b/app/assets/javascripts/discourse/templates/components/search-result-post.hbs new file mode 100644 index 00000000000..da9737daf42 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/search-result-post.hbs @@ -0,0 +1,12 @@ +{{#each results as |result|}} + + + {{i18n 'search.post_format' post_number=result.post_number username=result.username}} + + {{#unless site.mobileView}} + + {{{unbound result.blurb}}} + + {{/unless}} + +{{/each}} diff --git a/app/assets/javascripts/discourse/templates/components/search-result-topic.hbs b/app/assets/javascripts/discourse/templates/components/search-result-topic.hbs new file mode 100644 index 00000000000..66e39893871 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/search-result-topic.hbs @@ -0,0 +1,14 @@ +{{#each results as |result|}} +
  • + + + {{topic-status topic=result.topic disableActions=true}}{{unbound result.topic.title}}{{category-badge result.topic.category}} + + {{#unless site.mobileView}} + + {{format-age result.created_at}} - {{{unbound result.blurb}}} + + {{/unless}} + +
  • +{{/each}} diff --git a/app/assets/javascripts/discourse/templates/components/search-result-user.hbs b/app/assets/javascripts/discourse/templates/components/search-result-user.hbs new file mode 100644 index 00000000000..9cf46a1519f --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/search-result-user.hbs @@ -0,0 +1,8 @@ +{{#each results as |result|}} +
  • + + {{avatar result imageSize="small"}} + {{unbound result.username}} + +
  • +{{/each}} diff --git a/app/assets/javascripts/discourse/templates/search.hbs b/app/assets/javascripts/discourse/templates/search.hbs index 6ad3c159554..60bf45870c2 100644 --- a/app/assets/javascripts/discourse/templates/search.hbs +++ b/app/assets/javascripts/discourse/templates/search.hbs @@ -1,4 +1,5 @@ -{{view "search-text-field" value=term searchContextEnabled=searchContextEnabled searchContext=searchContext id="search-term"}} +{{search-text-field value=term searchContextEnabled=searchContextEnabled id="search-term"}} +
    {{#if searchContext}}
    {{else}} - {{#each resultType in content.resultTypes}} + {{#each content.resultTypes as |resultType|}}
    {{#if resultType.more}} diff --git a/app/assets/javascripts/discourse/templates/search/category_result.hbs b/app/assets/javascripts/discourse/templates/search/category_result.hbs deleted file mode 100644 index ad0c40031b0..00000000000 --- a/app/assets/javascripts/discourse/templates/search/category_result.hbs +++ /dev/null @@ -1,3 +0,0 @@ - - {{category-badge this}} - diff --git a/app/assets/javascripts/discourse/templates/search/post_result.hbs b/app/assets/javascripts/discourse/templates/search/post_result.hbs deleted file mode 100644 index 197ba516416..00000000000 --- a/app/assets/javascripts/discourse/templates/search/post_result.hbs +++ /dev/null @@ -1,10 +0,0 @@ - - - {{i18n 'search.post_format' post_number=post_number username=username}} - - {{#unless controller.site.mobileView}} - - {{{unbound blurb}}} - - {{/unless}} - diff --git a/app/assets/javascripts/discourse/templates/search/topic_result.hbs b/app/assets/javascripts/discourse/templates/search/topic_result.hbs deleted file mode 100644 index ef58e870d9e..00000000000 --- a/app/assets/javascripts/discourse/templates/search/topic_result.hbs +++ /dev/null @@ -1,10 +0,0 @@ - - - {{topic-status topic=topic disableActions=true}}{{unbound topic.title}}{{category-badge topic.category}} - - {{#unless controller.site.mobileView}} - - {{format-age created_at}} - {{{unbound blurb}}} - - {{/unless}} - diff --git a/app/assets/javascripts/discourse/templates/search/user_result.hbs b/app/assets/javascripts/discourse/templates/search/user_result.hbs deleted file mode 100644 index 057d2145730..00000000000 --- a/app/assets/javascripts/discourse/templates/search/user_result.hbs +++ /dev/null @@ -1,4 +0,0 @@ - - {{avatar this imageSize="small"}} - {{unbound username}} - diff --git a/app/assets/javascripts/discourse/views/search-results-type.js.es6 b/app/assets/javascripts/discourse/views/search-results-type.js.es6 deleted file mode 100644 index f90de08c1ef..00000000000 --- a/app/assets/javascripts/discourse/views/search-results-type.js.es6 +++ /dev/null @@ -1,15 +0,0 @@ -export default Ember.CollectionView.extend({ - tagName: 'ul', - itemViewClass: Discourse.GroupedView.extend({ - tagName: 'li', - classNameBindings: ['selected'], - templateName: Discourse.computed.fmt('parentView.displayType', "search/%@_result") - }), - didInsertElement: function(){ - var term = this.get('controller.term'); - if(!_.isEmpty(term)) { - this.$('.blurb').highlight(term.split(/\s+/), {className: 'search-highlight'}); - this.$('.topic-title').highlight(term.split(/\s+/), {className: 'search-highlight'} ); - } - } -}); diff --git a/app/assets/javascripts/discourse/views/search-text-field.js.es6 b/app/assets/javascripts/discourse/views/search-text-field.js.es6 deleted file mode 100644 index 4db554a7dec..00000000000 --- a/app/assets/javascripts/discourse/views/search-text-field.js.es6 +++ /dev/null @@ -1,27 +0,0 @@ -/** - This is a text field that supports a dynamic placeholder based on search context. - - @class SearchTextField - @extends Discourse.TextField - @namespace Discourse - @module Discourse -**/ - -import TextField from 'discourse/components/text-field'; - -export default TextField.extend({ - - /** - A dynamic placeholder for the search field based on our context - - @property placeholder - **/ - placeholder: function() { - - if(this.get('searchContextEnabled')){ - return ""; - } - - return I18n.t('search.title'); - }.property('searchContextEnabled') -}); diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index 9a3b8c092d3..83e3490976d 100644 --- a/app/assets/javascripts/main_include.js +++ b/app/assets/javascripts/main_include.js @@ -45,6 +45,7 @@ //= require ./discourse/views/cloaked //= require ./discourse/components/combo-box //= require ./discourse/views/button +//= require ./discourse/components/search-result //= require ./discourse/components/dropdown-button //= require ./discourse/components/notifications-button //= require ./discourse/components/topic-notifications-button diff --git a/test/javascripts/acceptance/search-test.js.es6 b/test/javascripts/acceptance/search-test.js.es6 new file mode 100644 index 00000000000..702cb1dde06 --- /dev/null +++ b/test/javascripts/acceptance/search-test.js.es6 @@ -0,0 +1,18 @@ +import { acceptance } from "helpers/qunit-helpers"; +acceptance("Search"); + +test("search", (assert) => { + visit("/"); + + click('#search-button'); + + andThen(() => { + assert.ok(exists('#search-term'), 'it shows the search bar'); + assert.ok(!exists('#search-dropdown .results ul li'), 'no results by default'); + }); + + fillIn('#search-term', 'dev'); + andThen(() => { + assert.ok(exists('#search-dropdown .results ul li'), 'it shows results'); + }); +}); diff --git a/test/javascripts/fixtures/search-fixtures.js.es6 b/test/javascripts/fixtures/search-fixtures.js.es6 index 73e405b5203..b9f30c9fded 100644 --- a/test/javascripts/fixtures/search-fixtures.js.es6 +++ b/test/javascripts/fixtures/search-fixtures.js.es6 @@ -1 +1,666 @@ -export default {"/search": {"posts":[{"id":61693,"name":"Manoel Lemos","username":"mlemos","avatar_template":"/letter_avatar/mlemos/{size}/2.png","uploaded_avatar_id":null,"created_at":"2014-07-15T21:53:56.337-04:00","cooked":"

    Gents, I'm using an very interesting tool to drive traffic across the different destinations of my community. One destination is the discussion forum running Discourse and other is the blog running WordPress.

    \n\n

    This tool is called Hello Bar and it is basically a bar that stays in the top of the site and links to other places or can be used to collect emails or social networks followers. It worked fine when I added the bar to my Wordpress Blog, but it failed (a bit) when added to my Discourse forum.

    \n\n

    You can check the two situations here:

    \n\n\n\n

    The error is that the Discourse page was suposed to be pushed down by the Hello Bar, but it isn't being fully pushed down. Then the layout is broken.

    \n\n

    Can anyone help me with this?

    ","post_number":1,"post_type":1,"updated_at":"2014-07-15T21:53:56.337-04:00","reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":30,"incoming_link_count":2,"reads":40,"score":19.5,"yours":false,"topic_slug":null,"topic_id":17638,"display_username":"Manoel Lemos","primary_group_name":null,"version":2,"can_edit":false,"can_delete":false,"can_recover":false,"user_title":null,"actions_summary":[{"id":2,"count":0,"hidden":false,"can_act":null},{"id":3,"count":0,"hidden":false,"can_act":null},{"id":4,"count":0,"hidden":false,"can_act":null},{"id":5,"count":0,"hidden":true,"can_act":null},{"id":6,"count":0,"hidden":false,"can_act":null},{"id":7,"count":0,"hidden":false,"can_act":null},{"id":8,"count":0,"hidden":false,"can_act":null}],"moderator":false,"admin":false,"staff":false,"user_id":7595,"hidden":false,"hidden_reason_id":null,"trust_level":1,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false,"blurb":"...the discussion forum running Discourse and other is the blog running WordPress. This tool is called Hello Bar and it is basically a bar that stays in the top of the site and links to other places or can be..."},{"id":56514,"name":"Ova Light","username":"ChrisOva","avatar_template":"/letter_avatar/chrisova/{size}/2.png","uploaded_avatar_id":null,"created_at":"2014-06-13T02:56:00.794-04:00","cooked":"

    Do you know what are the elements whichI need to modify in the custom CSS editor in order for these parts of the forum to change color ?

    \n\n

    \n\n

    Is anyone here willing to work for a theme? (50$)

    \n\n

    Thanks for reading !

    ","post_number":1,"post_type":1,"updated_at":"2014-06-13T03:05:31.865-04:00","reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":24,"incoming_link_count":1,"reads":31,"score":57.4,"yours":false,"topic_slug":null,"topic_id":16504,"display_username":"Ova Light","primary_group_name":null,"version":5,"can_edit":false,"can_delete":false,"can_recover":false,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":null},{"id":3,"count":0,"hidden":false,"can_act":null},{"id":4,"count":0,"hidden":false,"can_act":null},{"id":5,"count":0,"hidden":true,"can_act":null},{"id":6,"count":0,"hidden":false,"can_act":null},{"id":7,"count":0,"hidden":false,"can_act":null},{"id":8,"count":0,"hidden":false,"can_act":null}],"moderator":false,"admin":false,"staff":false,"user_id":10477,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":"downloaded local copies of images","can_view_edit_history":true,"wiki":false,"blurb":"Do you know what are the elements whichI need to modify in the custom CSS editor in order for these parts of the forum to change color ? 00000674.png 00000674.png 2644x932 239 KB Is anyone here wil..."},{"id":40862,"name":"Moe","username":"Moe","avatar_template":"/letter_avatar/moe/{size}/2.png","uploaded_avatar_id":null,"created_at":"2014-02-14T18:35:48.395-05:00","cooked":"

    Hello,

    \n\n

    First and foremost, thanks to the developers of Discourse and everyone who is making great efforts in creating an awesome forum!

    \n\n

    I am a Discouse newbie and I am trying to do sso using the CAS sso auth plugin in https://github.com/eriko/cas_sso .

    \n\n

    I am currently making some experiments in authenticating users in Discourse using a test web app I am using as CAS server. I have set up discourse in a VM (vagrant) whereas the CAS server runs in the host machine. I have installed the sso plugin and set the values as indicated (i.e. cas_sso_host, cas_sso_port, etc, as it is a non-standard CAS server), with ssh disabled.

    \n\n

    One thing that I noticed is that it seems that there seems to be a cross domain protection acting, the following is a message that appears in the console when I go to login in vagrant discourse:

    \n\n

    Started GET \"/session/csrf\" for 10.0.2.2 at 2014-02-14 17:53:12 -0500\nProcessing by SessionController#csrf as */*\nCompleted 200 OK in 1ms (Views: 0.2ms | ActiveRecord: 0.0ms)
    \n\n

    Here 10.0.2.2 is the ip of the CAS server. This server is accessible from vagrant, I tested access to the CAS server URLs using lynx (everything works fine). I should likely mention also that cas_sso_login_url is set to /login and cas_sso_path is set to /cas in the Discourse sso plugin options. I have been also monitoring requests to /cas/login in the CAS server and none of my login attempts from Discourse have made it there.

    \n\n

    I am brand new to Discourse and Ruby, so I am having a bit of a struggle here and any insight would be greatly appreciated.

    \n\n

    Thank you very much in advance for your time and excellent disposition.

    \n\n

    Best regards,
    Moe

    ","post_number":1,"post_type":1,"updated_at":"2014-02-14T18:35:48.395-05:00","reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":20,"incoming_link_count":60,"reads":57,"score":357.4,"yours":false,"topic_slug":null,"topic_id":12731,"display_username":"Moe","primary_group_name":null,"version":2,"can_edit":false,"can_delete":false,"can_recover":false,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":null},{"id":3,"count":0,"hidden":false,"can_act":null},{"id":4,"count":0,"hidden":false,"can_act":null},{"id":5,"count":0,"hidden":true,"can_act":null},{"id":6,"count":0,"hidden":false,"can_act":null},{"id":7,"count":0,"hidden":false,"can_act":null},{"id":8,"count":0,"hidden":false,"can_act":null}],"moderator":false,"admin":false,"staff":false,"user_id":8596,"hidden":false,"hidden_reason_id":null,"trust_level":1,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false,"blurb":"Hello, First and foremost, thanks to the developers of Discourse and everyone who is making great efforts..."},{"id":18691,"name":"Sam Saffron","username":"sam","avatar_template":"/letter_avatar/sam/{size}/2.png","uploaded_avatar_id":null,"created_at":"2013-05-30T21:17:09.213-04:00","cooked":"

    For the feature I was working on yesterday, @codinghorror wanted a rather complex sentence.

    \n\n

    There is 1 unread and 9 new topics remaining, or browse other topics in [category]
    \n\n

    This seemingly simple sentence was a royal nightmare to localize with our existing localization system. Think through all the permutations:

    \n\n

    \"There are 2 unread and 9 new topics remaining, or browse other topics in [category]\"
    \"There are 2 unread and 1 new topic remaining, or browse other topics in [category]\"
    \"There is 1 unread and 1 new topic remaining, or browse other topics in [category]\"

    \n\n

    Trouble with our current system was that you have no sane way of building these kind of sentences, see: http://stackoverflow.com/questions/16825932/clean-pattern-for-localizing-sentences-in-rails-i18n , you can only easily localize one count in a non compound sentence.

    \n\n

    To alleviate this I introduce a new mechanism that is available (optionally) client side. The above sentence is localized using:

    \n\n

    There {UNREAD, plural, \n   one {is <a href='/unread'>1 unread</a>} \n   other {are <a href='/unread'># unread</a>}\n} and {NEW, plural, \n  one {<a href='/new'>1 new</a> topic} \n  other {<a href='/new'># new</a> topics}} remaining, or browse other topics in {catLink}
    \n\n

    The client localization file has a special rule, if a key ends with _MF it is interpreted as a MessageFormat message, then to access it on the client you use:

    \n\n

    I18n.messageFormat(\"topic.read_more_in_category_MF\", {\"UNREAD\": unreadTopics, \"NEW\": newTopics, catLink: opts.catLink})
    \n\n

    You can see a few other examples here:

    \n\n

    \n\n

    We do not plan at the moment to move to message format style localization everywhere, however it is nice to have this extra bit of flexibility that lets us generate interesting sentences.

    \n\n

    On a technical note, this feature adds almost no weight to the client side JavaScript, all message format strings are pre-compiled into a JavaScript function with no external dependencies. The tricks used can be viewed here: https://github.com/discourse/discourse/blob/master/lib/js_locale_helper.rb

    \n\n

    1 minute Message Format primer

    \n\n

    f = \"hello\"\nf() => \"hello\"\n\nf = \"hello {WORLD}\"\nf(WORLD: \"world\") => \"hello world\" \nf(WORLD: \"other world\") => \"hello other world\" \n\nf = \"I have {HATS, plural, one {one hat} other {# hats}}\"\nf(HATS: 1) => \"I have one hat\"\nf(HATS: 10) => \"I have 10 hats\" \n\nf = \"I am a {GENDER, select, male {boy}, female {girl}}\"\nf(GENDER: \"male\") => \"I am a boy\"\nf(GENDER: \"female\") => \"I am a girl\"
    \n\n

    Our plan for now is to use this strategically, however it is worth noting that this gives more flexibility in localization, for example in czech, the plural form is rather interesting as @kuba could attest :

    \n\n

    MessageFormat.locale.cs = function (n) {\n  if (n == 1) {\n    return 'one';\n  }\n  if (n == 2 || n == 3 || n == 4) {\n    return 'few';\n  }\n  return 'other';\n};
    \n\n

    Message Format supports this fine, built in.

    \n\n

    f = \"I have {HATS, plural, one {one hat} other {# hats} few {# few hats}}\"
    ","post_number":1,"post_type":1,"updated_at":"2013-05-30T21:31:57.135-04:00","reply_count":1,"reply_to_post_number":null,"quote_count":0,"avg_time":33,"incoming_link_count":44,"reads":68,"score":450.25,"yours":false,"topic_slug":null,"topic_id":7035,"display_username":"Sam Saffron","primary_group_name":"discourse","version":3,"can_edit":false,"can_delete":false,"can_recover":false,"user_title":"co-founder","actions_summary":[{"id":2,"count":6,"hidden":false,"can_act":null},{"id":3,"count":0,"hidden":false,"can_act":null},{"id":4,"count":0,"hidden":false,"can_act":null},{"id":5,"count":0,"hidden":true,"can_act":null},{"id":6,"count":0,"hidden":false,"can_act":null},{"id":7,"count":0,"hidden":false,"can_act":null},{"id":8,"count":0,"hidden":false,"can_act":null}],"moderator":true,"admin":true,"staff":true,"user_id":1,"hidden":false,"hidden_reason_id":null,"trust_level":3,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false,"blurb":"...hub.com/discourse/discourse/blob/master/lib/js_locale_helper.rb 1 minute Message Format primer f = \"hello\" f() = > \"hello\" f = \"hello {WORLD}\" f(WORLD: \"world\") = > \"hello world\" f(WORLD: \"other world\") =..."},{"id":41969,"name":"Sam Saffron","username":"sam","avatar_template":"/letter_avatar/sam/{size}/2.png","uploaded_avatar_id":null,"created_at":"2014-02-25T03:30:34.423-05:00","cooked":"

    Discourse now ships with official hooks to perform auth offsite.

    \n\n

    The Problem

    \n\n

    Many sites wish to integrate with a Discourse site, however want to keep all user registration in a separate site. In such a setup all Login operations should be outsourced to a different site.

    \n\n

    What if I would like SSO in conjunction with existing auth?

    \n\n

    The intention around SSO is to replace Discourse authentication, if you would like to add a new provider see existing plugins such as: https://meta.discourse.org/t/vk-com-login-vkontakte/12987

    \n\n

    Enabling SSO

    \n\n

    To enable single sign on you have 3 settings you need to fill out:

    \n\n

    \n\n

    enable_sso : must be enabled, global switch
    sso_url: the offsite URL users will be sent to when attempting to log on
    sso_secret: a secret string used to hash SSO payloads. Ensures payloads are authentic.

    \n\n

    Once enable_sso is set to true:

    \n\n\n\n

    What if you check it by mistake?

    \n\n

    If you check enable_sso by mistake and need to revert to the original state and no longer have access to the admin panel

    \n\n

    run:

    \n\n

    ./launcher enter app\nrails c\nirb > SiteSetting.enable_sso = false\nirb > exit\nexit
    \n\n

    Implementing SSO on your site

    \n\n

    Discourse will redirect clients to sso_url with a signed payload: (say sso_url is https://somesite.com/sso)

    \n\n

    You will receive incoming traffic with the following

    \n\n

    https://somesite.com/sso?sso=PAYLOAD&sig=SIG

    \n\n

    The payload is a Base64 encoded string comprising of a nonce. The payload is always a valid querystring.

    \n\n

    For example, if the nonce is ABCD. raw_payload will be:

    \n\n

    nonce=ABCD, this raw payload is base 64 encoded.

    \n\n

    The endpoint being called must

    \n\n
      \n
    1. Validate the signature, ensure that HMAC-SHA256 of sso_secret, PAYLOAD is equal to the sig
    2. \n
    3. Perform whatever authentication it has to
    4. \n
    5. Create a new payload with nonce, email, external_id and optionally (username, name)
    6. \n
    7. Base64 encode the payload
    8. \n
    9. Calculate a HMAC-SHA256 hash of the using sso_secret as the key and Base64 encoded payload as text
    10. \n
    11. Redirect back to http://discourse_site/session/sso_login?sso=payload&sig=sig\n
    12. \n
    \n\n

    Discourse will validate that the nonce is valid (if valid it will expire it right away so it can no longer be used) it will attempt to:

    \n\n
      \n
    1. Log the user on by looking up an already associated external_id in the SingleSignOnRecord model
    2. \n
    3. Log the user on by using the email provided (updating external_id)
    4. \n
    5. Create a new account for the user providing (email, username, name) updating external_id
    6. \n
    \n\n

    Security concerns

    \n\n

    The nonce (one time token) will expire automatically after 10 minutes. This means that as soon as the user is redirected to your site they have 10 minutes to log in / create a new account.

    \n\n

    The protocol is safe against replay attacks as nonce may only be used once.

    \n\n

    Reference implementation

    \n\n

    Discourse contains a reference implementation of the SSO class:

    \n\n\n\n\n

    A trivial implementation would be:

    \n\n

    class DiscourseSsoController < ApplicationController\n  def sso\n    secret = \"MY_SECRET_STRING\"\n    sso = SingleSignOn.parse(request.query_string, secret)\n    sso.email = \"user@email.com\"\n    sso.name = \"Bill Hicks\"\n    sso.username = \"bill@hicks.com\"\n    sso.external_id = \"123\" # unique to your application\n    sso.sso_secret = secret\n\n    redirect_to sso.to_url(\"http://l.discourse/session/sso_login\")\n  end\nend
    \n\n

    Transitioning to and from single sign on.

    \n\n

    The system always trusts emails provided by the single sign on endpoint. This means that if you had an existing account in the past on Discourse with SSO disabled, SSO will simply re-use it and avoid creating a new account.

    \n\n

    If you ever turn off SSO, users will be able to reset passwords and gain access back to their accounts.

    \n\n

    Real world example:

    \n\n

    Given the following settings:

    \n\n

    Discourse domain: http://discuss.example.com
    SSO url : http://www.example.com/discourse/sso
    SSO secret: d836444a9e4084d5b224a60c208dce14

    \n\n

    User attempt to login

    \n\n\n\n

    Finally browser is redirected to:

    \n\n

    http://www.example.com/discourse/sso?sso=bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGI%3D%0A&sig=2828aa29899722b35a2f191d34ef9b3ce695e0e6eeec47deb46d588d70c7cb56

    \n\n

    On the other end

    \n\n
      \n
    1. Payload is validated using HMAC-SHA256, if the sig mismatches, process aborts.
    2. \n
    3. By reversing the steps above nonce is extracted.
    4. \n
    \n\n

    User logs in:

    \n\n

    name: sam\nexternal_id: hello123\nemail: test@test.com\nusername: samsam
    \n\n\n\n

    nonce=cb68251eefb5211e58c00ff1395f0c0b&name=sam&username=samsam&email=test%40test.com&external_id=hello123

    \n\n

    order does not matter, values are URL encoded

    \n\n\n\n

    \"bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGImbmFtZT1z\\nYW0mdXNlcm5hbWU9c2Ftc2FtJmVtYWlsPXRlc3QlNDB0ZXN0LmNvbSZleHRl\\ncm5hbF9pZD1oZWxsbzEyMw==\\n

    \n\n\n\n

    bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGImbmFtZT1z%0AYW0mdXNlcm5hbWU9c2Ftc2FtJmVtYWlsPXRlc3QlNDB0ZXN0LmNvbSZleHRl%0Acm5hbF9pZD1oZWxsbzEyMw%3D%3D%0A

    \n\n\n\n

    1c884222282f3feacd76802a9dd94e8bc8deba5d619b292bed75d63eb3152c0b

    \n\n\n\n

    http://discuss.example.com/session/sso_login?sso=bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGImbmFtZT1z%0AYW0mdXNlcm5hbWU9c2Ftc2FtJmVtYWlsPXRlc3QlNDB0ZXN0LmNvbSZleHRl%0Acm5hbF9pZD1oZWxsbzEyMw%3D%3D%0A&sig=1c884222282f3feacd76802a9dd94e8bc8deba5d619b292bed75d63eb3152c0b

    \n\n

    Future work

    \n\n\n\n

    Advanced Features

    \n\n\n\n

    Updates:

    \n\n

    2-Feb-2014

    \n\n\n\n

    4-April-2014

    \n\n\n\n

    24-April-2014

    \n\n\n\n

    01-August-2014

    \n\n","post_number":1,"post_type":1,"updated_at":"2014-08-01T17:44:20.164-04:00","reply_count":5,"reply_to_post_number":null,"quote_count":0,"avg_time":41,"incoming_link_count":2284,"reads":536,"score":12367.25,"yours":false,"topic_slug":null,"topic_id":13045,"display_username":"Sam Saffron","primary_group_name":"discourse","version":12,"can_edit":false,"can_delete":false,"can_recover":false,"user_title":"co-founder","actions_summary":[{"id":2,"count":41,"hidden":false,"can_act":null},{"id":3,"count":0,"hidden":false,"can_act":null},{"id":4,"count":0,"hidden":false,"can_act":null},{"id":5,"count":0,"hidden":true,"can_act":null},{"id":6,"count":0,"hidden":false,"can_act":null},{"id":7,"count":0,"hidden":false,"can_act":null},{"id":8,"count":0,"hidden":false,"can_act":null}],"moderator":true,"admin":true,"staff":true,"user_id":1,"hidden":false,"hidden_reason_id":null,"trust_level":3,"deleted_at":null,"user_deleted":false,"edit_reason":"","can_view_edit_history":true,"wiki":false,"blurb":"...ocess aborts. By reversing the steps above nonce is extracted. User logs in: name: sam external_id: hello123 email: test@test.com username: samsam Unsigned payload is generated: nonce=cb68251eefb5211e58c00..."}],"topics":[{"id":17638,"title":"Hello Bar integration issues","fancy_title":"Hello Bar integration issues","slug":"hello-bar-integration-issues","posts_count":5,"reply_count":2,"highest_post_number":5,"image_url":null,"created_at":"2014-07-15T21:53:56.226-04:00","last_posted_at":"2014-07-15T22:51:01.719-04:00","bumped":true,"bumped_at":"2014-07-15T22:01:39.716-04:00","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":true,"archived":false,"views":84,"like_count":1,"has_summary":false,"archetype":"regular","last_poster_username":null,"category_id":6,"posters":[]},{"id":16504,"title":"Hello, I have two questions :D","fancy_title":"Hello, I have two questions :D","slug":"hello-i-have-two-questions-d","posts_count":2,"reply_count":0,"highest_post_number":2,"image_url":"/uploads/default/_optimized/b35/289/b2338e0876_690x243.png","created_at":"2014-06-13T02:56:00.695-04:00","last_posted_at":"2014-06-13T06:27:28.903-04:00","bumped":true,"bumped_at":"2014-06-13T06:27:28.903-04:00","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"views":97,"like_count":3,"has_summary":false,"archetype":"regular","last_poster_username":null,"category_id":14,"posters":[]},{"id":12731,"title":"CAS sso auth plugin question","fancy_title":"CAS sso auth plugin question","slug":"cas-sso-auth-plugin-question","posts_count":15,"reply_count":5,"highest_post_number":15,"image_url":null,"created_at":"2014-02-14T18:35:48.242-05:00","last_posted_at":"2014-02-25T20:50:16.306-05:00","bumped":true,"bumped_at":"2014-02-25T20:50:16.306-05:00","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"views":440,"like_count":2,"has_summary":false,"archetype":"regular","last_poster_username":null,"category_id":5,"posters":[]},{"id":7035,"title":"Message Format support for localization","fancy_title":"Message Format support for localization","slug":"message-format-support-for-localization","posts_count":7,"reply_count":5,"highest_post_number":7,"image_url":"http://meta.discourse.org/assets/favicons/github-65cd2c8ba8283c55eca7f9e257fa7604.png","created_at":"2013-05-30T21:17:08.971-04:00","last_posted_at":"2014-09-03T03:11:36.653-04:00","bumped":true,"bumped_at":"2014-09-03T03:11:36.653-04:00","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"views":699,"like_count":10,"has_summary":false,"archetype":"regular","last_poster_username":null,"category_id":2,"posters":[]},{"id":13045,"title":"Official Single-Sign-On for Discourse","fancy_title":"Official Single-Sign-On for Discourse","slug":"official-single-sign-on-for-discourse","posts_count":61,"reply_count":37,"highest_post_number":64,"image_url":"/uploads/default/_optimized/07c/3bf/3fa1d69ceb_690x207.png","created_at":"2014-02-25T03:30:34.321-05:00","last_posted_at":"2014-08-01T17:44:56.523-04:00","bumped":true,"bumped_at":"2014-08-07T13:27:14.684-04:00","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"views":13377,"like_count":74,"has_summary":true,"archetype":"regular","last_poster_username":null,"category_id":10,"posters":[]}],"users":[{"id":3836,"username":"HelloWorld","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/helloworld/{size}/2.png"},{"id":6315,"username":"instagra","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/instagra/{size}/2.png"},{"id":1743,"username":"hello_jmk","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/hello_jmk/{size}/2.png"},{"id":9349,"username":"hellocate","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/hellocate/{size}/2.png"},{"id":10596,"username":"hellooperator","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/hellooperator/{size}/2.png"}],"categories":[],"grouped_search_result":{"more_posts":true,"more_users":null,"more_categories":null,"post_ids":[61693,56514,40862,18691,41969],"user_ids":[3836,6315,1743,9349,10596],"category_ids":[]}}}; +export default { + "search/query": { +   "posts": [ +     { +       "id": 3833, +       "name": "Bill Dudney", +       "username": "bdudney", +       "avatar_template": "/user_avatar/meta.discourse.org/bdudney/{size}/8343_1.png", +       "uploaded_avatar_id": 8343, +       "created_at": "2013-02-07T17:46:57.469Z", +       "cooked": "

    I've gotten vagrant up and running with a development environment but it's taking forever to load.<\/p>\n\n

    For example http://192.168.10.200:3000/<\/a> takes tens of seconds to load.<\/p>\n\n

    I'm running the whole stack on a new rMBP with OS X 10.8.2.<\/p>\n\n

    Any ideas of what I've done wrong? Or is this just a function of being on the bleeding edge?<\/p>\n\n

    Thanks,<\/p>\n\n

    -bd<\/p>", +       "post_number": 1, +       "post_type": 1, +       "updated_at": "2013-02-07T17:46:57.469Z", +       "like_count": 0, +       "reply_count": 1, +       "reply_to_post_number": null, +       "quote_count": 0, +       "avg_time": 24, +       "incoming_link_count": 4422, +       "reads": 327, +       "score": 21978.4, +       "yours": false, +       "topic_id": 2179, +       "topic_slug": "development-mode-super-slow", +       "display_username": "Bill Dudney", +       "primary_group_name": null, +       "version": 2, +       "can_edit": false, +       "can_delete": false, +       "can_recover": false, +       "user_title": null, +       "actions_summary": [ +         { +           "id": 2, +           "count": 0, +           "hidden": false, +           "can_act": false +         }, +         { +           "id": 3, +           "count": 0, +           "hidden": false, +           "can_act": false +         }, +         { +           "id": 4, +           "count": 0, +           "hidden": false, +           "can_act": false +         }, +         { +           "id": 5, +           "count": 0, +           "hidden": true, +           "can_act": false +         }, +         { +           "id": 6, +           "count": 0, +           "hidden": false, +           "can_act": false +         }, +         { +           "id": 7, +           "count": 0, +           "hidden": false, +           "can_act": false +         }, +         { +           "id": 8, +           "count": 0, +           "hidden": false, +           "can_act": false +         } +       ], +       "moderator": false, +       "admin": false, +       "staff": false, +       "user_id": 1828, +       "hidden": false, +       "hidden_reason_id": null, +       "trust_level": 1, +       "deleted_at": null, +       "user_deleted": false, +       "edit_reason": null, +       "can_view_edit_history": true, +       "wiki": false, +       "blurb": "I've gotten vagrant up and running with a development environment but it's taking forever to load. For example http://192.168.10.200:3000/ takes..." +     }, +     { +       "id": 48887, +       "name": "Arpit Jalan", +       "username": "techAPJ", +       "avatar_template": "/user_avatar/meta.discourse.org/techapj/{size}/3281_1.png", +       "uploaded_avatar_id": 3281, +       "created_at": "2014-04-12T22:22:07.930Z", +       "cooked": "

    So you want to set up Discourse on Ubuntu to hack on and develop with?<\/p>\n\n

    We'll assume that you don't have Ruby/Rails/Postgre/Redis installed on your Ubuntu system. Let's begin!<\/p>\n\n

    Although this guide assumes that you are using Ubuntu, but the set-up instructions will work fine for any Debian based ditribution.<\/em><\/p>\n\n

    (If you want to install Discourse for production use, see our install guide<\/a>)<\/em><\/p>\n\n

    Install Discourse Dependencies<\/h2>\n\n

    Run this script<\/a> in terminal, to setup Rails development environment:<\/p>\n\n

    bash <(wget -qO- https://raw.githubusercontent.com/techAPJ/install-rails/master/linux)<\/code><\/pre>\n\n

    \nlinux_script.png<\/span>770x211 9.62 KB<\/span><\/span>\n<\/div><\/a><\/div> <\/p>\n\n

    This will install following new packages on your system:<\/p>\n\n