mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-23 23:58:31 -05:00
FEATURE: first pass at user summary page
This commit is contained in:
parent
9ad226aaa8
commit
7303f8f309
16 changed files with 292 additions and 7 deletions
|
@ -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 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;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
export default Discourse.Route.extend({
|
|
||||||
});
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
export default Discourse.Route.extend({
|
||||||
|
model() {
|
||||||
|
return this.modelFor("User").summary();
|
||||||
|
}
|
||||||
|
});
|
62
app/assets/javascripts/discourse/templates/user/summary.hbs
Normal file
62
app/assets/javascripts/discourse/templates/user/summary.hbs
Normal 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}}
|
||||||
|
|
|
@ -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}}
|
||||||
|
|
|
@ -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
|
||||||
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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
56
app/models/user_summary.rb
Normal file
56
app/models/user_summary.rb
Normal 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
|
18
app/serializers/user_summary_serializer.rb
Normal file
18
app/serializers/user_summary_serializer.rb
Normal 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
|
|
@ -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"
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -102,6 +102,4 @@ describe UserStat do
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue