User invites page now has search, displays first `invites_shown` records

This commit is contained in:
Robin Ward 2013-11-05 17:52:50 -05:00
parent 11260b4af4
commit 25ef66c60b
18 changed files with 215 additions and 259 deletions

View File

@ -2,11 +2,31 @@
This controller handles actions related to a user's invitations
@class UserInvitedController
@extends Discourse.ObjectController
@extends Ember.ArrayController
@namespace Discourse
@module Discourse
**/
Discourse.UserInvitedController = Discourse.ObjectController.extend({
Discourse.UserInvitedController = Ember.ArrayController.extend({
_searchTermChanged: Discourse.debounce(function() {
var self = this;
Discourse.Invite.findInvitedBy(self.get('user'), this.get('searchTerm')).then(function (invites) {
self.set('model', invites);
});
}, 250).observes('searchTerm'),
maxInvites: function() {
return Discourse.SiteSettings.invites_shown;
}.property(),
showSearch: function() {
if (Em.isNone(this.get('searchTerm')) && this.get('model.length') === 0) { return false; }
return true;
}.property('searchTerm', 'model.length'),
truncated: function() {
return this.get('model.length') === Discourse.SiteSettings.invites_shown;
}.property('model.length'),
actions: {
rescind: function(invite) {

View File

@ -27,6 +27,19 @@ Discourse.Invite.reopenClass({
result.user = Discourse.User.create(result.user);
}
return result;
},
findInvitedBy: function(user, filter) {
if (!user) { return Ember.RSVP.resolve(); }
var data = {};
if (!Em.isNone(filter)) { data.filter = filter; }
return Discourse.ajax("/users/" + user.get('username_lower') + "/invited.json", {data: data}).then(function (result) {
return result.map(function (i) {
return Discourse.Invite.create(i);
});
});
}
});

View File

@ -1,37 +0,0 @@
/**
A data model representing a list of Invites
@class InviteList
@extends Discourse.Model
@namespace Discourse
@module Discourse
**/
Discourse.InviteList = Discourse.Model.extend({
empty: (function() {
return this.blank('pending') && this.blank('redeemed');
}).property('pending.@each', 'redeemed.@each')
});
Discourse.InviteList.reopenClass({
findInvitedBy: function(user) {
return Discourse.ajax("/users/" + (user.get('username_lower')) + "/invited.json").then(function (result) {
var invitedList = result.invited_list;
if (invitedList.pending) {
invitedList.pending = invitedList.pending.map(function(i) {
return Discourse.Invite.create(i);
});
}
if (invitedList.redeemed) {
invitedList.redeemed = invitedList.redeemed.map(function(i) {
return Discourse.Invite.create(i);
});
}
invitedList.user = user;
return Discourse.InviteList.create(invitedList);
});
}
});

View File

@ -1,7 +1,7 @@
/**
A data model representing a navigation item on the list views
@class InviteList
@class NavItem
@extends Discourse.Model
@namespace Discourse
@module Discourse

View File

@ -12,11 +12,15 @@ Discourse.UserInvitedRoute = Discourse.Route.extend({
},
model: function() {
return Discourse.InviteList.findInvitedBy(this.modelFor('user'));
return Discourse.Invite.findInvitedBy(this.modelFor('user'));
},
setupController: function(controller, model) {
controller.set('model', model);
controller.setProperties({
model: model,
user: this.controllerFor('user').get('model'),
searchTerm: ''
});
this.controllerFor('user').set('indexStream', false);
}

View File

@ -1,46 +1,48 @@
<form class="form-horizontal">
<section class='user-content'>
<form class="form-horizontal">
<div class="control-group">
<div class="controls">
<h3>{{i18n user.change_email.title}}</h3>
<div class="control-group">
<div class="controls">
<h3>{{i18n user.change_email.title}}</h3>
</div>
</div>
</div>
{{#if success}}
{{#if success}}
<div class="control-group">
<div class="instructions">
<p>{{i18n user.change_email.success}}</p>
</div>
</div>
{{else}}
{{#if error}}
<div class="control-group">
<div class="instructions">
<div class='alert error'>{{i18n user.change_email.error}}</div>
</div>
</div>
{{/if}}
<div class="control-group">
<div class="instructions">
<p>{{i18n user.change_email.success}}</p>
<label class="control-label">{{i18n user.email.title}}</label>
<div class="controls">
{{textField value=newEmail id="change_email" classNames="input-xxlarge"}}
</div>
<div class='instructions'>
{{#if taken}}
{{i18n user.change_email.taken}}
{{else}}
{{i18n user.email.instructions}}
{{/if}}
</div>
</div>
{{else}}
{{#if error}}
<div class="control-group">
<div class="instructions">
<div class='alert error'>{{i18n user.change_email.error}}</div>
<div class="controls">
<button {{action changeEmail}} {{bindAttr disabled="saveDisabled"}} class="btn btn-primary">{{saveButtonText}}</button>
</div>
</div>
{{/if}}
<div class="control-group">
<label class="control-label">{{i18n user.email.title}}</label>
<div class="controls">
{{textField value=newEmail id="change_email" classNames="input-xxlarge"}}
</div>
<div class='instructions'>
{{#if taken}}
{{i18n user.change_email.taken}}
{{else}}
{{i18n user.email.instructions}}
{{/if}}
</div>
</div>
<div class="control-group">
<div class="controls">
<button {{action changeEmail}} {{bindAttr disabled="saveDisabled"}} class="btn btn-primary">{{saveButtonText}}</button>
</div>
</div>
{{/if}}
</form>
</form>
</section>

View File

@ -1,70 +1,59 @@
<div id='invited-users'>
{{#if empty}}
<div id='no-invites' class='boxed white'>
{{i18n user.invited.none username="user.username"}}
</div>
{{else}}
{{#if redeemed}}
<div class='invites'>
<h2>{{i18n user.invited.redeemed}}</h2>
<div class='boxed white'>
<table class='table'>
<tr>
<th>{{i18n user.invited.user}}</th>
<th>{{i18n user.invited.redeemed_at}}</th>
<th>{{i18n user.last_seen}}</th>
<th>{{i18n user.invited.topics_entered}}</th>
<th>{{i18n user.invited.posts_read_count}}</th>
<th>{{i18n user.invited.time_read}}</th>
<th>{{i18n user.invited.days_visited}}</th>
</tr>
{{#each redeemed}}
<tr>
<td>
<a href="{{unbound user.path}}">{{avatar user imageSize="tiny"}}</a>
<a href="{{unbound user.path}}">{{user.username}}</a>
</td>
<td>{{date redeemed_at}}</td>
<td>{{date user.last_seen_at}}</td>
<td>{{number user.topics_entered}}</td>
<td>{{number user.posts_read_count}}</td>
<td>{{{unbound user.time_read}}}</td>
<td><span title="{{i18n user.invited.days_visited}}">{{{unbound user.days_visited}}}</span>
/
<span title="{{i18n user.invited.account_age_days}}">{{{unbound user.days_since_created}}}</span></td>
</tr>
{{/each}}
</table>
</div>
</div>
<section class='user-content'>
<h2>{{i18n user.invited.title}}</h2>
{{#if showSearch}}
<form>
{{textField value=searchTerm placeholderKey="user.invited.search"}}
</form>
{{/if}}
{{#if model}}
<table class='table'>
<tr>
<th>{{i18n user.invited.user}}</th>
<th>{{i18n user.invited.redeemed_at}}</th>
<th>{{i18n user.last_seen}}</th>
<th>{{i18n user.invited.topics_entered}}</th>
<th>{{i18n user.invited.posts_read_count}}</th>
<th>{{i18n user.invited.time_read}}</th>
<th>{{i18n user.invited.days_visited}}</th>
</tr>
{{#each model}}
<tr>
{{#if user}}
<td>
{{#linkTo 'user' user}}{{avatar user imageSize="tiny"}}{{/linkTo}}
{{#linkTo 'user' user}}{{user.username}}{{/linkTo}}
</td>
<td>{{date redeemed_at}}</td>
<td>{{date user.last_seen_at}}</td>
<td>{{number user.topics_entered}}</td>
<td>{{number user.posts_read_count}}</td>
<td>{{{unbound user.time_read}}}</td>
<td><span title="{{i18n user.invited.days_visited}}">{{{unbound user.days_visited}}}</span>
/
<span title="{{i18n user.invited.account_age_days}}">{{{unbound user.days_since_created}}}</span></td>
{{else}}
<td>{{unbound email}}</td>
<td colspan='6'>
{{#if rescinded}}
{{i18n user.invited.rescinded}}
{{else}}
<button class='btn' {{action rescind this}}>{{i18n user.invited.rescind}}</button>
{{/if}}
</td>
{{/if}}
</tr>
{{/each}}
</table>
{{#if truncated}}
<p>{{i18n user.invited.truncated count=maxInvites}}</p>
{{/if}}
{{#if pending}}
<div class='invites'>
<h2>{{i18n user.invited.pending}}</h2>
<div class='boxed white'>
<table class='table'>
<tr>
<th style='width: 60%'>{{i18n user.email.title}}</th>
<th style='width: 20%'>{{i18n created}}</th>
<th>&nbsp;</th>
</tr>
{{#each pending}}
<tr>
<td>{{email}}</td>
<td>{{date created_at}}</td>
<td>
{{#if rescinded}}
{{i18n user.invited.rescinded}}
{{else}}
<button class='btn' {{action rescind this}}>{{i18n user.invited.rescind}}</button>
{{/if}}
</td>
</tr>
{{/each}}
</table>
</div>
</div>
{{/if}}
{{else}}
{{i18n user.invited.none}}
{{/if}}
</div>
</section>

View File

@ -1,37 +1,39 @@
<form class="form-horizontal">
<section class='user-content'>
<form class="form-horizontal">
<div class="control-group">
<div class="controls">
<h3>{{i18n user.change_username.title}}</h3>
</div>
</div>
{{#if error}}
<div class="control-group">
<div class="instructions">
<div class='alert error'>{{i18n user.change_username.error}}</div>
<div class="controls">
<h3>{{i18n user.change_username.title}}</h3>
</div>
</div>
{{/if}}
<div class="control-group">
<label class="control-label">{{i18n user.username.title}}</label>
<div class="controls">
{{textField value=newUsername id="change_username" classNames="input-xxlarge" maxlength="15"}}
</div>
<div class='instructions'>
{{#if taken}}
{{i18n user.change_username.taken}}
{{/if}}
<span>{{ errorMessage }}</span>
</div>
</div>
{{#if error}}
<div class="control-group">
<div class="instructions">
<div class='alert error'>{{i18n user.change_username.error}}</div>
</div>
</div>
{{/if}}
<div class="control-group">
<div class="controls">
<button {{action changeUsername}} {{bindAttr disabled="saveDisabled"}} class="btn btn-primary">{{saveButtonText}}</button>
{{#if saved}}{{i18n saved}}{{/if}}
<div class="control-group">
<label class="control-label">{{i18n user.username.title}}</label>
<div class="controls">
{{textField value=newUsername id="change_username" classNames="input-xxlarge" maxlength="15"}}
</div>
<div class='instructions'>
{{#if taken}}
{{i18n user.change_username.taken}}
{{/if}}
<span>{{ errorMessage }}</span>
</div>
</div>
</div>
</form>
<div class="control-group">
<div class="controls">
<button {{action changeUsername}} {{bindAttr disabled="saveDisabled"}} class="btn btn-primary">{{saveButtonText}}</button>
{{#if saved}}{{i18n saved}}{{/if}}
</div>
</div>
</form>
</section>

View File

@ -89,23 +89,6 @@
}
}
#no-invites {
padding: 10px;
}
#invited-users {
h2 {
color: $darkish_gray;
font-size: 20px;
}
.invites {
margin-bottom: 20px;
tr {
height: 45px;
}
}
}
.user-main {
width: 850px;
float: left;
@ -124,6 +107,26 @@
background-color: white;
border: 1px solid #ddd;
margin-bottom: 10px;
h2 {
margin-bottom: 10px;
}
table {
width: 100%;
margin-top: 10px;
th {
text-align: left;
border-bottom: 1px solid #aaa;
padding: 5px;
}
td {
padding: 5px;
border-bottom: 1px solid #ddd;
}
}
}
.about {

View File

@ -88,23 +88,6 @@
}
}
#no-invites {
padding: 10px;
}
#invited-users {
h2 {
color: $darkish_gray;
font-size: 20px;
}
.invites {
margin-bottom: 20px;
tr {
height: 45px;
}
}
}
.user-main {
clear: both;
margin-bottom: 50px;
@ -160,7 +143,7 @@
margin-top: 0px;
padding: 10px 10px 0 10px;
.btn {
.btn {
margin-bottom: 10px;
float: none;
}

View File

@ -85,8 +85,29 @@ class UsersController < ApplicationController
end
def invited
invited_list = InvitedList.new(fetch_user_from_params)
render_serialized(invited_list, InvitedListSerializer)
params.require(:username)
params.permit(:filter)
by_user = fetch_user_from_params
invited = Invite.where(invited_by_id: by_user.id)
.includes(:user => :user_stat)
.order('CASE WHEN invites.user_id IS NOT NULL THEN 0 ELSE 1 END',
'user_stats.time_read DESC',
'invites.redeemed_at DESC')
.limit(SiteSetting.invites_shown)
.references('user_stats')
unless guardian.can_see_pending_invites_from?(by_user)
invited = invited.where('invites.user_id IS NOT NULL')
end
if params[:filter].present?
invited = invited.where('(LOWER(invites.email) LIKE :filter) or (LOWER(users.username) LIKE :filter)', filter: "%#{params[:filter].downcase}%")
.references(:users)
end
render_serialized(invited.to_a, InviteSerializer)
end
def is_local_username

View File

@ -1,25 +0,0 @@
# A nice object to help keep track of invited users
class InvitedList
attr_accessor :pending
attr_accessor :redeemed
attr_accessor :by_user
def initialize(user)
@pending = []
@redeemed = []
@by_user = user
invited = Invite.where(invited_by_id: @by_user.id)
.includes(:user => :user_stat)
.order(:redeemed_at)
invited.each do |i|
if i.redeemed?
@redeemed << i
else
@pending << i unless i.expired?
end
end
end
end

View File

@ -272,6 +272,7 @@ class SiteSetting < ActiveRecord::Base
client_setting(:display_name_on_posts, false)
client_setting(:enable_names, true)
client_setting(:invites_shown, 30)
def self.call_discourse_hub?
self.enforce_global_nicknames? && self.discourse_org_access_key.present?

View File

@ -3,8 +3,6 @@ class InviteSerializer < ApplicationSerializer
attributes :email, :created_at, :redeemed_at
has_one :user, embed: :objects, serializer: InvitedUserSerializer
def include_email?
!object.redeemed?
end

View File

@ -1,10 +0,0 @@
class InvitedListSerializer < ApplicationSerializer
has_many :pending, serializer: InviteSerializer, embed: :objects
has_many :redeemed, serializer: InviteSerializer, embed: :objects
def include_pending?
scope.can_see_pending_invites_from?(object.by_user)
end
end

View File

@ -324,9 +324,11 @@ en:
other: "after {{count}} minutes"
invited:
search: "type to search invites..."
title: "Invites"
user: "Invited User"
none: "{{username}} hasn't invited any users to the site."
none: "No invites were found."
truncated: "Showing the first {{count}} invites."
redeemed: "Redeemed Invites"
redeemed_at: "Redeemed At"
pending: "Pending Invites"

View File

@ -723,6 +723,7 @@ en:
enable_names: "Allow users to show their full names"
display_name_on_posts: "Also show a user's full name on their posts"
invites_shown: "Maximum invites shown on a user page"
notification_types:
mentioned: "%{display_username} mentioned you in %{link}"

View File

@ -71,11 +71,6 @@ describe Invite do
@invite.topics.should == [topic]
end
it 'is pending in the invite list for the creator' do
InvitedList.new(inviter).pending.should == [@invite]
end
context 'when added by another user' do
let(:coding_horror) { Fabricate(:coding_horror) }
let(:new_invite) { topic.invite_by_email(coding_horror, iceking) }
@ -197,12 +192,6 @@ describe Invite do
end
it 'works correctly' do
# no longer in the pending list for that user
InvitedList.new(invite.invited_by).pending.should be_blank
# is redeeemed in the invite list for the creator
InvitedList.new(invite.invited_by).redeemed.should == [invite]
# has set the user_id attribute
invite.user.should == user