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 ###