diff --git a/app/controllers/client_api/teams_controller.rb b/app/controllers/client_api/teams_controller.rb
index 029282d73..0ffba958a 100644
--- a/app/controllers/client_api/teams_controller.rb
+++ b/app/controllers/client_api/teams_controller.rb
@@ -1,5 +1,6 @@
module ClientApi
class TeamsController < ApplicationController
+ include ClientApi::Users::UserTeamsHelper
MissingTeamError = Class.new(StandardError)
def index
@@ -34,11 +35,11 @@ module ClientApi
end
def teams
- { teams: current_user.teams }
+ { teams: current_user.teams_data }
end
def change_current_team
- team_id = params.fetch(:team_id) { raise MissingTeamError }
+ team_id = params.fetch(:team_id) { raise MissingTeamError }
unless current_user.teams.pluck(:id).include? team_id
raise MissingTeamError
end
diff --git a/app/controllers/client_api/users/user_teams_controller.rb b/app/controllers/client_api/users/user_teams_controller.rb
new file mode 100644
index 000000000..de1a39f8f
--- /dev/null
+++ b/app/controllers/client_api/users/user_teams_controller.rb
@@ -0,0 +1,86 @@
+module ClientApi
+ module Users
+ class UserTeamsController < ApplicationController
+ include NotificationsHelper
+ include InputSanitizeHelper
+ include ClientApi::Users::UserTeamsHelper
+
+ before_action :find_user_team, only: :leave_team
+
+ def leave_team
+ if user_cant_leave?
+ unsuccess_response
+ else
+ begin
+ assign_new_team_owner
+ generate_new_notification
+ success_response
+ rescue
+ unsuccess_response
+ end
+ end
+ end
+
+ private
+
+ def find_user_team
+ @team = Team.find_by_id(params[:team])
+ @user_team = UserTeam.where(team: @team, user: current_user).first
+ end
+
+ def user_cant_leave?
+ return unless @user_team && @team
+ @user_team.admin? &&
+ @team.user_teams.where(role: 2).count <= 1
+ end
+
+ def success_response
+ respond_to do |format|
+ # return a list of teams
+ format.json do
+ render template: '/client_api/teams/index',
+ status: :ok,
+ locals: {
+ teams: current_user.teams_data,
+ flash_message: t('client_api.user_teams.leave_flash',
+ team: @team.name)
+ }
+ end
+ end
+ end
+
+ def unsuccess_response
+ respond_to do |format|
+ format.json do
+ render json: { message: t(
+ 'client_api.user_teams.leave_team_error'
+ ) },
+ status: :unprocessable_entity
+ end
+ end
+ end
+
+ def assign_new_team_owner
+ new_owner = @team.user_teams
+ .where(role: 2)
+ .where.not(id: @user_team.id)
+ .first.user
+ new_owner ||= current_user
+ reset_user_current_team(@user_team)
+ @user_team.destroy(new_owner)
+ end
+
+ def reset_user_current_team(user_team)
+ ids = user_team.user.teams_ids
+ ids -= [user_team.team.id]
+ user_team.user.current_team_id = ids.first
+ user_team.user.save
+ end
+
+ def generate_new_notification
+ generate_notification(@user_team.user, @user_team.user, @user_team.team,
+ false, false)
+ end
+ end
+ end
+end
diff --git a/app/helpers/client_api/users/user_teams_helper.rb b/app/helpers/client_api/users/user_teams_helper.rb
new file mode 100644
index 000000000..699b5a086
--- /dev/null
+++ b/app/helpers/client_api/users/user_teams_helper.rb
@@ -0,0 +1,10 @@
+module ClientApi
+ module Users
+ module UserTeamsHelper
+ def retrive_role_name(index)
+ return unless index
+ ['Guest', 'Normal user', 'Administrator'].at(index)
+ end
+ end
+ end
+end
diff --git a/app/javascript/packs/app/action_types.js b/app/javascript/packs/app/action_types.js
index 51e990aea..c80d3fe76 100644
--- a/app/javascript/packs/app/action_types.js
+++ b/app/javascript/packs/app/action_types.js
@@ -23,3 +23,7 @@ export const CHANGE_RECENT_NOTIFICATION_EMAIL =
"CHANGE_RECENT_NOTIFICATION_EMAIL";
export const CHANGE_SYSTEM_MESSAGE_NOTIFICATION_EMAIL =
"CHANGE_SYSTEM_MESSAGE_NOTIFICATION_EMAIL";
+
+// user teams
+export const LEAVE_TEAM = "LEAVE_TEAM"
+export const SHOW_LEAVE_TEAM_MODAL = "SHOW_LEAVE_TEAM_MODAL"
diff --git a/app/javascript/packs/app/reducers.js b/app/javascript/packs/app/reducers.js
index d83169529..0cfd67fc9 100644
--- a/app/javascript/packs/app/reducers.js
+++ b/app/javascript/packs/app/reducers.js
@@ -5,10 +5,12 @@ import {
} from "../shared/reducers/TeamReducers";
import { globalActivities } from "../shared/reducers/ActivitiesReducers";
import { currentUser } from "../shared/reducers/UsersReducer";
+import { showLeaveTeamModal } from "../shared/reducers/LeaveTeamReducer";
export default combineReducers({
current_team: setCurrentTeam,
all_teams: getListOfTeams,
global_activities: globalActivities,
- current_user: currentUser
+ current_user: currentUser,
+ showLeaveTeamModal
});
diff --git a/app/javascript/packs/app/routes.js b/app/javascript/packs/app/routes.js
index 373db2b32..4f926c2d5 100644
--- a/app/javascript/packs/app/routes.js
+++ b/app/javascript/packs/app/routes.js
@@ -22,6 +22,9 @@ export const PREMIUM_LINK = "http://scinote.net/premium/";
export const CONTACT_US_LINK =
"http://scinote.net/story-of-scinote/#contact-scinote";
+// user teams
+export const LEAVE_TEAM_PATH = "/client_api/users/leave_team"
+
// settings
export const SETTINGS_ACCOUNT_PROFILE = "/settings/account/profile";
export const SETTINGS_ACCOUNT_PREFERENCES = "/settings/account/preferences";
diff --git a/app/javascript/packs/locales/messages.js b/app/javascript/packs/locales/messages.js
index cd8afa045..7932e3533 100644
--- a/app/javascript/packs/locales/messages.js
+++ b/app/javascript/packs/locales/messages.js
@@ -18,6 +18,19 @@ export default {
info_label: "Info"
},
settings_page: {
+ all_teams: "All teams",
+ in_team: "You are member of {num} team",
+ in_teams: "You are member of {num} team",
+ leave_team: "Leave team",
+ leave_team_modal: {
+ title: "Leave team {teamName}",
+ subtitle: "Are you sure you wish to leave team My projects? This action is irreversible.",
+ warnings: "Leaving team has following consequences:",
+ warning_message_one: "you will lose access to all content belonging to the team (including projects, tasks, protocols and activities);",
+ warning_message_two: "all projects in the team where you were the sole Owner will receive a new owner from the team administrators;",
+ warning_message_three: "all repository protocols in the team belonging to you will be reassigned onto a new owner from team administrators.",
+ leave_team: "Leave"
+ },
account: "Account",
team: "Team",
avatar: "Avatar",
diff --git a/app/javascript/packs/shared/actions/LeaveTeamActions.js b/app/javascript/packs/shared/actions/LeaveTeamActions.js
new file mode 100644
index 000000000..ffd489a56
--- /dev/null
+++ b/app/javascript/packs/shared/actions/LeaveTeamActions.js
@@ -0,0 +1,8 @@
+import { SHOW_LEAVE_TEAM_MODAL } from "../../app/action_types";
+
+export function leaveTeamModalShow(show = false, id = 0, teamName = "") {
+ return {
+ payload: { show, id, teamName },
+ type: SHOW_LEAVE_TEAM_MODAL
+ };
+}
diff --git a/app/javascript/packs/shared/actions/TeamsActions.js b/app/javascript/packs/shared/actions/TeamsActions.js
index a13590fed..40f4b7295 100644
--- a/app/javascript/packs/shared/actions/TeamsActions.js
+++ b/app/javascript/packs/shared/actions/TeamsActions.js
@@ -3,16 +3,16 @@ import _ from "lodash";
import { TEAMS_PATH, CHANGE_TEAM_PATH } from "../../app/routes";
import { GET_LIST_OF_TEAMS, SET_CURRENT_TEAM } from "../../app/action_types";
-function addTeamsData(data) {
+export function addTeamsData(data) {
return {
type: GET_LIST_OF_TEAMS,
payload: data
};
}
-export function setCurrentUser(user) {
+export function setCurrentTeam(team) {
return {
- user,
+ team,
type: SET_CURRENT_TEAM
};
}
@@ -22,10 +22,10 @@ export function getTeamsList() {
axios
.get(TEAMS_PATH, { withCredentials: true })
.then(response => {
- let teams = _.values(response.data);
+ const teams = response.data.teams.collection;
dispatch(addTeamsData(teams));
- let current_team = _.find(teams, team => team.current_team);
- dispatch(setCurrentUser(current_team));
+ const currentTeam = _.find(teams, team => team.current_team);
+ dispatch(setCurrentTeam(currentTeam));
})
.catch(error => {
console.log("get Teams Error: ", error);
@@ -33,15 +33,15 @@ export function getTeamsList() {
};
}
-export function changeTeam(team_id) {
+export function changeTeam(teamId) {
return dispatch => {
axios
- .post(CHANGE_TEAM_PATH, { team_id }, { withCredentials: true })
+ .post(CHANGE_TEAM_PATH, { teamId }, { withCredentials: true })
.then(response => {
- let teams = _.values(response.data);
+ const teams = response.data.teams.collection;
dispatch(addTeamsData(teams));
- let current_team = _.find(teams, team => team.current_team);
- dispatch(setCurrentUser(current_team));
+ const currentTeam = _.find(teams, team => team.current_team);
+ dispatch(setCurrentTeam(currentTeam));
})
.catch(error => {
console.log("get Teams Error: ", error);
diff --git a/app/javascript/packs/shared/data_table/index.jsx b/app/javascript/packs/shared/data_table/index.jsx
index 25f37e657..7d75f861f 100644
--- a/app/javascript/packs/shared/data_table/index.jsx
+++ b/app/javascript/packs/shared/data_table/index.jsx
@@ -108,4 +108,4 @@ DataTable.propTypes = {
data: PropTypes.arrayOf(PropTypes.object).isRequired
};
-export default DataTable;
\ No newline at end of file
+export default DataTable;
diff --git a/app/javascript/packs/shared/modals_container/index.jsx b/app/javascript/packs/shared/modals_container/index.jsx
new file mode 100644
index 000000000..7cf9aff55
--- /dev/null
+++ b/app/javascript/packs/shared/modals_container/index.jsx
@@ -0,0 +1,7 @@
+import React from "react";
+import LeaveTeamModal from "./modals/LeaveTeamModal";
+
+export default () =>
+
+
+
;
diff --git a/app/javascript/packs/shared/modals_container/modals/LeaveTeamModal.jsx b/app/javascript/packs/shared/modals_container/modals/LeaveTeamModal.jsx
new file mode 100644
index 000000000..790ba3a59
--- /dev/null
+++ b/app/javascript/packs/shared/modals_container/modals/LeaveTeamModal.jsx
@@ -0,0 +1,103 @@
+import React, { Component } from "react";
+import PropTypes, { bool, number, string, func } from "prop-types";
+import { Modal, Button, Alert, Glyphicon } from "react-bootstrap";
+import { FormattedMessage, FormattedHTMLMessage } from "react-intl";
+import { connect } from "react-redux";
+import axios from "../../../app/axios";
+
+import { LEAVE_TEAM_PATH } from "../../../app/routes";
+import { leaveTeamModalShow } from "../../actions/LeaveTeamActions";
+import { addTeamsData, setCurrentTeam } from "../../actions/TeamsActions";
+
+class LeaveTeamModal extends Component {
+ constructor(props) {
+ super(props);
+ this.onCloseModal = this.onCloseModal.bind(this);
+ this.leaveTeam = this.leaveTeam.bind(this);
+ }
+
+ onCloseModal() {
+ this.props.leaveTeamModalShow(false);
+ }
+
+ leaveTeam() {
+ const teamUrl = `${LEAVE_TEAM_PATH}?team=${this.props.teamId}`;
+ axios
+ .delete(teamUrl, {
+ withCredentials: true
+ })
+ .then(response => {
+ console.log(response);
+ const teams = response.data.teams.collection;
+ this.props.addTeamsData(teams);
+ const currentTeam = _.find(teams, team => team.current_team);
+ this.props.setCurrentTeam(currentTeam);
+ })
+ .catch(error => {
+ console.log("error: ", error.response.data.message);
+ });
+ this.props.leaveTeamModalShow(false);
+ }
+
+ render() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+LeaveTeamModal.propTypes = {
+ showModal: bool.isRequired,
+ teamId: number.isRequired,
+ teamName: string.isRequired,
+ addTeamsData: func.isRequired,
+ leaveTeamModalShow: func.isRequired
+};
+const mapStateToProps = ({ showLeaveTeamModal }) => ({
+ showModal: showLeaveTeamModal.show,
+ teamId: showLeaveTeamModal.id,
+ teamName: showLeaveTeamModal.teamName
+});
+
+export default connect(mapStateToProps, {
+ leaveTeamModalShow,
+ addTeamsData,
+ setCurrentTeam
+})(LeaveTeamModal);
diff --git a/app/javascript/packs/shared/navigation/components/TeamSwitch.jsx b/app/javascript/packs/shared/navigation/components/TeamSwitch.jsx
index bbb1a5ec2..35094a8a8 100644
--- a/app/javascript/packs/shared/navigation/components/TeamSwitch.jsx
+++ b/app/javascript/packs/shared/navigation/components/TeamSwitch.jsx
@@ -7,7 +7,7 @@ import styled from "styled-components";
import _ from "lodash";
import { BORDER_GRAY_COLOR } from "../../../app/constants/colors";
-import { setCurrentUser, changeTeam } from "../../actions/TeamsActions";
+import { changeTeam } from "../../actions/TeamsActions";
import { getTeamsList } from "../../actions/TeamsActions";
const StyledNavDropdown = styled(NavDropdown)`
@@ -89,14 +89,11 @@ TeamSwitch.propTypes = {
// Map the states from store to component
const mapStateToProps = ({ all_teams, current_team }) => ({
current_team,
- all_teams: _.values(all_teams)
+ all_teams: all_teams.collection
});
// Map the fetch activity action to component
const mapDispatchToProps = dispatch => ({
- setCurrentUser() {
- dispatch(setCurrentUser());
- },
changeTeam(teamId) {
dispatch(changeTeam(teamId));
},
diff --git a/app/javascript/packs/shared/reducers/LeaveTeamReducer.js b/app/javascript/packs/shared/reducers/LeaveTeamReducer.js
new file mode 100644
index 000000000..a784bc582
--- /dev/null
+++ b/app/javascript/packs/shared/reducers/LeaveTeamReducer.js
@@ -0,0 +1,11 @@
+import { SHOW_LEAVE_TEAM_MODAL } from "../../app/action_types";
+
+export function showLeaveTeamModal(
+ state = { show: false, id: 0, teamName: "" },
+ action
+) {
+ if (action.type === SHOW_LEAVE_TEAM_MODAL) {
+ return { ...state, ...action.payload };
+ }
+ return state;
+}
diff --git a/app/javascript/packs/shared/reducers/TeamReducers.js b/app/javascript/packs/shared/reducers/TeamReducers.js
index 707752c93..a3ca8a7c5 100644
--- a/app/javascript/packs/shared/reducers/TeamReducers.js
+++ b/app/javascript/packs/shared/reducers/TeamReducers.js
@@ -3,14 +3,17 @@ import { SET_CURRENT_TEAM, GET_LIST_OF_TEAMS } from "../../app/action_types";
const initialState = { name: "", id: 0, current_team: true };
export const setCurrentTeam = (state = initialState, action) => {
if (action.type === SET_CURRENT_TEAM) {
- return Object.assign({}, state, action.user);
+ return Object.assign({}, state, action.team);
}
return state;
};
-export const getListOfTeams = (state = [], action) => {
+export const getListOfTeams = (state = { collection: [] }, action) => {
if (action.type === GET_LIST_OF_TEAMS) {
- return Object.assign({}, state, action.payload);
+ return {
+ ...state,
+ collection: action.payload
+ };
}
return state;
};
diff --git a/app/javascript/packs/src/settings/app.jsx b/app/javascript/packs/src/settings/app.jsx
index 3269663ed..76b992326 100644
--- a/app/javascript/packs/src/settings/app.jsx
+++ b/app/javascript/packs/src/settings/app.jsx
@@ -10,6 +10,7 @@ import store from "../../app/store";
import messages from "../../locales/messages";
import MainNav from "./components/MainNav";
+import ModalsContainer from "../../shared/modals_container";
addLocaleData([...enLocaleData]);
const locale = "en-US";
@@ -17,6 +18,7 @@ const locale = "en-US";
const SettingsPage = () =>
+
;
document.addEventListener("DOMContentLoaded", () => {
diff --git a/app/javascript/packs/src/settings/components/team/SettingsTeams.jsx b/app/javascript/packs/src/settings/components/team/SettingsTeams.jsx
index b6114583b..eb261b4eb 100644
--- a/app/javascript/packs/src/settings/components/team/SettingsTeams.jsx
+++ b/app/javascript/packs/src/settings/components/team/SettingsTeams.jsx
@@ -1,7 +1,16 @@
import React from "react";
+import PropTypes, { number, string, bool } from "prop-types";
import styled from "styled-components";
+import { connect } from "react-redux";
+import { FormattedMessage } from "react-intl";
-import { BORDER_LIGHT_COLOR } from "../../../../app/constants/colors";
+import {
+ BORDER_LIGHT_COLOR,
+ COLOR_CONCRETE
+} from "../../../../app/constants/colors";
+
+import TeamsPageDetails from "./components/TeamsPageDetails";
+import TeamsDataTable from "./components/TeamsDataTable";
const Wrapper = styled.div`
background: white;
@@ -9,12 +18,42 @@ const Wrapper = styled.div`
border: 1px solid ${BORDER_LIGHT_COLOR};
border-top: none;
margin: 0;
- padding: 16px 0 50px 0;
+ padding: 16px 15px 50px 15px;
`;
-const SettingsTeams = () =>
+const TabTitle = styled.div`
+ background-color: ${COLOR_CONCRETE};
+ padding: 15px;
+`;
+
+const SettingsTeams = ({ teams }) =>
- Settings Teams
+
+
+
+
+
;
-export default SettingsTeams;
+SettingsTeams.propTypes = {
+ teams: PropTypes.arrayOf(
+ PropTypes.shape({
+ id: number.isRequired,
+ name: string.isRequired,
+ current_team: bool.isRequired,
+ role: string.isRequired,
+ members: number.isRequired,
+ can_be_leaved: bool.isRequired
+ }).isRequired
+ )
+};
+
+SettingsTeams.defaultProps = {
+ teams: [{id: 0, name: "", current_team: "", role: "", members: 0}]
+};
+
+const mapStateToProps = ({ all_teams }) => ({
+ teams: all_teams.collection
+});
+
+export default connect(mapStateToProps)(SettingsTeams);
diff --git a/app/javascript/packs/src/settings/components/team/actions/UserTeamsActions.js b/app/javascript/packs/src/settings/components/team/actions/UserTeamsActions.js
new file mode 100644
index 000000000..33f07eb5a
--- /dev/null
+++ b/app/javascript/packs/src/settings/components/team/actions/UserTeamsActions.js
@@ -0,0 +1,2 @@
+
+import { LEAVE_TEAM_PATH } from '../../../../../app/routes'
diff --git a/app/javascript/packs/src/settings/components/team/components/TeamsDataTable.jsx b/app/javascript/packs/src/settings/components/team/components/TeamsDataTable.jsx
new file mode 100644
index 000000000..e638f9516
--- /dev/null
+++ b/app/javascript/packs/src/settings/components/team/components/TeamsDataTable.jsx
@@ -0,0 +1,106 @@
+import React, { Component } from "react";
+import PropTypes, { func, number, string, bool } from "prop-types";
+import { connect } from "react-redux";
+import { Button } from "react-bootstrap";
+import _ from "lodash";
+import { FormattedMessage } from "react-intl";
+import { leaveTeamModalShow } from "../../../../../shared/actions/LeaveTeamActions";
+import DataTable from "../../../../../shared/data_table";
+
+class TeamsDataTable extends Component {
+ constructor(props) {
+ super(props);
+
+ this.leaveTeamModal = this.leaveTeamModal.bind(this);
+ this.leaveTeamButton = this.leaveTeamButton.bind(this);
+ }
+
+ leaveTeamModal(e, id) {
+ const team = _.find(this.props.teams, el => el.id === id);
+ this.props.leaveTeamModalShow(true, id, team.name);
+ }
+
+ leaveTeamButton(id) {
+ const team = _.find(this.props.teams, el => el.id === id);
+ if (team.can_be_leaved) {
+ return (
+
+ );
+ }
+ return (
+
+ );
+ }
+
+ render() {
+ const options = {
+ defaultSortName: "name",
+ defaultSortOrder: "desc",
+ sizePerPageList: [10, 25, 50, 100],
+ paginationPosition: "top",
+ alwaysShowAllBtns: false
+ };
+ const columns = [
+ {
+ id: 1,
+ name: "Name",
+ isKey: false,
+ textId: "name",
+ position: 0,
+ dataSort: true
+ },
+ {
+ id: 2,
+ name: "Role",
+ isKey: false,
+ textId: "role",
+ position: 1,
+ dataSort: true
+ },
+ {
+ id: 3,
+ name: "Members",
+ isKey: false,
+ textId: "members",
+ position: 2,
+ dataSort: true
+ },
+ {
+ id: 4,
+ name: "",
+ isKey: true,
+ textId: "id",
+ dataFormat: this.leaveTeamButton,
+ position: 3
+ }
+ ];
+ return (
+
+ );
+ }
+}
+
+TeamsDataTable.propTypes = {
+ leaveTeamModalShow: func.isRequired,
+ teams: PropTypes.arrayOf(
+ PropTypes.shape({
+ id: number.isRequired,
+ name: string.isRequired,
+ current_team: bool.isRequired,
+ role: string.isRequired,
+ members: number.isRequired,
+ can_be_leaved: bool.isRequired
+ }).isRequired
+ )
+};
+
+export default connect(null, { leaveTeamModalShow })(TeamsDataTable);
diff --git a/app/javascript/packs/src/settings/components/team/components/TeamsPageDetails.jsx b/app/javascript/packs/src/settings/components/team/components/TeamsPageDetails.jsx
new file mode 100644
index 000000000..a31b94959
--- /dev/null
+++ b/app/javascript/packs/src/settings/components/team/components/TeamsPageDetails.jsx
@@ -0,0 +1,59 @@
+import React from "react";
+import PropTypes, { number, string, bool } from "prop-types";
+import styled from "styled-components";
+import { FormattedMessage, FormattedPlural } from "react-intl";
+import { Button, Glyphicon } from "react-bootstrap";
+
+const Wrapper = styled.div`margin: 15px 0;`;
+const TeamsPageDetails = ({ teams }) => {
+ const teamsNumber = teams.length;
+ return (
+
+
+ }
+ other={
+
+ }
+ />
+
+
+ );
+};
+
+TeamsPageDetails.propTypes = {
+ teams: PropTypes.arrayOf(
+ PropTypes.shape({
+ id: number.isRequired,
+ name: string.isRequired,
+ current_team: bool.isRequired,
+ role: string.isRequired,
+ members: number.isRequired,
+ can_be_leaved: bool.isRequired
+ })
+ )
+};
+
+TeamsPageDetails.defaultProps = {
+ teams: []
+};
+
+export default TeamsPageDetails;
diff --git a/app/models/user.rb b/app/models/user.rb
index 561f1e2db..b6ced8b27 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -204,6 +204,24 @@ class User < ApplicationRecord
Team.find_by_id(self.current_team_id)
end
+ # Retrieves the data needed in all teams page
+ def teams_data
+ ActiveRecord::Base.connection.execute(
+ ActiveRecord::Base.send(
+ :sanitize_sql_array,
+ ['SELECT teams.id AS id, teams.name AS name, user_teams.role ' \
+ 'AS role, (SELECT COUNT(*) FROM user_teams WHERE ' \
+ 'user_teams.team_id = teams.id) AS members, ' \
+ 'CASE WHEN teams.id=? THEN true ELSE false END AS current_team, ' \
+ 'CASE WHEN (SELECT COUNT(*) FROM user_teams WHERE ' \
+ 'user_teams.team_id=teams.id AND role=2) >= 2 THEN true ELSE false ' \
+ 'END AS can_be_leaved FROM teams INNER JOIN user_teams ON ' \
+ 'teams.id=user_teams.team_id WHERE user_teams.user_id=?',
+ self.current_team_id, self.id]
+ )
+ )
+ end
+
# Search all active users for username & email. Can
# also specify which team to ignore.
def self.search(
diff --git a/app/views/client_api/teams/index.json.jbuilder b/app/views/client_api/teams/index.json.jbuilder
index fb48d4e90..e58e8356c 100644
--- a/app/views/client_api/teams/index.json.jbuilder
+++ b/app/views/client_api/teams/index.json.jbuilder
@@ -1,5 +1,10 @@
-json.array! teams do |team|
- json.id team.id
- json.name team.name
- json.current_team team == current_user.current_team
+json.teams do
+ json.collection teams do |team|
+ json.id team.fetch('id')
+ json.name team.fetch('name')
+ json.members team.fetch('members')
+ json.role retrive_role_name(team.fetch('role') { nil })
+ json.current_team team.fetch('current_team')
+ json.can_be_leaved team.fetch('can_be_leaved')
+ end
end
diff --git a/app/views/client_api/users/user_teams/leave_team.json.jbuilder b/app/views/client_api/users/user_teams/leave_team.json.jbuilder
new file mode 100644
index 000000000..803baf010
--- /dev/null
+++ b/app/views/client_api/users/user_teams/leave_team.json.jbuilder
@@ -0,0 +1,11 @@
+json.teams do
+ json.flash_message flash_message
+ json.collection teams do |team|
+ json.id team.fetch('id')
+ json.name team.fetch('name')
+ json.members team.fetch('members')
+ json.role json.role retrive_role_name(team.fetch('role') { nil })
+ json.current_team team.fetch('current_team')
+ json.can_be_leaved team.fetch('can_be_leaved')
+ end
+end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 5361cc8ac..07989c05b 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1819,3 +1819,8 @@ en:
More: "More"
Added: 'Added'
by: 'by'
+
+ client_api:
+ user_teams:
+ leave_team_error: "An error occured."
+ leave_flash: "Successfuly left team %{team}."
diff --git a/config/routes.rb b/config/routes.rb
index a557b7a66..a19e10477 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -23,6 +23,10 @@ Rails.application.routes.draw do
get '/recent_notifications', to: 'notifications#recent_notifications'
# users
get '/current_user_info', to: 'users#current_user_info'
+
+ namespace :users do
+ delete '/leave_team', to: 'user_teams#leave_team'
+ end
end
# Save sample table state
diff --git a/spec/controllers/client_api/users/user_teams_controller.rb b/spec/controllers/client_api/users/user_teams_controller.rb
new file mode 100644
index 000000000..d1e0604bd
--- /dev/null
+++ b/spec/controllers/client_api/users/user_teams_controller.rb
@@ -0,0 +1,32 @@
+require 'rails_helper'
+
+describe ClientApi::Users::UserTeamsController, type: :controller do
+ describe 'DELETE #leave_team' do
+ login_user
+ before do
+ @user_one = User.first
+ @user_two = FactoryGirl.create(:user, email: 'sec_user@asdf.com')
+ @team = FactoryGirl.create :team
+ FactoryGirl.create :user_team, team: @team, user: @user_one, role: 2
+ end
+
+ it 'Returns HTTP success if user can leave the team' do
+ FactoryGirl.create :user_team, team: @team, user: @user_two, role: 2
+ delete :leave_team, params: { team: @team.id }, format: :json
+ expect(response).to be_success
+ expect(response).to have_http_status(200)
+ end
+
+ it 'Returns HTTP unprocessable_entity if user can\'t leave the team' do
+ delete :leave_team, params: { team: @team.id }, format: :json
+ expect(response).to_not be_success
+ expect(response).to have_http_status(:unprocessable_entity)
+ end
+
+ it 'Returns HTTP unprocessable_entity if no params given' do
+ delete :leave_team, format: :json
+ expect(response).to_not be_success
+ expect(response).to have_http_status(:unprocessable_entity)
+ end
+ end
+end
diff --git a/spec/factories/user_teams.rb b/spec/factories/user_teams.rb
new file mode 100644
index 000000000..b63ecffd4
--- /dev/null
+++ b/spec/factories/user_teams.rb
@@ -0,0 +1,5 @@
+FactoryGirl.define do
+ factory :user_team do
+ role 'admin'
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index aa55b0a06..8a118e209 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -149,4 +149,47 @@ describe User, type: :model do
expect(user.name).to eq 'Axe'
end
end
+
+ describe 'teams_data should return a list of teams' do
+ # needs persistence because is testing a sql query
+ let(:team) { create :team }
+ let(:user_one) do
+ create :user, email: 'user1@asdf.com', current_team_id: team.id
+ end
+ let(:user_two) { create :user, email: 'user2@asdf.com' }
+
+ it 'in a specific format: {id: .., name: .., members: .., role: ' \
+ '.., current_team: .., can_be_leaved: ..}' do
+ create :user_team, team: team, user: user_one
+ expected_result = {
+ id: team.id,
+ name: team.name,
+ members: 1,
+ role: 2,
+ current_team: true,
+ can_be_leaved: false
+ }
+
+ user_one.teams_data.first.each do |k, v|
+ expect(v).to eq(expected_result.fetch(k.to_sym))
+ end
+ end
+
+ it 'should return correct number of team members' do
+ create :user_team, team: team, user: user_one
+ create :user_team, team: team, user: user_two
+ expected_result = {
+ id: team.id,
+ name: team.name,
+ members: 2,
+ role: 2,
+ current_team: true,
+ can_be_leaved: true
+ }
+
+ user_one.teams_data.first.each do |k, v|
+ expect(v).to eq(expected_result.fetch(k.to_sym))
+ end
+ end
+ end
end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index 32c2525a0..c21a559d5 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -2,6 +2,8 @@
require 'spec_helper'
require 'shoulda-matchers'
require 'database_cleaner'
+require 'devise'
+require_relative 'support/controller_macros'
ENV['RAILS_ENV'] = 'test'
require File.expand_path('../../config/environment', __FILE__)
# Prevent database truncation if the environment is production
@@ -78,6 +80,9 @@ RSpec.configure do |config|
# includes FactoryGirl in rspec
config.include FactoryGirl::Syntax::Methods
+ # Devise
+ config.include Devise::Test::ControllerHelpers, type: :controller
+ config.extend ControllerMacros, type: :controller
end
# config shoulda matchers to work with rspec
diff --git a/spec/support/controller_macros.rb b/spec/support/controller_macros.rb
new file mode 100644
index 000000000..eb3b63c90
--- /dev/null
+++ b/spec/support/controller_macros.rb
@@ -0,0 +1,10 @@
+module ControllerMacros
+ def login_user
+ before(:each) do
+ @request.env['devise.mapping'] = Devise.mappings[:user]
+ user = FactoryGirl.create(:user)
+ user.confirm
+ sign_in user
+ end
+ end
+end