User invites page now has search, displays first `invites_shown` records
This commit is contained in:
parent
11260b4af4
commit
25ef66c60b
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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> </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>
|
|
@ -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>
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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"
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue