FEATURE: first pass at user summary page

This commit is contained in:
Sam Saffron 2016-01-20 15:11:52 +11:00
parent 9ad226aaa8
commit 7303f8f309
16 changed files with 292 additions and 7 deletions

View file

@ -0,0 +1,13 @@
export default Ember.Controller.extend({
needs: ['user'],
user: Em.computed.alias('controllers.user.model'),
moreTopics: function(){
return this.get('model.topics').length > 5;
}.property('model'),
moreReplies: function(){
return this.get('model.replies').length > 5;
}.property('model'),
moreBadges: function(){
return this.get('model.badges').length > 5;
}.property('model')
});

View file

@ -10,6 +10,7 @@ import UserBadge from 'discourse/models/user-badge';
import UserActionStat from 'discourse/models/user-action-stat'; import UserActionStat from 'discourse/models/user-action-stat';
import UserAction from 'discourse/models/user-action'; import UserAction from 'discourse/models/user-action';
import Group from 'discourse/models/group'; import Group from 'discourse/models/group';
import Topic from 'discourse/models/topic';
const User = RestModel.extend({ const User = RestModel.extend({
@ -355,6 +356,38 @@ const User = RestModel.extend({
}); });
} }
}); });
},
summary() {
return Discourse.ajax(`/users/${this.get("username_lower")}/summary.json`)
.then(json => {
const topicMap = {};
json.topics.forEach(t => {
topicMap[t.id] = Topic.create(t);
});
const badgeMap = {};
Badge.createFromJson(json).forEach(b => {
badgeMap[b.id] = b;
});
const summary = json["user_summary"];
summary.replies.forEach(r => {
r.topic = topicMap[r.topic_id];
r.url = r.topic.urlForPostNumber(r.post_number);
r.createdAt = new Date(r.created_at);
});
summary.topics = summary.topic_ids.map(id => topicMap[id]);
summary.badges = summary.badges.map(ub => {
const badge = badgeMap[ub.badge_id];
badge.count = ub.count;
return badge;
});
return summary;
});
} }
}); });

View file

@ -58,6 +58,7 @@ export default function() {
// User routes // User routes
this.resource('users'); this.resource('users');
this.resource('user', { path: '/users/:username' }, function() { this.resource('user', { path: '/users/:username' }, function() {
this.route('summary');
this.resource('userActivity', { path: '/activity' }, function() { this.resource('userActivity', { path: '/activity' }, function() {
this.route('topics'); this.route('topics');
this.route('replies'); this.route('replies');

View file

@ -1,2 +0,0 @@
export default Discourse.Route.extend({
});

View file

@ -0,0 +1,5 @@
export default Discourse.Route.extend({
model() {
return this.modelFor("User").summary();
}
});

View file

@ -0,0 +1,62 @@
{{#if model.replies.length}}
<div class='top-section'>
<h3>{{i18n "user.summary.top_replies"}}</h3>
{{#each reply in model.replies}}
<ul>
<li>
<a href="{{reply.url}}">{{reply.topic.title}}</a> {{#if reply.like_count}}<span class='like-count'>{{reply.like_count}}<i class='fa fa-heart'></i></span>{{/if}} {{format-date reply.createdAt format="tiny" noTitle="true"}}
</li>
</ul>
{{/each}}
{{#if moreReplies}}
{{#link-to "userActivity.replies" user class="more"}}{{i18n "user.summary.more_replies"}}{{/link-to}}
{{/if}}
</div>
{{/if}}
{{#if model.topics.length}}
<div class='top-section'>
<h3>{{i18n "user.summary.top_topics"}}</h3>
{{#each topic in model.topics}}
<ul>
<li>
<a href="{{topic.url}}">{{topic.title}}</a> {{#if topic.like_count}}<span class='like-count'>{{topic.like_count}}<i class='fa fa-heart'></i></span>{{/if}} {{format-date topic.createdAt format="tiny" noTitle="true"}}
</li>
</ul>
{{/each}}
{{#if moreTopics}}
{{#link-to "userActivity.topics" user class="more"}}{{i18n "user.summary.more_topics"}}{{/link-to}}
{{/if}}
</div>
{{/if}}
<div class='top-section stats-section'>
<h3>{{i18n "user.summary.stats"}}</h3>
<dl>
<dt>{{i18n "user.summary.topic_count"}}</dt>
<dd>{{model.topic_count}}</dd>
<dt>{{i18n "user.summary.post_count"}}</dt>
<dd>{{model.post_count}}</dd>
<dt>{{i18n "user.summary.likes_given"}}</dt>
<dd>{{model.likes_given}}</dd>
<dt>{{i18n "user.summary.likes_received"}}</dt>
<dd>{{model.likes_received}}</dd>
<dt>{{i18n "user.summary.days_visited"}}</dt>
<dd>{{model.days_visited}}</dd>
<dt>{{i18n "user.summary.posts_read_count"}}</dt>
<dd>{{model.posts_read_count}}</dd>
</dl>
</div>
{{#if model.badges.length}}
<div class='top-section badges-section'>
<h3>{{i18n "user.summary.top_badges"}}</h3>
{{#each badge in model.badges}}
{{user-badge badge=badge count=badge.count}}
{{/each}}
{{#if moreBadges}}
{{#link-to "user.badges" user class="more"}}{{i18n "user.summary.more_badges"}}{{/link-to}}
{{/if}}
</div>
{{/if}}

View file

@ -149,7 +149,7 @@
</section> </section>
<ul class="nav nav-pills user-nav"> <ul class="nav nav-pills user-nav">
<li class='selected'>{{#link-to 'userActivity'}}{{i18n 'user.activity_stream'}}{{/link-to}}</li> <li>{{#link-to 'userActivity'}}{{i18n 'user.activity_stream'}}{{/link-to}}</li>
{{#if showNotificationsTab}} {{#if showNotificationsTab}}
<li> <li>
{{#link-to 'userNotifications'}} {{#link-to 'userNotifications'}}
@ -167,6 +167,7 @@
{{#if showBadges}} {{#if showBadges}}
<li>{{#link-to 'user.badges'}}{{fa-icon "certificate"}}{{i18n 'badges.title'}}{{/link-to}}</li> <li>{{#link-to 'user.badges'}}{{fa-icon "certificate"}}{{i18n 'badges.title'}}{{/link-to}}</li>
{{/if}} {{/if}}
<li>{{#link-to 'user.summary'}}{{i18n 'user.summary.title'}}{{/link-to}}</li>
{{#if model.can_edit}} {{#if model.can_edit}}
<li>{{#link-to 'preferences'}}{{fa-icon "cog"}}{{i18n 'user.preferences'}}{{/link-to}}</li> <li>{{#link-to 'preferences'}}{{fa-icon "cog"}}{{i18n 'user.preferences'}}{{/link-to}}</li>
{{/if}} {{/if}}

View file

@ -149,3 +149,61 @@
} }
} }
.top-section {
display: inline-block;
width: 45%;
max-width: 500px;
padding-right: 20px;
vertical-align: top;
margin-bottom: 30px;
.more {
display: block;
margin-top: 10px;
color: dark-light-choose(scale-color($primary, $lightness: 40%), scale-color($secondary, $lightness: 60%));
}
h3 {
margin-bottom: 15px;
}
.relative-date {
color: lighten($primary, 40%);
font-size: 0.8em;
margin-left: 5px;
}
.like-count {
margin-left: 5px;
}
ul {
list-style-type: none;
padding: 0;
margin: 0;
li {
margin: 0;
padding: 8px 0;
.fa-heart {
margin-left: 3px;
}
}
}
dt,dd {
float:left;
}
dd {
min-width: 80px;
text-align: right;
}
dt {
clear: left;
min-width: 100px;
color: dark-light-choose(scale-color($primary, $lightness: 25%), scale-color($secondary, $lightness: 75%));
}
}
@media all
and (max-width : 600px) {
.top-section {
width: 90%;
}
}

View file

@ -176,6 +176,13 @@ class UsersController < ApplicationController
end end
end end
def summary
user = fetch_user_from_params
summary = UserSummary.new(user, guardian)
serializer = UserSummarySerializer.new(summary, scope: guardian)
render_json_dump(serializer)
end
def invited def invited
inviter = fetch_user_from_params inviter = fetch_user_from_params
offset = params[:offset].to_i || 0 offset = params[:offset].to_i || 0

View file

@ -619,7 +619,7 @@ class User < ActiveRecord::Base
user_badges.select('distinct badge_id').count user_badges.select('distinct badge_id').count
end end
def featured_user_badges def featured_user_badges(limit=3)
user_badges user_badges
.joins(:badge) .joins(:badge)
.order("CASE WHEN badges.id = (SELECT MAX(ub2.badge_id) FROM user_badges ub2 .order("CASE WHEN badges.id = (SELECT MAX(ub2.badge_id) FROM user_badges ub2
@ -629,7 +629,7 @@ class User < ActiveRecord::Base
.includes(:user, :granted_by, badge: :badge_type) .includes(:user, :granted_by, badge: :badge_type)
.where("user_badges.id in (select min(u2.id) .where("user_badges.id in (select min(u2.id)
from user_badges u2 where u2.user_id = ? group by u2.badge_id)", id) from user_badges u2 where u2.user_id = ? group by u2.badge_id)", id)
.limit(3) .limit(limit)
end end
def self.count_by_signup_date(start_date, end_date) def self.count_by_signup_date(start_date, end_date)

View file

@ -0,0 +1,56 @@
# ViewModel used on Summary tab on User page
class UserSummary
MAX_FEATURED_BADGES = 7
MAX_TOPICS = 6
alias :read_attribute_for_serialization :send
def initialize(user, guardian)
@user = user
@guardian = guardian
end
def topics
Topic
.secured(@guardian)
.listable_topics
.where(user: @user)
.order('like_count desc, created_at asc')
.includes(:user, :category)
.limit(MAX_TOPICS)
end
def replies
Post
.secured(@guardian)
.where(user: @user)
.where('post_number > 1')
.where('topics.archetype <> ?', Archetype.private_message)
.order('posts.like_count desc, posts.created_at asc')
.includes(:user, {topic: :category})
.references(:topic)
.limit(MAX_TOPICS)
end
def badges
user_badges = @user.user_badges
user_badges = user_badges.group(:badge_id)
.select(UserBadge.attribute_names.map {|x|
"MAX(#{x}) as #{x}" }, 'COUNT(*) as count')
.includes(badge: [:badge_grouping, :badge_type])
.includes(post: :topic)
.includes(:granted_by)
.limit(MAX_FEATURED_BADGES)
end
def user_stat
@user.user_stat
end
delegate :likes_given, :likes_received, :days_visited,
:posts_read_count, :topic_count, :post_count,
to: :user_stat
end

View file

@ -0,0 +1,18 @@
class UserSummarySerializer < ApplicationSerializer
class TopicSerializer < BasicTopicSerializer
attributes :like_count, :slug, :created_at
end
class ReplySerializer < ApplicationSerializer
attributes :post_number, :like_count, :created_at
has_one :topic, serializer: TopicSerializer
end
has_many :topics, serializer: TopicSerializer
has_many :replies, serializer: ReplySerializer, embed: :object
has_many :badges, serializer: UserBadgeSerializer, embed: :object
attributes :likes_given, :likes_received, :posts_read_count,
:days_visited, :topic_count, :post_count
end

View file

@ -703,6 +703,23 @@ en:
ok: "Your password looks good." ok: "Your password looks good."
instructions: "At least %{count} characters." instructions: "At least %{count} characters."
summary:
title: "Summary"
stats: "Stats"
topic_count: "Topics Created"
post_count: "Posts Created"
likes_given: "Likes Given"
likes_received: "Likes Received"
days_visited: "Days Visited"
posts_read_count: "Posts Read"
top_replies: "Top Replies"
top_topics: "Top Topics"
top_badges: "Top Badges"
more_topics: "More Topics"
more_replies: "More Replies"
more_badges: "More Badges"
associated_accounts: "Logins" associated_accounts: "Logins"
ip_address: ip_address:
title: "Last IP Address" title: "Last IP Address"

View file

@ -302,10 +302,13 @@ Discourse::Application.routes.draw do
put "users/:username/preferences/card-badge" => "users#update_card_badge", constraints: {username: USERNAME_ROUTE_FORMAT} put "users/:username/preferences/card-badge" => "users#update_card_badge", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/staff-info" => "users#staff_info", constraints: {username: USERNAME_ROUTE_FORMAT} get "users/:username/staff-info" => "users#staff_info", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/summary" => "users#summary", 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/invited_count" => "users#invited_count", constraints: {username: USERNAME_ROUTE_FORMAT} get "users/:username/invited_count" => "users#invited_count", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/invited/:filter" => "users#invited", constraints: {username: USERNAME_ROUTE_FORMAT} get "users/:username/invited/:filter" => "users#invited", constraints: {username: USERNAME_ROUTE_FORMAT}
post "users/action/send_activation_email" => "users#send_activation_email" post "users/action/send_activation_email" => "users#send_activation_email"
get "users/:username/summary" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/activity" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} get "users/:username/activity" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/activity/:filter" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} get "users/:username/activity/:filter" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/badges" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} get "users/:username/badges" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT}

View file

@ -1607,4 +1607,19 @@ describe UsersController do
end end
context '#summary' do
it "generates summary info" do
user = Fabricate(:user)
create_post(user: user)
xhr :get, :summary, username: user.username_lower
expect(response).to be_success
json = JSON.parse(response.body)
expect(json["user_summary"]["topic_count"]).to eq(1)
expect(json["user_summary"]["post_count"]).to eq(1)
end
end
end end

View file

@ -102,6 +102,4 @@ describe UserStat do
end end
end end
end end