User Profile enhancements:
- Added PreloadStore support to avoid duplicate requests - preliminary SEO - Support for opengraph/twitter cards
This commit is contained in:
parent
0af114aff5
commit
d1d4530efd
|
@ -274,34 +274,35 @@ Discourse.User.reopenClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
Find a user by username
|
||||||
|
|
||||||
|
@method find
|
||||||
|
@param {String} username the username of the user we want to find
|
||||||
|
**/
|
||||||
find: function(username) {
|
find: function(username) {
|
||||||
var promise,
|
|
||||||
_this = this;
|
// Check the preload store first
|
||||||
promise = new RSVP.Promise();
|
return PreloadStore.get("user_" + username, function() {
|
||||||
$.ajax({
|
return $.ajax({ url: "/users/" + username + '.json' });
|
||||||
url: "/users/" + username + '.json',
|
}).then(function (json) {
|
||||||
success: function(json) {
|
|
||||||
// todo: decompose to object
|
// Create a user from the resulting JSON
|
||||||
var user;
|
json.user.stats = Discourse.User.groupStats(json.user.stats.map(function(s) {
|
||||||
json.user.stats = _this.groupStats(json.user.stats.map(function(s) {
|
var stat = Em.Object.create(s);
|
||||||
var obj;
|
stat.set('isPM', stat.get('action_type') === Discourse.UserAction.NEW_PRIVATE_MESSAGE ||
|
||||||
obj = Em.Object.create(s);
|
stat.get('action_type') === Discourse.UserAction.GOT_PRIVATE_MESSAGE);
|
||||||
obj.isPM = obj.action_type === Discourse.UserAction.NEW_PRIVATE_MESSAGE || obj.action_type === Discourse.UserAction.GOT_PRIVATE_MESSAGE;
|
return stat;
|
||||||
return obj;
|
}));
|
||||||
|
|
||||||
|
if (json.user.stream) {
|
||||||
|
json.user.stream = Discourse.UserAction.collapseStream(json.user.stream.map(function(ua) {
|
||||||
|
return Discourse.UserAction.create(ua);
|
||||||
}));
|
}));
|
||||||
if (json.user.stream) {
|
|
||||||
json.user.stream = Discourse.UserAction.collapseStream(json.user.stream.map(function(ua) {
|
|
||||||
return Discourse.UserAction.create(ua);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
user = Discourse.User.create(json.user);
|
|
||||||
return promise.resolve(user);
|
|
||||||
},
|
|
||||||
error: function(xhr) {
|
|
||||||
return promise.reject(xhr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Discourse.User.create(json.user);
|
||||||
});
|
});
|
||||||
return promise;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
createAccount: function(name, email, password, username, passwordConfirm, challenge) {
|
createAccount: function(name, email, password, username, passwordConfirm, challenge) {
|
||||||
|
|
|
@ -1,23 +1,45 @@
|
||||||
/* We can insert data into the PreloadStore when the document is loaded.
|
|
||||||
The data can be accessed once by a key, after which it is removed */
|
/**
|
||||||
|
We can insert data into the PreloadStore when the document is loaded.
|
||||||
|
The data can be accessed once by a key, after which it is removed
|
||||||
|
|
||||||
|
@class PreloadStore
|
||||||
|
**/
|
||||||
PreloadStore = {
|
PreloadStore = {
|
||||||
data: {},
|
data: {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
Store an object in the store
|
||||||
|
|
||||||
|
@method store
|
||||||
|
@param {String} key the key to store the object with
|
||||||
|
@param {String} value the object we're inserting into the store
|
||||||
|
**/
|
||||||
store: function(key, value) {
|
store: function(key, value) {
|
||||||
this.data[key] = value;
|
this.data[key] = value;
|
||||||
},
|
},
|
||||||
/* To retrieve a key, you provide the key you want, plus a finder to
|
|
||||||
load it if the key cannot be found. Once the key is used once, it is
|
/**
|
||||||
removed from the store. So, for example, you can't load a preloaded topic
|
To retrieve a key, you provide the key you want, plus a finder to
|
||||||
more than once. */
|
load it if the key cannot be found. Once the key is used once, it is
|
||||||
|
removed from the store. So, for example, you can't load a preloaded topic
|
||||||
|
more than once.
|
||||||
|
|
||||||
|
@method get
|
||||||
|
@param {String} key the key to look up the object with
|
||||||
|
@param {function} finder a function to find the object with
|
||||||
|
@returns {Promise} a promise that will eventually be the object we want.
|
||||||
|
**/
|
||||||
get: function(key, finder) {
|
get: function(key, finder) {
|
||||||
var promise, result;
|
var promise = new RSVP.Promise();
|
||||||
promise = new RSVP.Promise();
|
|
||||||
if (this.data[key]) {
|
if (this.data[key]) {
|
||||||
promise.resolve(this.data[key]);
|
promise.resolve(this.data[key]);
|
||||||
delete this.data[key];
|
delete this.data[key];
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
if (finder) {
|
if (finder) {
|
||||||
result = finder();
|
var result = finder();
|
||||||
|
|
||||||
// If the finder returns a promise, we support that too
|
// If the finder returns a promise, we support that too
|
||||||
if (result.then) {
|
if (result.then) {
|
||||||
|
@ -30,22 +52,35 @@ PreloadStore = {
|
||||||
promise.resolve(result);
|
promise.resolve(result);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
promise.resolve(void 0);
|
promise.resolve(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return promise;
|
return promise;
|
||||||
},
|
},
|
||||||
/* Does the store contain a particular key? Does not delete, just returns
|
|
||||||
true or false. */
|
/**
|
||||||
|
Does the store contain a particular key? Does not delete.
|
||||||
|
|
||||||
|
@method contains
|
||||||
|
@param {String} key the key to look up the object with
|
||||||
|
@returns {Boolean} whether the object exists
|
||||||
|
**/
|
||||||
contains: function(key) {
|
contains: function(key) {
|
||||||
return this.data[key] !== void 0;
|
return this.data[key] !== void 0;
|
||||||
},
|
},
|
||||||
/* If we are sure it's preloaded, we don't have to supply a finder. Just
|
|
||||||
returns undefined if it's not in the store. */
|
/**
|
||||||
|
If we are sure it's preloaded, we don't have to supply a finder. Just returns
|
||||||
|
undefined if it's not in the store.
|
||||||
|
|
||||||
|
@method getStatic
|
||||||
|
@param {String} key the key to look up the object with
|
||||||
|
@returns {Object} the object from the store
|
||||||
|
**/
|
||||||
getStatic: function(key) {
|
getStatic: function(key) {
|
||||||
var result;
|
var result = this.data[key];
|
||||||
result = this.data[key];
|
|
||||||
delete this.data[key];
|
delete this.data[key];
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,7 +2,7 @@ require_dependency 'discourse_hub'
|
||||||
|
|
||||||
class UsersController < ApplicationController
|
class UsersController < ApplicationController
|
||||||
|
|
||||||
skip_before_filter :check_xhr, only: [:password_reset, :update, :activate_account, :avatar, :authorize_email, :user_preferences_redirect]
|
skip_before_filter :check_xhr, only: [:show, :password_reset, :update, :activate_account, :avatar, :authorize_email, :user_preferences_redirect]
|
||||||
skip_before_filter :authorize_mini_profiler, only: [:avatar]
|
skip_before_filter :authorize_mini_profiler, only: [:avatar]
|
||||||
skip_before_filter :check_restricted_access, only: [:avatar]
|
skip_before_filter :check_restricted_access, only: [:avatar]
|
||||||
|
|
||||||
|
@ -10,8 +10,15 @@ class UsersController < ApplicationController
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@user = fetch_user_from_params
|
@user = fetch_user_from_params
|
||||||
anonymous_etag(@user) do
|
user_serializer = UserSerializer.new(@user, scope: guardian, root: 'user')
|
||||||
render_serialized(@user, UserSerializer)
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
|
store_preloaded("user_#{@user.username}", MultiJson.dump(user_serializer))
|
||||||
|
end
|
||||||
|
|
||||||
|
format.json do
|
||||||
|
render_json_dump(user_serializer)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -32,23 +32,23 @@ module ApplicationHelper
|
||||||
current_user.try(:admin?)
|
current_user.try(:admin?)
|
||||||
end
|
end
|
||||||
|
|
||||||
def crawlable_meta_data(url, title, description)
|
# Creates open graph and twitter card meta data
|
||||||
# Image to supply as meta data
|
def crawlable_meta_data(opts=nil)
|
||||||
image = "#{Discourse.base_url}#{SiteSetting.logo_url}"
|
|
||||||
|
opts ||= {}
|
||||||
|
opts[:image] ||= "#{Discourse.base_url}#{SiteSetting.logo_url}"
|
||||||
|
opts[:url] ||= "#{Discourse.base_url}#{request.fullpath}"
|
||||||
|
|
||||||
# Add opengraph tags
|
# Add opengraph tags
|
||||||
result = tag(:meta, property: 'og:site_name', content: SiteSetting.title) << "\n"
|
result = tag(:meta, property: 'og:site_name', content: SiteSetting.title) << "\n"
|
||||||
result << tag(:meta, property: 'og:image', content: image) << "\n"
|
|
||||||
result << tag(:meta, property: 'og:url', content: url) << "\n"
|
|
||||||
result << tag(:meta, property: 'og:title', content: title) << "\n"
|
|
||||||
result << tag(:meta, property: 'og:description', content: description) << "\n"
|
|
||||||
|
|
||||||
# Add twitter card
|
result << tag(:meta, property: 'twitter:card', content: "summary")
|
||||||
result << tag(:meta, property: 'twitter:card', content: "summary") << "\n"
|
[:image, :url, :title, :description].each do |property|
|
||||||
result << tag(:meta, property: 'twitter:url', content: url) << "\n"
|
if opts[property].present?
|
||||||
result << tag(:meta, property: 'twitter:title', content: title) << "\n"
|
result << tag(:meta, property: "og:#{property}", content: opts[property]) << "\n"
|
||||||
result << tag(:meta, property: 'twitter:description', content: description) << "\n"
|
result << tag(:meta, property: "twitter:#{property}", content: opts[property]) << "\n"
|
||||||
result << tag(:meta, property: 'twitter:image', content: image) << "\n"
|
end
|
||||||
|
end
|
||||||
|
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
require_dependency 'email_token'
|
require_dependency 'email_token'
|
||||||
require_dependency 'trust_level'
|
require_dependency 'trust_level'
|
||||||
require_dependency 'pbkdf2'
|
require_dependency 'pbkdf2'
|
||||||
|
require_dependency 'summarize'
|
||||||
|
|
||||||
class User < ActiveRecord::Base
|
class User < ActiveRecord::Base
|
||||||
attr_accessible :name, :username, :password, :email, :bio_raw, :website
|
attr_accessible :name, :username, :password, :email, :bio_raw, :website
|
||||||
|
@ -447,6 +448,11 @@ class User < ActiveRecord::Base
|
||||||
username
|
username
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def bio_summary
|
||||||
|
return nil unless bio_cooked.present?
|
||||||
|
Summarize.new(bio_cooked).summary
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def cook
|
def cook
|
||||||
|
|
|
@ -24,5 +24,5 @@
|
||||||
|
|
||||||
<% content_for :head do %>
|
<% content_for :head do %>
|
||||||
<%= auto_discovery_link_tag(@topic_view, {action: :feed, format: :rss}, title: t('rss_posts_in_topic', topic: @topic_view.title), type: 'application/rss+xml') %>
|
<%= auto_discovery_link_tag(@topic_view, {action: :feed, format: :rss}, title: t('rss_posts_in_topic', topic: @topic_view.title), type: 'application/rss+xml') %>
|
||||||
<%= crawlable_meta_data(@topic_view.absolute_url, @topic_view.title, @topic_view.summary) %>
|
<%= crawlable_meta_data(title: @topic_view.title, description: @topic_view.summary) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
<h2><%= @user.username %></h2>
|
||||||
|
|
||||||
|
<p><%= raw @user.bio_cooked %></p>
|
||||||
|
|
||||||
|
<p><%= t 'powered_by_html' %></p>
|
||||||
|
|
||||||
|
<% content_for :head do %>
|
||||||
|
<%= crawlable_meta_data(title: @user.username, description: @user.bio_summary) %>
|
||||||
|
<% end %>
|
|
@ -4,7 +4,7 @@ require_dependency 'admin_constraint'
|
||||||
|
|
||||||
# This used to be User#username_format, but that causes a preload of the User object
|
# This used to be User#username_format, but that causes a preload of the User object
|
||||||
# and makes Guard not work properly.
|
# and makes Guard not work properly.
|
||||||
USERNAME_ROUTE_FORMAT = /[A-Za-z0-9\._]+/
|
USERNAME_ROUTE_FORMAT = /[A-Za-z0-9\_]+/
|
||||||
|
|
||||||
Discourse::Application.routes.draw do
|
Discourse::Application.routes.draw do
|
||||||
|
|
||||||
|
@ -88,14 +88,14 @@ Discourse::Application.routes.draw do
|
||||||
get 'users/hp' => 'users#get_honeypot_value'
|
get 'users/hp' => 'users#get_honeypot_value'
|
||||||
|
|
||||||
get 'user_preferences' => 'users#user_preferences_redirect'
|
get 'user_preferences' => 'users#user_preferences_redirect'
|
||||||
get 'users/:username/private-messages' => 'user_actions#private_messages', :format => false, :constraints => {:username => USERNAME_ROUTE_FORMAT}
|
get 'users/:username/private-messages' => 'user_actions#private_messages', :constraints => {:username => USERNAME_ROUTE_FORMAT}
|
||||||
get 'users/:username' => 'users#show', :format => false, :constraints => {:username => USERNAME_ROUTE_FORMAT}
|
get 'users/:username' => 'users#show', :constraints => {:username => USERNAME_ROUTE_FORMAT}
|
||||||
put 'users/:username' => 'users#update', :format => false, :constraints => {:username => USERNAME_ROUTE_FORMAT}
|
put 'users/:username' => 'users#update', :constraints => {:username => USERNAME_ROUTE_FORMAT}
|
||||||
get 'users/:username/preferences' => 'users#preferences', :format => false, :constraints => {:username => USERNAME_ROUTE_FORMAT}, :as => :email_preferences
|
get 'users/:username/preferences' => 'users#preferences', :constraints => {:username => USERNAME_ROUTE_FORMAT}, :as => :email_preferences
|
||||||
get 'users/:username/preferences/email' => 'users#preferences', :format => false, :constraints => {:username => USERNAME_ROUTE_FORMAT}
|
get 'users/:username/preferences/email' => 'users#preferences', :constraints => {:username => USERNAME_ROUTE_FORMAT}
|
||||||
put 'users/:username/preferences/email' => 'users#change_email', :format => false, :constraints => {:username => USERNAME_ROUTE_FORMAT}
|
put 'users/:username/preferences/email' => 'users#change_email', :constraints => {:username => USERNAME_ROUTE_FORMAT}
|
||||||
get 'users/:username/preferences/username' => 'users#preferences', :format => false, :constraints => {:username => USERNAME_ROUTE_FORMAT}
|
get 'users/:username/preferences/username' => 'users#preferences', :constraints => {:username => USERNAME_ROUTE_FORMAT}
|
||||||
put 'users/:username/preferences/username' => 'users#username', :format => false, :constraints => {:username => USERNAME_ROUTE_FORMAT}
|
put 'users/:username/preferences/username' => 'users#username', :constraints => {:username => USERNAME_ROUTE_FORMAT}
|
||||||
get 'users/:username/avatar(/:size)' => 'users#avatar', :constraints => {:username => USERNAME_ROUTE_FORMAT}
|
get 'users/:username/avatar(/:size)' => 'users#avatar', :constraints => {:username => USERNAME_ROUTE_FORMAT}
|
||||||
get 'users/:username/invited' => 'users#invited', :constraints => {:username => USERNAME_ROUTE_FORMAT}
|
get 'users/:username/invited' => 'users#invited', :constraints => {:username => USERNAME_ROUTE_FORMAT}
|
||||||
get 'users/:username/send_activation_email' => 'users#send_activation_email', :constraints => {:username => USERNAME_ROUTE_FORMAT}
|
get 'users/:username/send_activation_email' => 'users#send_activation_email', :constraints => {:username => USERNAME_ROUTE_FORMAT}
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Summarize a HTML field into regular text. Used currently
|
||||||
|
# for meta tags
|
||||||
|
|
||||||
|
class Summarize
|
||||||
|
include ActionView::Helpers
|
||||||
|
|
||||||
|
def initialize(text)
|
||||||
|
@text = text
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.max_length
|
||||||
|
500
|
||||||
|
end
|
||||||
|
|
||||||
|
def summary
|
||||||
|
return nil if @text.blank?
|
||||||
|
|
||||||
|
result = sanitize(@text, tags: [], attributes: [])
|
||||||
|
result.gsub!(/\n/, ' ')
|
||||||
|
result.strip!
|
||||||
|
|
||||||
|
return result if result.length <= Summarize.max_length
|
||||||
|
"#{result[0..Summarize.max_length]}..."
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -1,8 +1,8 @@
|
||||||
require_dependency 'guardian'
|
require_dependency 'guardian'
|
||||||
require_dependency 'topic_query'
|
require_dependency 'topic_query'
|
||||||
|
require_dependency 'summarize'
|
||||||
|
|
||||||
class TopicView
|
class TopicView
|
||||||
include ActionView::Helpers
|
|
||||||
|
|
||||||
attr_accessor :topic, :min, :max, :draft, :draft_key, :draft_sequence, :posts
|
attr_accessor :topic, :min, :max, :draft, :draft_key, :draft_sequence, :posts
|
||||||
|
|
||||||
|
@ -76,11 +76,7 @@ class TopicView
|
||||||
|
|
||||||
def summary
|
def summary
|
||||||
return nil if posts.blank?
|
return nil if posts.blank?
|
||||||
first_post_content = sanitize(posts.first.cooked, tags: [], attributes: [])
|
Summarize.new(posts.first.cooked).summary
|
||||||
first_post_content.gsub!(/\n/, ' ')
|
|
||||||
|
|
||||||
return first_post_content if first_post_content.length <= 500
|
|
||||||
"#{first_post_content[0..500]}..."
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_posts(opts = {})
|
def filter_posts(opts = {})
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'summarize'
|
||||||
|
|
||||||
|
describe Summarize do
|
||||||
|
|
||||||
|
it "is blank when the input is nil" do
|
||||||
|
Summarize.new(nil).summary.should be_blank
|
||||||
|
end
|
||||||
|
|
||||||
|
it "is blank when the input is an empty string" do
|
||||||
|
Summarize.new("").summary.should be_blank
|
||||||
|
end
|
||||||
|
|
||||||
|
it "removes html tags" do
|
||||||
|
Summarize.new("hello <b>robin</b>").summary.should == "hello robin"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "strips leading and trailing space" do
|
||||||
|
Summarize.new("\t \t hello \t ").summary.should == "hello"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "trims long strings and adds an ellipsis" do
|
||||||
|
Summarize.stubs(:max_length).returns(11)
|
||||||
|
Summarize.new("discourse is a cool forum").summary.should == "discourse is..."
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -218,6 +218,7 @@ describe User do
|
||||||
|
|
||||||
its(:email_tokens) { should be_present }
|
its(:email_tokens) { should be_present }
|
||||||
its(:bio_cooked) { should be_present }
|
its(:bio_cooked) { should be_present }
|
||||||
|
its(:bio_summary) { should be_present }
|
||||||
its(:topics_entered) { should == 0 }
|
its(:topics_entered) { should == 0 }
|
||||||
its(:posts_read_count) { should == 0 }
|
its(:posts_read_count) { should == 0 }
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue