diff --git a/backend/__init__.py b/backend/__init__.py index 88d8848bbe..b33af8e92b 100644 --- a/backend/__init__.py +++ b/backend/__init__.py @@ -106,7 +106,7 @@ def create_app(env="backend.config.EnvironmentConfig"): env = "backend.config.TestEnvironmentConfig" app.config.from_object(env) # Enable logging to files - # initialise_logger(app) + initialise_logger(app) app.logger.info("Starting up a new Tasking Manager application") # Connect to database @@ -380,7 +380,7 @@ def add_api_endpoints(app): SystemAuthenticationLoginAPI, SystemAuthenticationCallbackAPI, OSMTeamsAuthenticationCallbackAPI, - OSMTeamsAuthenticationAPI + OSMTeamsAuthenticationAPI, ) from backend.api.system.applications import SystemApplicationsRestAPI from backend.api.system.image_upload import SystemImageUploadRestAPI @@ -936,7 +936,8 @@ def add_api_endpoints(app): SystemAuthenticationCallbackAPI, format_url("system/authentication/callback/") ) api.add_resource( - OSMTeamsAuthenticationCallbackAPI, format_url("system/osm-teams-authentication/callback/") + OSMTeamsAuthenticationCallbackAPI, + format_url("system/osm-teams-authentication/callback/"), ) api.add_resource( SystemAuthenticationEmailAPI, format_url("system/authentication/email/") diff --git a/backend/api/system/authentication.py b/backend/api/system/authentication.py index 1bc07bb189..4733e70976 100644 --- a/backend/api/system/authentication.py +++ b/backend/api/system/authentication.py @@ -65,8 +65,8 @@ def get(self): state = AuthenticationService.generate_random_state() osm_teams.state = state login_url, state = osm_teams.authorization_url( - EnvironmentConfig.OSM_TEAMS_AUTH_URL - ) + EnvironmentConfig.OSM_TEAMS_AUTH_URL + ) return {"auth_url": login_url, "state": state}, 200 diff --git a/frontend/src/components/teamsAndOrgs/messages.js b/frontend/src/components/teamsAndOrgs/messages.js index 170927d135..25e1c679b9 100644 --- a/frontend/src/components/teamsAndOrgs/messages.js +++ b/frontend/src/components/teamsAndOrgs/messages.js @@ -659,4 +659,16 @@ export default defineMessages({ id: 'management.stats.overview', defaultMessage: 'Overview', }, + joinTeam: { + id: 'teamsAndOrgs.management.button.join_team', + defaultMessage: 'Join team', + }, + cancelRequest: { + id: 'teamsAndOrgs.management.button.cancel_request', + defaultMessage: 'Cancel request', + }, + leaveTeam: { + id: 'teamsAndOrgs.management.button.leave_team', + defaultMessage: 'Leave team', + }, }); diff --git a/frontend/src/components/teamsAndOrgs/teams.js b/frontend/src/components/teamsAndOrgs/teams.js index 6732478f13..a305d4b2c6 100644 --- a/frontend/src/components/teamsAndOrgs/teams.js +++ b/frontend/src/components/teamsAndOrgs/teams.js @@ -7,13 +7,17 @@ import { Form, Field, useFormState } from 'react-final-form'; import ReactTooltip from 'react-tooltip'; import messages from './messages'; -import { InfoIcon } from '../svgIcons'; +import { ExternalLinkIcon, InfoIcon } from '../svgIcons'; import { useEditTeamAllowed } from '../../hooks/UsePermissions'; import { UserAvatar, UserAvatarList } from '../user/avatar'; import { AddButton, ViewAllLink, Management, VisibilityBox, JoinMethodBox } from './management'; import { RadioField, OrganisationSelectInput, TextField } from '../formInputs'; -import { Button, EditButton } from '../button'; +import { Button, CustomButton, EditButton } from '../button'; import { nCardPlaceholders } from './teamsPlaceholder'; +import { OSM_TEAMS_API_URL } from '../../config'; +import { Alert } from '../alert'; +import Popup from 'reactjs-popup'; +import { LeaveTeamConfirmationAlert } from './leaveTeamConfirmationAlert'; export function TeamsManagement({ teams, @@ -416,6 +420,23 @@ export function TeamSideBar({ team, members, managers, requestedToJoin }: Object )} + {team.osm_teams_id && ( + + {' '} + + + + + + )} @@ -463,3 +484,59 @@ export const TeamBox = ({ team, className }: Object) => ( ); + +export const TeamDetailPageFooter = ({ team, isMember, joinTeamFn, leaveTeamFn }) => { + return ( +
+
+ + + + + +
+
+ {isMember ? ( + + + + } + modal + closeOnEscape + > + {(close) => ( + + )} + + ) : ( + team.joinMethod !== 'BY_INVITE' && ( + joinTeamFn()} + disabled={team.joinMethod === 'OSM_TEAMS'} + > + + + ) + )} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/teamsAndOrgs/tests/teams.test.js b/frontend/src/components/teamsAndOrgs/tests/teams.test.js index 88015c7897..de0162f2eb 100644 --- a/frontend/src/components/teamsAndOrgs/tests/teams.test.js +++ b/frontend/src/components/teamsAndOrgs/tests/teams.test.js @@ -3,6 +3,7 @@ import TestRenderer from 'react-test-renderer'; import { render, screen, waitFor, act } from '@testing-library/react'; import { FormattedMessage } from 'react-intl'; import { MemoryRouter } from 'react-router-dom'; +import userEvent from '@testing-library/user-event'; import { createComponentWithIntl, @@ -11,7 +12,7 @@ import { renderWithRouter, createComponentWithMemoryRouter, } from '../../../utils/testWithIntl'; -import { TeamBox, TeamsBoxList, TeamsManagement, Teams, TeamCard, TeamSideBar } from '../teams'; +import { TeamBox, TeamsBoxList, TeamsManagement, Teams, TeamCard, TeamSideBar, TeamDetailPageFooter } from '../teams'; import { store } from '../../../store'; import { teams, team } from '../../../network/tests/mockData/teams'; @@ -401,4 +402,149 @@ describe('TeamSideBar component', () => { }), ).not.toBeInTheDocument(); }); + + it('when OSM Teams sync is enabled, it should show a message', () => { + const teamWithOSMTeams = {...team}; + teamWithOSMTeams.osm_teams_id = 1234; + teamWithOSMTeams.joinMethod = 'OSM_TEAMS'; + renderWithRouter( + + + , + ); + + expect( + screen.getByText( + 'The members and managers of this team are configured through the OSM Teams platform.' + ) + ).toBeInTheDocument(); + expect( + screen.getByText('Open on OSM Teams').href.endsWith('/teams/1234') + ).toBeTruthy(); + }); }); + + +describe('TeamDetailPageFooter component', () => { + const joinTeamFn = jest.fn(); + const leaveTeamFn = jest.fn(); + + it('has Join team button enabled for ANY joinMethod if user is not member', async () => { + renderWithRouter( + + + + ); + expect(screen.getByText('Join team').disabled).toBeFalsy(); + await userEvent.click(screen.getByText('Join team')); + expect(joinTeamFn).toHaveBeenCalledTimes(1); + expect( + screen.getByRole('link').href.endsWith('/contributions/teams') + ).toBeTruthy(); + }); + + it('has Leave team button enabled for ANY joinMethod if user is a member', async () => { + renderWithRouter( + + + + ); + expect(screen.getByText('Leave team').disabled).toBeFalsy(); + await userEvent.click(screen.getByText('Leave team')); + await userEvent.click(screen.getByText('Leave')); + expect(leaveTeamFn).toHaveBeenCalledTimes(1); + expect( + screen.getByRole('link').href.endsWith('/contributions/teams') + ).toBeTruthy(); + }); + + it('has Join team button enabled for BY_REQUEST joinMethod if user is not member', async () => { + renderWithRouter( + + + + ); + expect(screen.getByText('Join team').disabled).toBeFalsy(); + await userEvent.click(screen.getByText('Join team')); + expect(joinTeamFn).toHaveBeenCalledTimes(1); + }); + + it('has Leave team button enabled for BY_REQUEST joinMethod if user is a member', async () => { + renderWithRouter( + + + + ); + expect(screen.getByText('Leave team').disabled).toBeFalsy(); + await userEvent.click(screen.getByText('Leave team')); + await userEvent.click(screen.getByText('Leave')); + expect(leaveTeamFn).toHaveBeenCalledTimes(1); + }); + + it('has Leave team button enabled for BY_INVITE joinMethod if user is a member', async () => { + renderWithRouter( + + + + ); + expect(screen.getByText('Leave team').disabled).toBeFalsy(); + await userEvent.click(screen.getByText('Leave team')); + await userEvent.click(screen.getByText('Leave')); + expect(leaveTeamFn).toHaveBeenCalledTimes(1); + }); + + it('has Join team button disabled for OSM_TEAMS joinMethod if user is not a member', async () => { + renderWithRouter( + + + + ); + expect(screen.getByText('Join team').disabled).toBeTruthy(); + await userEvent.click(screen.getByText('Join team')); + expect(joinTeamFn).toHaveBeenCalledTimes(0); + }); + + it('has Leave team button disabled for OSM_TEAMS joinMethod if user is a member', async () => { + renderWithRouter( + + + + ); + expect(screen.getByText('Leave team').disabled).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/frontend/src/views/messages.js b/frontend/src/views/messages.js index 207d265491..b99129bacb 100644 --- a/frontend/src/views/messages.js +++ b/frontend/src/views/messages.js @@ -133,22 +133,6 @@ export default defineMessages({ id: 'teamsAndOrgs.management.campaign.button.create', defaultMessage: 'Create campaign', }, - myTeams: { - id: 'teamsAndOrgs.management.button.my_teams', - defaultMessage: 'My teams', - }, - joinTeam: { - id: 'teamsAndOrgs.management.button.join_team', - defaultMessage: 'Join team', - }, - cancelRequest: { - id: 'teamsAndOrgs.management.button.cancel_request', - defaultMessage: 'Cancel request', - }, - leaveTeam: { - id: 'teamsAndOrgs.management.button.leave_team', - defaultMessage: 'Leave team', - }, cancel: { id: 'teamsAndOrgs.management.button.cancel', defaultMessage: 'Cancel', diff --git a/frontend/src/views/teams.js b/frontend/src/views/teams.js index b1227db2c2..702970afa3 100644 --- a/frontend/src/views/teams.js +++ b/frontend/src/views/teams.js @@ -13,7 +13,6 @@ import { } from 'use-query-params'; import { stringify } from 'query-string'; import toast from 'react-hot-toast'; -import Popup from 'reactjs-popup'; import messages from './messages'; import { OSM_TEAMS_CLIENT_ID } from '../config'; @@ -36,10 +35,10 @@ import { TeamForm, TeamsManagement, TeamSideBar, + TeamDetailPageFooter, } from '../components/teamsAndOrgs/teams'; import { MessageMembers } from '../components/teamsAndOrgs/messageMembers'; import { Projects } from '../components/teamsAndOrgs/projects'; -import { LeaveTeamConfirmationAlert } from '../components/teamsAndOrgs/leaveTeamConfirmationAlert'; import { FormSubmitButton, CustomButton } from '../components/button'; import { DeleteModal } from '../components/deleteModal'; import { NotFound } from './notFound'; @@ -591,55 +590,12 @@ export function TeamDetail() { /> -
-
- - - - - -
-
- {isMember ? ( - - - - } - modal - closeOnEscape - > - {(close) => ( - - )} - - ) : ( - team.joinMethod !== 'BY_INVITE' && ( - joinTeam()} - > - - - ) - )} -
-
+ ); } diff --git a/frontend/src/views/tests/teams.test.js b/frontend/src/views/tests/teams.test.js index 2dd3ba66dd..a6d7a3324f 100644 --- a/frontend/src/views/tests/teams.test.js +++ b/frontend/src/views/tests/teams.test.js @@ -1,5 +1,5 @@ import '@testing-library/jest-dom'; -import { screen, waitFor, within, act, render } from '@testing-library/react'; +import { screen, waitFor, within, act } from '@testing-library/react'; import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6'; import { QueryParamProvider } from 'use-query-params'; import userEvent from '@testing-library/user-event'; @@ -14,7 +14,6 @@ import { ManageTeams, EditTeam, CreateTeam, MyTeams } from '../teams'; import { store } from '../../store'; import { setupFaultyHandlers } from '../../network/tests/server'; import * as config from '../../config'; -import { teamWithOSMTeams } from '../../network/tests/mockData/teams'; jest.mock('react-hot-toast', () => ({ success: jest.fn(), diff --git a/migrations/versions/52a67f6cef20_.py b/migrations/versions/52a67f6cef20_.py index 99337cf00a..b685ba0e80 100644 --- a/migrations/versions/52a67f6cef20_.py +++ b/migrations/versions/52a67f6cef20_.py @@ -1,8 +1,8 @@ """empty message Revision ID: 52a67f6cef20 -Revises: a9cbd2c6c213 -Create Date: 2023-01-12 12:26:39.420411 +Revises: 42c45e74752b +Create Date: 2023-07-06 12:26:39.420411 """ from alembic import op @@ -10,19 +10,19 @@ # revision identifiers, used by Alembic. -revision = '52a67f6cef20' -down_revision = 'a9cbd2c6c213' +revision = "52a67f6cef20" +down_revision = "42c45e74752b" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('teams', sa.Column('osm_teams_id', sa.BigInteger(), nullable=True)) + op.add_column("teams", sa.Column("osm_teams_id", sa.BigInteger(), nullable=True)) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('teams', 'osm_teams_id') + op.drop_column("teams", "osm_teams_id") # ### end Alembic commands ###