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