mirror of
https://github.com/discourse/discourse.git
synced 2025-06-06 22:44:41 +08:00
User invites page now has search, displays first invites_shown
records
This commit is contained in:
@ -2,11 +2,31 @@
|
|||||||
This controller handles actions related to a user's invitations
|
This controller handles actions related to a user's invitations
|
||||||
|
|
||||||
@class UserInvitedController
|
@class UserInvitedController
|
||||||
@extends Discourse.ObjectController
|
@extends Ember.ArrayController
|
||||||
@namespace Discourse
|
@namespace Discourse
|
||||||
@module 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: {
|
actions: {
|
||||||
rescind: function(invite) {
|
rescind: function(invite) {
|
||||||
|
@ -27,6 +27,19 @@ Discourse.Invite.reopenClass({
|
|||||||
result.user = Discourse.User.create(result.user);
|
result.user = Discourse.User.create(result.user);
|
||||||
}
|
}
|
||||||
return result;
|
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
|
A data model representing a navigation item on the list views
|
||||||
|
|
||||||
@class InviteList
|
@class NavItem
|
||||||
@extends Discourse.Model
|
@extends Discourse.Model
|
||||||
@namespace Discourse
|
@namespace Discourse
|
||||||
@module Discourse
|
@module Discourse
|
||||||
|
@ -12,11 +12,15 @@ Discourse.UserInvitedRoute = Discourse.Route.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
model: function() {
|
model: function() {
|
||||||
return Discourse.InviteList.findInvitedBy(this.modelFor('user'));
|
return Discourse.Invite.findInvitedBy(this.modelFor('user'));
|
||||||
},
|
},
|
||||||
|
|
||||||
setupController: function(controller, model) {
|
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);
|
this.controllerFor('user').set('indexStream', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<form class="form-horizontal">
|
<section class='user-content'>
|
||||||
|
<form class="form-horizontal">
|
||||||
|
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
@ -43,4 +44,5 @@
|
|||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
</section>
|
@ -1,13 +1,14 @@
|
|||||||
<div id='invited-users'>
|
<section class='user-content'>
|
||||||
{{#if empty}}
|
|
||||||
<div id='no-invites' class='boxed white'>
|
<h2>{{i18n user.invited.title}}</h2>
|
||||||
{{i18n user.invited.none username="user.username"}}
|
|
||||||
</div>
|
{{#if showSearch}}
|
||||||
{{else}}
|
<form>
|
||||||
{{#if redeemed}}
|
{{textField value=searchTerm placeholderKey="user.invited.search"}}
|
||||||
<div class='invites'>
|
</form>
|
||||||
<h2>{{i18n user.invited.redeemed}}</h2>
|
{{/if}}
|
||||||
<div class='boxed white'>
|
|
||||||
|
{{#if model}}
|
||||||
<table class='table'>
|
<table class='table'>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{i18n user.invited.user}}</th>
|
<th>{{i18n user.invited.user}}</th>
|
||||||
@ -18,11 +19,12 @@
|
|||||||
<th>{{i18n user.invited.time_read}}</th>
|
<th>{{i18n user.invited.time_read}}</th>
|
||||||
<th>{{i18n user.invited.days_visited}}</th>
|
<th>{{i18n user.invited.days_visited}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
{{#each redeemed}}
|
{{#each model}}
|
||||||
<tr>
|
<tr>
|
||||||
|
{{#if user}}
|
||||||
<td>
|
<td>
|
||||||
<a href="{{unbound user.path}}">{{avatar user imageSize="tiny"}}</a>
|
{{#linkTo 'user' user}}{{avatar user imageSize="tiny"}}{{/linkTo}}
|
||||||
<a href="{{unbound user.path}}">{{user.username}}</a>
|
{{#linkTo 'user' user}}{{user.username}}{{/linkTo}}
|
||||||
</td>
|
</td>
|
||||||
<td>{{date redeemed_at}}</td>
|
<td>{{date redeemed_at}}</td>
|
||||||
<td>{{date user.last_seen_at}}</td>
|
<td>{{date user.last_seen_at}}</td>
|
||||||
@ -32,39 +34,26 @@
|
|||||||
<td><span title="{{i18n user.invited.days_visited}}">{{{unbound user.days_visited}}}</span>
|
<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>
|
<span title="{{i18n user.invited.account_age_days}}">{{{unbound user.days_since_created}}}</span></td>
|
||||||
</tr>
|
{{else}}
|
||||||
{{/each}}
|
<td>{{unbound email}}</td>
|
||||||
</table>
|
<td colspan='6'>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/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}}
|
{{#if rescinded}}
|
||||||
{{i18n user.invited.rescinded}}
|
{{i18n user.invited.rescinded}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<button class='btn' {{action rescind this}}>{{i18n user.invited.rescind}}</button>
|
<button class='btn' {{action rescind this}}>{{i18n user.invited.rescind}}</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</td>
|
</td>
|
||||||
|
{{/if}}
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
|
||||||
</div>
|
{{#if truncated}}
|
||||||
|
<p>{{i18n user.invited.truncated count=maxInvites}}</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{else}}
|
||||||
|
{{i18n user.invited.none}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
|
||||||
|
</section>
|
@ -1,4 +1,5 @@
|
|||||||
<form class="form-horizontal">
|
<section class='user-content'>
|
||||||
|
<form class="form-horizontal">
|
||||||
|
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
@ -34,4 +35,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</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 {
|
.user-main {
|
||||||
width: 850px;
|
width: 850px;
|
||||||
float: left;
|
float: left;
|
||||||
@ -124,6 +107,26 @@
|
|||||||
background-color: white;
|
background-color: white;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
margin-bottom: 10px;
|
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 {
|
.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 {
|
.user-main {
|
||||||
clear: both;
|
clear: both;
|
||||||
margin-bottom: 50px;
|
margin-bottom: 50px;
|
||||||
|
@ -85,8 +85,29 @@ class UsersController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def invited
|
def invited
|
||||||
invited_list = InvitedList.new(fetch_user_from_params)
|
params.require(:username)
|
||||||
render_serialized(invited_list, InvitedListSerializer)
|
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
|
end
|
||||||
|
|
||||||
def is_local_username
|
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(:display_name_on_posts, false)
|
||||||
client_setting(:enable_names, true)
|
client_setting(:enable_names, true)
|
||||||
|
client_setting(:invites_shown, 30)
|
||||||
|
|
||||||
def self.call_discourse_hub?
|
def self.call_discourse_hub?
|
||||||
self.enforce_global_nicknames? && self.discourse_org_access_key.present?
|
self.enforce_global_nicknames? && self.discourse_org_access_key.present?
|
||||||
|
@ -3,8 +3,6 @@ class InviteSerializer < ApplicationSerializer
|
|||||||
attributes :email, :created_at, :redeemed_at
|
attributes :email, :created_at, :redeemed_at
|
||||||
has_one :user, embed: :objects, serializer: InvitedUserSerializer
|
has_one :user, embed: :objects, serializer: InvitedUserSerializer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def include_email?
|
def include_email?
|
||||||
!object.redeemed?
|
!object.redeemed?
|
||||||
end
|
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"
|
other: "after {{count}} minutes"
|
||||||
|
|
||||||
invited:
|
invited:
|
||||||
|
search: "type to search invites..."
|
||||||
title: "Invites"
|
title: "Invites"
|
||||||
user: "Invited User"
|
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: "Redeemed Invites"
|
||||||
redeemed_at: "Redeemed At"
|
redeemed_at: "Redeemed At"
|
||||||
pending: "Pending Invites"
|
pending: "Pending Invites"
|
||||||
|
@ -723,6 +723,7 @@ en:
|
|||||||
|
|
||||||
enable_names: "Allow users to show their full names"
|
enable_names: "Allow users to show their full names"
|
||||||
display_name_on_posts: "Also show a user's full name on their posts"
|
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:
|
notification_types:
|
||||||
mentioned: "%{display_username} mentioned you in %{link}"
|
mentioned: "%{display_username} mentioned you in %{link}"
|
||||||
|
@ -71,11 +71,6 @@ describe Invite do
|
|||||||
@invite.topics.should == [topic]
|
@invite.topics.should == [topic]
|
||||||
end
|
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
|
context 'when added by another user' do
|
||||||
let(:coding_horror) { Fabricate(:coding_horror) }
|
let(:coding_horror) { Fabricate(:coding_horror) }
|
||||||
let(:new_invite) { topic.invite_by_email(coding_horror, iceking) }
|
let(:new_invite) { topic.invite_by_email(coding_horror, iceking) }
|
||||||
@ -197,12 +192,6 @@ describe Invite do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'works correctly' do
|
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
|
# has set the user_id attribute
|
||||||
invite.user.should == user
|
invite.user.should == user
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user