diff --git a/app/assets/javascripts/discourse/controllers/user_invited_controller.js b/app/assets/javascripts/discourse/controllers/user_invited_controller.js index 4548bdb56..877871bfb 100644 --- a/app/assets/javascripts/discourse/controllers/user_invited_controller.js +++ b/app/assets/javascripts/discourse/controllers/user_invited_controller.js @@ -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) { diff --git a/app/assets/javascripts/discourse/models/invite.js b/app/assets/javascripts/discourse/models/invite.js index 503fc58df..00b5a35b6 100644 --- a/app/assets/javascripts/discourse/models/invite.js +++ b/app/assets/javascripts/discourse/models/invite.js @@ -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); + }); + }); } }); diff --git a/app/assets/javascripts/discourse/models/invite_list.js b/app/assets/javascripts/discourse/models/invite_list.js deleted file mode 100644 index 43376f998..000000000 --- a/app/assets/javascripts/discourse/models/invite_list.js +++ /dev/null @@ -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); - }); - } - -}); - - diff --git a/app/assets/javascripts/discourse/models/nav_item.js b/app/assets/javascripts/discourse/models/nav_item.js index fcba5f39a..2c947886d 100644 --- a/app/assets/javascripts/discourse/models/nav_item.js +++ b/app/assets/javascripts/discourse/models/nav_item.js @@ -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 diff --git a/app/assets/javascripts/discourse/routes/user_invited_route.js b/app/assets/javascripts/discourse/routes/user_invited_route.js index 420b730ed..2c72df220 100644 --- a/app/assets/javascripts/discourse/routes/user_invited_route.js +++ b/app/assets/javascripts/discourse/routes/user_invited_route.js @@ -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); } diff --git a/app/assets/javascripts/discourse/templates/user/email.js.handlebars b/app/assets/javascripts/discourse/templates/user/email.js.handlebars index 978e42a8f..bb3e812d7 100644 --- a/app/assets/javascripts/discourse/templates/user/email.js.handlebars +++ b/app/assets/javascripts/discourse/templates/user/email.js.handlebars @@ -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> \ No newline at end of file diff --git a/app/assets/javascripts/discourse/templates/user/invited.js.handlebars b/app/assets/javascripts/discourse/templates/user/invited.js.handlebars index 9488a2d7d..af8965883 100644 --- a/app/assets/javascripts/discourse/templates/user/invited.js.handlebars +++ b/app/assets/javascripts/discourse/templates/user/invited.js.handlebars @@ -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> \ No newline at end of file diff --git a/app/assets/javascripts/discourse/templates/user/username.js.handlebars b/app/assets/javascripts/discourse/templates/user/username.js.handlebars index ac1ec8a88..fd395fb6c 100644 --- a/app/assets/javascripts/discourse/templates/user/username.js.handlebars +++ b/app/assets/javascripts/discourse/templates/user/username.js.handlebars @@ -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> \ No newline at end of file diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index 6fb35437f..73cc0588a 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -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 { diff --git a/app/assets/stylesheets/mobile/user.scss b/app/assets/stylesheets/mobile/user.scss index c0728bc03..3e36e04be 100644 --- a/app/assets/stylesheets/mobile/user.scss +++ b/app/assets/stylesheets/mobile/user.scss @@ -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; } diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index b2a696a69..df19e15d1 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -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 diff --git a/app/models/invited_list.rb b/app/models/invited_list.rb deleted file mode 100644 index 35e3a6bf9..000000000 --- a/app/models/invited_list.rb +++ /dev/null @@ -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 diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb index 070b1115c..5921bb720 100644 --- a/app/models/site_setting.rb +++ b/app/models/site_setting.rb @@ -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? diff --git a/app/serializers/invite_serializer.rb b/app/serializers/invite_serializer.rb index 3b9c1a063..73989b96e 100644 --- a/app/serializers/invite_serializer.rb +++ b/app/serializers/invite_serializer.rb @@ -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 diff --git a/app/serializers/invited_list_serializer.rb b/app/serializers/invited_list_serializer.rb deleted file mode 100644 index 58d9d1a8d..000000000 --- a/app/serializers/invited_list_serializer.rb +++ /dev/null @@ -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 diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index cbbed3d2b..33a56aff7 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -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" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 626a1fc28..e8080081d 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -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}" diff --git a/spec/models/invite_spec.rb b/spec/models/invite_spec.rb index 727914e28..8b84a8f4a 100644 --- a/spec/models/invite_spec.rb +++ b/spec/models/invite_spec.rb @@ -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