Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Finished invite/kick participants features; Added Users.ajaxGet metho…

…d for future usage;
  • Loading branch information...
commit 8a3b04d060252cc5ac51144b38bf1c95988b30d1 1 parent 45e4a12
@fallenlord authored
View
1  Roadmap
@@ -23,6 +23,7 @@
* 可以定时自动更新新的Story
* 游戏页面可以直接编辑(点击标题就编辑标题)
* 牌的排列需要适应小屏幕
+* 关闭的游戏不允许增删人员和修改标题等操作
技术:
* 考虑重构游戏列表页面将初始显示也改为ajax的
View
48 app/controllers/Games.java
@@ -36,6 +36,54 @@ public static void show(@Required Long gameId) {
render(game);
}
+ public static void ajaxInvite(@Required Long gameId, @Required String username) {
+ Game game = Game.findById(gameId);
+ if (game == null) {
+ notFound(Messages.get("game.message.not-exists", gameId));
+ }
+ User user = User.find("byUsername", username).first();
+ if (user == null) {
+ notFound(Messages.get("user.message.not-exists", username));
+ }
+ if (game.hasMember(user)) {
+ error(409, Messages.get("participant.message.already-exists", user.username, game.name));
+ }
+ User connectedUser = Security.connectedUser();
+ if (game.type != Game.Type.BAZAAR && !game.master.equals(connectedUser)) {
+ forbidden(Messages.get("participant.message.forbidden", game.name));
+ }
+
+ game.addParticipant(user);
+ game.save();
+
+ request.format = "json";
+ render(user);
+ }
+
+ public static void ajaxKick(@Required Long gameId, @Required String username) {
+ Game game = Game.findById(gameId);
+ if (game == null) {
+ notFound(Messages.get("game.message.not-exists", gameId));
+ }
+ User user = User.find("byUsername", username).first();
+ if (user == null) {
+ notFound(Messages.get("user.message.not-exists", username));
+ }
+ if (!game.participants.contains(user)) {
+ notFound(Messages.get("participant.message.not-exists", username));
+ }
+ User connectedUser = Security.connectedUser();
+ if (game.type != Game.Type.BAZAAR && !game.master.equals(connectedUser)) {
+ forbidden(Messages.get("participant.message.forbidden", game.name));
+ }
+
+ game.participants.remove(user);
+ game.save();
+
+ request.format = "json";
+ render(user);
+ }
+
public static void ajaxCreate(@Required Game game) {
User user = Security.connectedUser();
game.type = Game.Type.TALKSHOW; // TODO This default type is temp
View
39 app/controllers/Users.java
@@ -1,8 +1,15 @@
package controllers;
+import java.util.Collections;
+import java.util.List;
+
import models.User;
+import play.data.validation.Required;
+import play.i18n.Messages;
import play.mvc.Controller;
+import com.mchange.v2.sql.SqlUtils;
+
/**
* Users controller.
*
@@ -11,10 +18,40 @@
*/
public class Users extends Controller {
- public static void show(String username) {
+ private static final int DEFAULT_SUGGESTION_SIZE = 10;
+
+ public static void show(String username) {
User user = User.find("byUsername", username).first();
notFoundIfNull(user);
render(user);
}
+
+ public static void ajaxGet(@Required String username) {
+ User user = User.find("byUsername", username).first();
+ if (user == null) {
+ notFound(Messages.get("user.message.not-exists", username));
+ }
+ request.format = "json";
+ render(user);
+ }
+
+ public static void ajaxSuggest(@Required String term) {
+ if (term.length() == 0) {
+ renderJSON(Collections.emptyList());
+ }
+
+ // Retrive users
+ List<User> users;
+ String likeString = "%" + SqlUtils.escapeBadSqlPatternChars(term) + "%";
+ if (!likeString.contains("@")) {
+ users = User.find("(username like ? or name like ? or email like ?) order by username",
+ likeString, likeString, likeString + "@%").fetch(DEFAULT_SUGGESTION_SIZE);
+ } else {
+ users = User.find("email like ? order by username", likeString).fetch(DEFAULT_SUGGESTION_SIZE);
+ }
+
+ request.format = "json";
+ render(users);
+ }
}
View
1  app/views/Games/ajaxInvite.json
@@ -0,0 +1 @@
+#{json.user user /}
View
1  app/views/Games/ajaxKick.json
@@ -0,0 +1 @@
+#{json.user user /}
View
32 app/views/Games/show.html
@@ -2,6 +2,7 @@
#{set "title"}${game.name}#{/set}
#{set "moreScripts"}
<script src="@{'/public/javascripts/views/stories.js'}" type="text/javascript" charset="utf-8"></script>
+<script src="@{'/public/javascripts/views/participants.js'}" type="text/javascript" charset="utf-8"></script>
#{/set}
<div class="sidebar">
@@ -19,14 +20,9 @@
<div class="done">&{"game.time.end", game.endTime.formatInterval()}</div>
#{/elseif}
</div>
- <div class="participants">
+ <div id="participants" class="participants">
<h3>&{"game.participants"}</h3>
<ul>
- #{list items:game.participants, as:"participant"}
- <li class="user">
- <a href="@{Users.show(participant.id)}">${participant.name}</a>
- </li>
- #{/list}
</ul>
</div>
<div class="share">
@@ -104,12 +100,13 @@
<script type="text/javascript">
$(function() {
+ // Stories
var getAllStoriesAction = #{jsAction @Stories.ajaxGetAll(":gameId") /}
var createStoryAction = #{jsAction @Stories.ajaxCreate(":gameId") /}
var updateStoryAction = #{jsAction @Stories.ajaxUpdate(":gameId", ":storyId") /}
var removeStoryAction = #{jsAction @Stories.ajaxDelete(":gameId", ":storyId") /}
- var show = new PP.views.Stories({
+ new PP.views.Stories({
game: #{json.game game /},
actions: {
getAllStories: getAllStoriesAction,
@@ -118,5 +115,26 @@
removeStory: removeStoryAction
}
});
+
+ // Participants
+ var showUserAction = #{jsAction @Users.show(":username") /}
+ var suggestUserAction = #{jsAction @Users.ajaxSuggest() /}
+ var inviteUserAction = #{jsAction @Games.ajaxInvite(":gameId", ":username") /}
+ var kickUserAction = #{jsAction @Games.ajaxKick(":gameId", ":username") /}
+ new PP.views.Participants({
+ game: #{json.game game /},
+ participants: [
+ #{list items:game.participants, as:"participant"}
+ #{json.user participant /}
+ ${ participant_isLast ? "" : "," }
+ #{/list}
+ ],
+ actions: {
+ showUser: showUserAction,
+ suggestUser: suggestUserAction,
+ inviteUser: inviteUserAction,
+ kickUser: kickUserAction
+ }
+ });
})
</script>
View
1  app/views/Users/ajaxGet.json
@@ -0,0 +1 @@
+#{json.user user /}
View
8 app/views/Users/ajaxSuggest.json
@@ -0,0 +1,8 @@
+[
+#{list items:users, as:"user"}
+ {
+ "label": "${user.name}(${user.username}) - ${user.email}",
+ "value": "${user.username}"
+ }${ user_isLast ? "" : "," }
+#{/list}
+]
View
4 conf/messages.en
@@ -79,6 +79,10 @@ story.message.not-exists=Story %s not exists.
story.message.forbidden=You have no permission no story %s.
story.message.save-failure=Cannot save story %s.
+participant.invite=Invite people...
+participant.message.already-exists=User %s already joined in game %s
+participant.message.forbidden=You have no permission to join people to game %s
+
user.message.not-exists=User %s not exists.
secure.username=Username:
View
4 conf/routes
@@ -14,8 +14,12 @@ GET /games/{<\d+>gameId} Games
POST /ajax/games/? Games.ajaxCreate
PUT /ajax/games/{<\d+>gameId} Games.ajaxUpdate
DELETE /ajax/games/{<\d+>gameId} Games.ajaxDelete
+PUT /ajax/games/{<\d+>gameId}/participants/{username} Games.ajaxInvite
+DELETE /ajax/games/{<\d+>gameId}/participants/{username} Games.ajaxKick
GET /users/{username} Users.show
+GET /ajax/users/{username} Users.ajaxGet
+GET /ajax/suggestions/users Users.ajaxSuggest
POST /ajax/games/{<\d+>gameId}/stories/? Stories.ajaxCreate
GET /ajax/games/{<\d+>gameId}/stories/? Stories.ajaxGetAll
View
317 public/javascripts/views/participants.js
@@ -0,0 +1,317 @@
+/**
+ * Planning Poker namespace.
+ * @type object
+ */
+var PP = PP || {};
+
+/**
+ * Views namespace.
+ * @type object
+ * @namespace PP
+ */
+PP.views = PP.views || {};
+
+/**
+ * Participants control class.
+ *
+ * @class PP.views.Participants
+ * @namespace PP.views
+ * @author GuoLin
+ */
+(function($, window, undefined) {
+
+ var alert = PP.widgets.alert,
+ confirm = PP.widgets.confirm,
+ wrap = PP.util.wrap;
+
+ /**
+ * Participants constructor.
+ *
+ * @constructor
+ * @param options {object} Settings of class
+ */
+ PP.views.Participants = function(options) {
+ this.options = $.extend({}, this.options, options);
+ this.options.doms = wrap(this.options.doms);
+
+ // Render participants
+ this.render();
+ };
+
+ PP.views.Participants.prototype = {
+
+ options: {
+
+ /**
+ * Current game object.
+ *
+ * @property game
+ * @type object
+ */
+ game: null,
+
+ /**
+ * Current participants in the game.
+ *
+ * @property participants
+ * @type array
+ */
+ participants: [],
+
+ /**
+ * HTML DOM elements that will be used in class.
+ * <p>
+ * All of elements will be replaced by jQuery object
+ * on construct</p>
+ *
+ * @property doms
+ * @type object
+ */
+ doms: {
+
+ /**
+ * Participants and buttons container.
+ *
+ * @property container
+ * @type string | jQuery
+ * @default "#participants"
+ */
+ container: "#participants",
+
+ /**
+ * Participants container.
+ *
+ * @property participants
+ * @type string | jQuery
+ * @default "#participants ul"
+ */
+ participants: "#participants ul"
+
+ },
+
+ /**
+ * Functions to generate URL of controller actions.
+ *
+ * @property actions
+ * @type object
+ */
+ actions: {
+
+ /**
+ * Functions to generate URL of <code>Users.show</code>.
+ *
+ * @property showUser
+ * @type function
+ */
+ showUser: null,
+
+ /**
+ * Functions to generate URL of <code>Users.ajaxSuggest</code>.
+ *
+ * @property suggestUser
+ * @type function
+ */
+ suggestUser: null,
+
+ /**
+ * Functions to generate URL of <code>Games.ajaxInvite</code>.
+ *
+ * @property inviteUser
+ * @type function
+ */
+ inviteUser: null,
+
+ /**
+ * Functions to generate URL of <code>Games.ajaxKick</code>.
+ *
+ * @property kickUser
+ * @type function
+ */
+ kickUser: null
+ }
+
+ },
+
+ /**
+ * Invite box, just for checking if invite box initialized for now.
+ *
+ * @property inviteBox
+ * @type jQuery
+ */
+ inviteBox: null,
+
+ /**
+ * Render all participants.
+ *
+ * @method render
+ */
+ render: function() {
+ var me = this,
+ participants = this.options.doms.participants;
+
+ // Render participants
+ participants.empty();
+ $.each(this.options.participants, function(i, participant) {
+ me._render(participant, participants);
+ });
+
+ // Render invite input once
+ if (!this.inviteBox) {
+ this._renderInvite();
+ }
+ },
+
+ /**
+ * Invite a user to current game.
+ *
+ * @method invite
+ * @param username {string} Username of inviting user
+ * @param successCallback {function} Optional, fire on invite success if specified
+ */
+ invite: function(username, successCallback) {
+ var me = this;
+ if (!username || !$.trim(username)) {
+ return;
+ }
+ $.ajax({
+ url: this.options.actions.inviteUser({ gameId: this.options.game.id, username: username }),
+ type: "PUT",
+ success: function(user) {
+ if (successCallback && successCallback instanceof Function) {
+ successCallback.call(me, user);
+ }
+ me.onInvited(user);
+ },
+ error: function(request) {
+ alert(i18n("label.error"), request.responseText);
+ }
+ });
+ },
+
+ /**
+ * Kick a user from current game.
+ *
+ * @method kick
+ * @param username {string} Username of kicking user
+ */
+ kick: function(username) {
+ var me = this;
+ $.ajax({
+ url: this.options.actions.kickUser({ gameId: this.options.game.id, username: username }),
+ type: "DELETE",
+ success: function(participant) {
+ me.onKicked(participant);
+ },
+ error: function(request) {
+ alert(i18n("label.error"), request.responseText);
+ }
+ });
+ },
+
+ /**
+ * Fire on a user invited.
+ *
+ * @method onInvited
+ * @param participant {object} Invited user object
+ */
+ onInvited: function(participant) {
+ this.options.participants.push(participant);
+ this.render();
+ },
+
+ /**
+ * Fire on a user has been kicked.
+ *
+ * @method onKicked
+ * @param user {object} Kicked user object
+ */
+ onKicked: function(user) {
+ this.options.participants = $.grep(this.options.participants, function(participant) {
+ return participant.username == user.username ? null : participant;
+ });
+ this.render();
+ },
+
+ /**
+ * Render a participant under container.
+ *
+ * @method _render
+ * @param participant {object} Rendering participant object
+ * @private
+ */
+ _render: function(participant, container) {
+ var me = this, item, elUser, username = participant.username,
+ showUserUrl = this.options.actions.showUser({ username: username });
+
+ item = $('<li></li>');
+ elUser = $('<div class="user"></div>').appendTo(item);
+ $('<a href="' + showUserUrl + '">' + participant.name + '</a>').appendTo(elUser);
+ $('<div class="remove"></div>').click(function() {
+ me.kick(username);
+ }).appendTo(item);
+
+ item.appendTo(container);
+ },
+
+ /**
+ * Render invitation elements.
+ *
+ * @method _renderInvite
+ * @private
+ */
+ _renderInvite: function() {
+ var me = this, container = this.options.doms.container, input, box, button,
+ showBox, hideBox;
+
+ showBox = function() {
+ button.hide();
+ box.show();
+ input.focus();
+ };
+
+ hideBox = function() {
+ box.hide();
+ input.autocomplete("close").val("");
+ button.show();
+ };
+
+ // Render invite button
+ button = $('<a href="javascript:void(0)">' + i18n("participant.invite") + '</a>')
+ .click(showBox)
+ .appendTo(container);
+
+ // Render invite box
+ box = $('<div class="invite-box"></div>').hide();
+ input = $('<input type="text" />').autocomplete({
+ source: this.options.actions.suggestUser(),
+ minLength: 2
+ })
+ .bind($.browser.opera ? "keypress" : "keyup", function(event) {
+ var keyCode = event.keyCode;
+ if (keyCode == 27) {
+ hideBox();
+ }
+ else if (keyCode == 13) {
+ me.invite($(this).val(), function(user) {
+ hideBox();
+ });
+ }
+ })
+ .appendTo(box);
+ $('<a href="javascript:void(0)" class="ok"></a>').click(function() {
+ me.invite(input.val(), function(user) {
+ hideBox();
+ });
+ }).appendTo(box);
+ $('<a href="javascript:void(0)" class="cancel"></a>').click(function() {
+ hideBox();
+ }).appendTo(box);
+ box.appendTo(container);
+
+ // Set flag
+ this.inviteBox = box;
+ }
+
+ };
+
+})(jQuery, window)
View
10 public/stylesheets/main.css
@@ -100,6 +100,16 @@ h2 { text-shadow: 1px 1px 2px #ddd; font-size: 14px; font-weight: bold; margin:
.sidebar .share { }
.sidebar .share .url { width: 95%; color: #665; background: #f5f5f0; border: 4px solid #d0d0d0; padding: 4px; border-radius: 8px; -webkit-border-radius: 8px; -moz-border-radius: 8px; }
.sidebar .participants { margin: 8px 0; }
+.sidebar .participants li { zoom: 1; overflow: hidden; }
+.sidebar .participants li:hover .user a { background: #e5e5e5; }
+.sidebar .participants li .user { float: left; }
+.sidebar .participants li .remove { visibility: hidden; cursor: pointer; float: right; width: 16px; height: 16px; background: #efefe0 url(planningpoker/images/ui-icons_888888_256x240.png) no-repeat -96px -125px; }
+.sidebar .participants li:hover .remove { visibility: visible; }
+.sidebar .participants .invite-box { zoom: 1; overflow: hidden; }
+.sidebar .participants .invite-box a { display: block; width: 16px; height: 16px; float: left; background: url(planningpoker/images/ui-icons_888888_256x240.png) no-repeat; }
+.sidebar .participants .invite-box input { float: left; height: 14px; padding: 0; borrder: 1px solid #d0d0d0; }
+.sidebar .participants .invite-box .ok { background-position: -64px -141px; }
+.sidebar .participants .invite-box .cancel { background-position: -96px -125px; }
.sidebar h3 { font-size: 12px; color: #990; margin-top: 12px; padding-top: 4px; border-top: 1px solid #e5e0da; }
.edit-table {}
Please sign in to comment.
Something went wrong with that request. Please try again.