FEATURE: first pass at user summary page
This commit is contained in:
parent
9ad226aaa8
commit
7303f8f309
|
@ -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')
|
||||
});
|
|
@ -10,6 +10,7 @@ import UserBadge from 'discourse/models/user-badge';
|
|||
import UserActionStat from 'discourse/models/user-action-stat';
|
||||
import UserAction from 'discourse/models/user-action';
|
||||
import Group from 'discourse/models/group';
|
||||
import Topic from 'discourse/models/topic';
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -58,6 +58,7 @@ export default function() {
|
|||
// User routes
|
||||
this.resource('users');
|
||||
this.resource('user', { path: '/users/:username' }, function() {
|
||||
this.route('summary');
|
||||
this.resource('userActivity', { path: '/activity' }, function() {
|
||||
this.route('topics');
|
||||
this.route('replies');
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
export default Discourse.Route.extend({
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
export default Discourse.Route.extend({
|
||||
model() {
|
||||
return this.modelFor("User").summary();
|
||||
}
|
||||
});
|
|
@ -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}}
|
||||
|
|
@ -149,7 +149,7 @@
|
|||
</section>
|
||||
|
||||
<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}}
|
||||
<li>
|
||||
{{#link-to 'userNotifications'}}
|
||||
|
@ -167,6 +167,7 @@
|
|||
{{#if showBadges}}
|
||||
<li>{{#link-to 'user.badges'}}{{fa-icon "certificate"}}{{i18n 'badges.title'}}{{/link-to}}</li>
|
||||
{{/if}}
|
||||
<li>{{#link-to 'user.summary'}}{{i18n 'user.summary.title'}}{{/link-to}}</li>
|
||||
{{#if model.can_edit}}
|
||||
<li>{{#link-to 'preferences'}}{{fa-icon "cog"}}{{i18n 'user.preferences'}}{{/link-to}}</li>
|
||||
{{/if}}
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -176,6 +176,13 @@ class UsersController < ApplicationController
|
|||
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
|
||||
inviter = fetch_user_from_params
|
||||
offset = params[:offset].to_i || 0
|
||||
|
|
|
@ -619,7 +619,7 @@ class User < ActiveRecord::Base
|
|||
user_badges.select('distinct badge_id').count
|
||||
end
|
||||
|
||||
def featured_user_badges
|
||||
def featured_user_badges(limit=3)
|
||||
user_badges
|
||||
.joins(:badge)
|
||||
.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)
|
||||
.where("user_badges.id in (select min(u2.id)
|
||||
from user_badges u2 where u2.user_id = ? group by u2.badge_id)", id)
|
||||
.limit(3)
|
||||
.limit(limit)
|
||||
end
|
||||
|
||||
def self.count_by_signup_date(start_date, end_date)
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -703,6 +703,23 @@ en:
|
|||
ok: "Your password looks good."
|
||||
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"
|
||||
ip_address:
|
||||
title: "Last IP Address"
|
||||
|
|
|
@ -302,10 +302,13 @@ Discourse::Application.routes.draw do
|
|||
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/summary" => "users#summary", 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/:filter" => "users#invited", constraints: {username: USERNAME_ROUTE_FORMAT}
|
||||
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/:filter" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT}
|
||||
get "users/:username/badges" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT}
|
||||
|
|
|
@ -1607,4 +1607,19 @@ describe UsersController do
|
|||
|
||||
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
|
||||
|
|
|
@ -102,6 +102,4 @@ describe UserStat do
|
|||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue