diff --git a/backend/api/issues/resources.py b/backend/api/issues/resources.py
index 737f4f84ed..d392c7d154 100644
--- a/backend/api/issues/resources.py
+++ b/backend/api/issues/resources.py
@@ -7,7 +7,7 @@
from backend.models.dtos.mapping_issues_dto import MappingIssueCategoryDTO
from backend.models.dtos.user_dto import AuthUserDTO
from backend.services.mapping_issues_service import MappingIssueCategoryService
-from backend.services.users.authentication_service import pm_only
+from backend.services.users.authentication_service import admin_only
router = APIRouter(
prefix="/tasks",
@@ -52,7 +52,7 @@ async def get_issue(category_id: int, db: Database = Depends(get_db)):
async def patch_issue(
request: Request,
category_id: int,
- user: AuthUserDTO = Depends(pm_only),
+ user: AuthUserDTO = Depends(admin_only),
db: Database = Depends(get_db),
data: MappingIssueCategoryDTO = Body(...),
):
@@ -121,7 +121,7 @@ async def patch_issue(
async def delete_issue(
request: Request,
category_id: int,
- user: AuthUserDTO = Depends(pm_only),
+ user: AuthUserDTO = Depends(admin_only),
db: Database = Depends(get_db),
):
"""
@@ -200,7 +200,7 @@ async def get_issues_categories(
@router.post("/issues/categories/", response_model=MappingIssueCategoryDTO)
async def post_issues_categories(
request: Request,
- user: AuthUserDTO = Depends(pm_only),
+ user: AuthUserDTO = Depends(admin_only),
db: Database = Depends(get_db),
data: dict = Body(...),
):
diff --git a/backend/api/licenses/resources.py b/backend/api/licenses/resources.py
index 429bb1ad86..8cf277a2a1 100644
--- a/backend/api/licenses/resources.py
+++ b/backend/api/licenses/resources.py
@@ -6,7 +6,7 @@
from backend.models.dtos.licenses_dto import LicenseDTO
from backend.models.dtos.user_dto import AuthUserDTO
from backend.services.license_service import LicenseService
-from backend.services.users.authentication_service import pm_only
+from backend.services.users.authentication_service import admin_only
router = APIRouter(
prefix="/licenses",
@@ -19,7 +19,7 @@
async def post_license(
license_dto: LicenseDTO,
db: Database = Depends(get_db),
- user: AuthUserDTO = Depends(pm_only),
+ user: AuthUserDTO = Depends(admin_only),
):
"""
Creates a new mapping license
@@ -100,7 +100,7 @@ async def patch_license(
license_dto: LicenseDTO,
license_id: int,
db: Database = Depends(get_db),
- user: AuthUserDTO = Depends(pm_only),
+ user: AuthUserDTO = Depends(admin_only),
):
"""
Update a specified mapping license
@@ -155,7 +155,7 @@ async def patch_license(
async def delete_license(
license_id: int,
db: Database = Depends(get_db),
- user: AuthUserDTO = Depends(pm_only),
+ user: AuthUserDTO = Depends(admin_only),
):
"""
Delete a specified mapping license
diff --git a/backend/api/mapping_badges/resources.py b/backend/api/mapping_badges/resources.py
index 580454ffe9..5243646694 100644
--- a/backend/api/mapping_badges/resources.py
+++ b/backend/api/mapping_badges/resources.py
@@ -11,7 +11,7 @@
)
from backend.models.dtos.user_dto import AuthUserDTO
from backend.services.mapping_badges import MappingBadgeService
-from backend.services.users.authentication_service import pm_only
+from backend.services.users.authentication_service import admin_only
router = APIRouter(
prefix="/badges",
@@ -39,7 +39,7 @@ async def get_mapping_badges(
async def create_mapping_badge(
data: MappingBadgeCreateDTO,
db: Database = Depends(get_db),
- user: AuthUserDTO = Depends(pm_only),
+ user: AuthUserDTO = Depends(admin_only),
) -> MappingBadgeDTO:
"""
Creates a new MappingBadge
@@ -73,7 +73,7 @@ async def update_mapping_badge(
data: MappingBadgeUpdateDTO,
badge_id: int,
db: Database = Depends(get_db),
- user: AuthUserDTO = Depends(pm_only),
+ user: AuthUserDTO = Depends(admin_only),
) -> MappingBadgeDTO:
"""
Updates a mapping badge
@@ -92,7 +92,7 @@ async def update_mapping_badge(
async def delete_mapping_badge(
badge_id: int,
db: Database = Depends(get_db),
- user: AuthUserDTO = Depends(pm_only),
+ user: AuthUserDTO = Depends(admin_only),
):
"""
Deletes a mapping badge
diff --git a/backend/api/mapping_levels/resources.py b/backend/api/mapping_levels/resources.py
index 569e6f8164..2ede8009bf 100644
--- a/backend/api/mapping_levels/resources.py
+++ b/backend/api/mapping_levels/resources.py
@@ -8,7 +8,7 @@
)
from backend.models.dtos.user_dto import AuthUserDTO
from backend.services.mapping_levels import MappingLevelService
-from backend.services.users.authentication_service import pm_only
+from backend.services.users.authentication_service import admin_only
router = APIRouter(
prefix="/levels",
@@ -36,7 +36,7 @@ async def get_mapping_levels(
async def create_mapping_level(
data: MappingLevelCreateDTO,
db: Database = Depends(get_db),
- user: AuthUserDTO = Depends(pm_only),
+ user: AuthUserDTO = Depends(admin_only),
):
"""
Create a new mapping level
@@ -70,7 +70,7 @@ async def update_mapping_level(
data: MappingLevelUpdateDTO,
level_id: int,
db: Database = Depends(get_db),
- user: AuthUserDTO = Depends(pm_only),
+ user: AuthUserDTO = Depends(admin_only),
):
"""
Update a given mapping level
@@ -89,7 +89,7 @@ async def update_mapping_level(
async def delete_mapping_level(
level_id: int,
db: Database = Depends(get_db),
- user: AuthUserDTO = Depends(pm_only),
+ user: AuthUserDTO = Depends(admin_only),
):
"""
Delete the specified mapping level
diff --git a/backend/api/users/actions.py b/backend/api/users/actions.py
index 85ba80b5ba..3c1f23a753 100644
--- a/backend/api/users/actions.py
+++ b/backend/api/users/actions.py
@@ -7,7 +7,7 @@
from backend.models.dtos.user_dto import AuthUserDTO, UserDTO, UserRegisterEmailDTO
from backend.services.interests_service import InterestService
from backend.services.messaging.message_service import MessageService
-from backend.services.users.authentication_service import login_required, pm_only
+from backend.services.users.authentication_service import login_required, admin_only
from backend.services.users.user_service import UserService, UserServiceError
router = APIRouter(
@@ -117,7 +117,7 @@ async def set_mapping_level(
request: Request,
username,
level,
- user: AuthUserDTO = Depends(pm_only),
+ user: AuthUserDTO = Depends(admin_only),
db: Database = Depends(get_db),
):
"""
@@ -173,7 +173,7 @@ async def set_user_role(
request: Request,
username: str,
role: str,
- user: AuthUserDTO = Depends(pm_only),
+ user: AuthUserDTO = Depends(admin_only),
db: Database = Depends(get_db),
):
"""
@@ -228,7 +228,7 @@ async def set_user_role(
async def update_stats(
request: Request,
username: str,
- _: AuthUserDTO = Depends(pm_only),
+ _: AuthUserDTO = Depends(admin_only),
db: Database = Depends(get_db),
):
"""
@@ -255,7 +255,7 @@ async def update_stats(
async def approve_level(
request: Request,
username: str,
- voter: AuthUserDTO = Depends(pm_only),
+ voter: AuthUserDTO = Depends(admin_only),
db: Database = Depends(get_db),
):
"""
@@ -282,7 +282,7 @@ async def set_user_is_expert(
request: Request,
user_name,
is_expert,
- user: AuthUserDTO = Depends(pm_only),
+ user: AuthUserDTO = Depends(admin_only),
db: Database = Depends(get_db),
):
"""
diff --git a/backend/main.py b/backend/main.py
index 70e7f837d2..29b10d91c1 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -10,7 +10,6 @@
from pyinstrument import Profiler
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
from starlette.middleware.authentication import AuthenticationMiddleware
-
from backend.config import settings
from backend.db import db_connection
from backend.exceptions import BadRequest, Conflict, Forbidden, NotFound, Unauthorized
@@ -55,16 +54,21 @@ async def lifespan(app):
# Custom exception handler for invalid token and logout.
@_app.exception_handler(HTTPException)
async def custom_http_exception_handler(request: Request, exc: HTTPException):
- if exc.status_code == 401 and "InvalidToken" in exc.detail.get("SubCode", ""):
- return JSONResponse(
- content={
- "Error": exc.detail["Error"],
- "SubCode": exc.detail["SubCode"],
- },
- status_code=exc.status_code,
- headers={"WWW-Authenticate": "Bearer"},
- )
-
+ try:
+ if exc.status_code == 401 and "InvalidToken" in exc.detail.get(
+ "SubCode", ""
+ ):
+ return JSONResponse(
+ content={
+ "Error": exc.detail["Error"],
+ "SubCode": exc.detail["SubCode"],
+ },
+ status_code=exc.status_code,
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+ except Exception as e:
+ logging.debug(f"Exception while handling custom HTTPException: {e}")
+ pass
if isinstance(exc.detail, dict) and "error" in exc.detail:
error_response = exc.detail
else:
diff --git a/backend/models/postgis/project.py b/backend/models/postgis/project.py
index 26d56b7258..2fda8b6e97 100644
--- a/backend/models/postgis/project.py
+++ b/backend/models/postgis/project.py
@@ -29,6 +29,7 @@
orm,
select,
update,
+ UniqueConstraint,
)
from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy.ext.hybrid import hybrid_property
@@ -96,10 +97,16 @@
class ProjectTeams(Base):
__tablename__ = "project_teams"
- team_id = Column(Integer, ForeignKey("teams.id"), primary_key=True)
- project_id = Column(Integer, ForeignKey("projects.id"), primary_key=True)
+
+ id = Column(BigInteger, primary_key=True, autoincrement=True)
+ team_id = Column(Integer, ForeignKey("teams.id"), nullable=False)
+ project_id = Column(Integer, ForeignKey("projects.id"), nullable=False)
role = Column(Integer, nullable=False)
+ __table_args__ = (
+ UniqueConstraint("team_id", "project_id", "role", name="uq_project_team_role"),
+ )
+
project = relationship(
"Project", backref=backref("teams", cascade="all, delete-orphan")
)
diff --git a/backend/models/postgis/task.py b/backend/models/postgis/task.py
index 4e2d9b974f..2af49565a8 100644
--- a/backend/models/postgis/task.py
+++ b/backend/models/postgis/task.py
@@ -1396,7 +1396,8 @@ async def get_tasks_as_geojson_feature_collection(
t.task_status,
ST_AsGeoJSON(t.geometry) AS geojson,
t.locked_by,
- t.mapped_by
+ t.mapped_by,
+ t.validated_by
FROM tasks t
WHERE t.project_id = :project_id
"""
@@ -1449,6 +1450,7 @@ async def get_tasks_as_geojson_feature_collection(
taskStatus=TaskStatus(row["task_status"]).name,
lockedBy=row["locked_by"],
mappedBy=row["mapped_by"],
+ validatedBy=row["validated_by"],
)
feature = geojson.Feature(
geometry=task_geometry, properties=task_properties
diff --git a/backend/services/users/authentication_service.py b/backend/services/users/authentication_service.py
index ee6d71b53a..714ce57561 100644
--- a/backend/services/users/authentication_service.py
+++ b/backend/services/users/authentication_service.py
@@ -70,18 +70,18 @@ def verify_token(token):
class TokenAuthBackend(AuthenticationBackend):
async def authenticate(self, conn):
if "authorization" not in conn.headers:
- return
+ return None
auth = conn.headers["authorization"]
try:
scheme, credentials = auth.split()
if scheme.lower() != "token":
- return
+ return None
try:
decoded_token = base64.b64decode(credentials).decode("ascii")
except UnicodeDecodeError:
logger.debug("Unable to decode token")
- return False
+ return None
except (ValueError, UnicodeDecodeError, binascii.Error):
raise AuthenticationError("Invalid auth credentials")
@@ -90,7 +90,7 @@ async def authenticate(self, conn):
)
if not valid_token:
logger.debug("Token not valid.")
- return
+ return None
tm.authenticated_user_id = user_id
return AuthCredentials(["authenticated"]), SimpleUser(user_id)
@@ -251,7 +251,6 @@ async def login_required(
raise AuthenticationError("Invalid auth credentials")
valid_token, user_id = AuthenticationService.is_valid_token(decoded_token, 604800)
if not valid_token:
- logger.debug("Token not valid")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={"Error": "Token is expired or invalid", "SubCode": "InvalidToken"},
@@ -275,17 +274,23 @@ async def login_required_optional(
decoded_token = base64.b64decode(credentials).decode("ascii")
except UnicodeDecodeError:
logger.debug("Unable to decode token")
- raise HTTPException(status_code=401, detail="Invalid token")
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail={
+ "Error": "Token is expired or invalid",
+ "SubCode": "InvalidToken",
+ },
+ headers={"WWW-Authenticate": "Bearer"},
+ )
except (ValueError, UnicodeDecodeError, binascii.Error):
raise AuthenticationError("Invalid auth credentials")
valid_token, user_id = AuthenticationService.is_valid_token(decoded_token, 604800)
if not valid_token:
- logger.debug("Token not valid")
return None
return AuthUserDTO(id=user_id)
-async def pm_only(
+async def admin_only(
Authorization: str = Security(APIKeyHeader(name="Authorization")),
db: Database = Depends(get_db),
):
diff --git a/frontend/src/components/comments/commentInput.js b/frontend/src/components/comments/commentInput.js
index 20f6826123..e567c6859d 100644
--- a/frontend/src/components/comments/commentInput.js
+++ b/frontend/src/components/comments/commentInput.js
@@ -1,4 +1,4 @@
-import { useRef, useEffect, useState } from 'react';
+import { useRef, useEffect, useState, useCallback } from 'react';
import { useSelector } from 'react-redux';
import MDEditor from '@uiw/react-md-editor';
import Tribute from 'tributejs';
@@ -21,6 +21,7 @@ import { CurrentUserAvatar } from '../user/avatar';
const maxFileSize = 1 * 1024 * 1024; // 1MB
function CommentInputField({
+ sessionkey,
comment,
setComment,
contributors,
@@ -130,6 +131,23 @@ function CommentInputField({
});
};
+ useEffect(() => {
+ if (!sessionkey) return;
+ const commenEvent = sessionStorage.getItem(sessionkey);
+ if (commenEvent) {
+ setComment(commenEvent);
+ }
+ }, [sessionkey, setComment]);
+
+ const onCommentChange = useCallback(
+ (e) => {
+ setComment(e);
+ if (!sessionkey) return;
+ sessionStorage.setItem(sessionkey, e);
+ },
+ [sessionkey, setComment],
+ );
+
return (
{isShowTabNavs && (
@@ -165,7 +183,7 @@ function CommentInputField({
extraCommands={[]}
height={200}
value={comment}
- onChange={setComment}
+ onChange={onCommentChange}
textareaProps={{
...getInputProps(),
spellCheck: 'true',
diff --git a/frontend/src/components/partners/partnersActivity.js b/frontend/src/components/partners/partnersActivity.js
index 4399396c56..9a2359b9aa 100644
--- a/frontend/src/components/partners/partnersActivity.js
+++ b/frontend/src/components/partners/partnersActivity.js
@@ -6,7 +6,7 @@ import PartnersProgresBar from './partnersProgresBar';
import messages from './messages';
import { OHSOME_STATS_API_URL } from '../../config';
-export const Activity = ({ partner }) => {
+export const Activity = ({ partner }: Object) => {
const [data, setData] = useState(null);
const fetchData = async () => {
@@ -17,10 +17,12 @@ export const Activity = ({ partner }) => {
}
primaryHashtag = primaryHashtag.toLowerCase();
- const secondaryHashtags = partner.secondary_hashtag
- ?.split(',')
- ?.map((tag) => tag.trim().replace('#', '').toLowerCase())
- ?.join(',');
+ const secondaryHashtags =
+ partner.secondary_hashtag
+ ?.split(',')
+ ?.map((tag) => tag.trim().replace('#', '').toLowerCase())
+ ?.join(',') || '';
+
const response = await fetch(
OHSOME_STATS_API_URL + `/stats/hashtags/${primaryHashtag},${secondaryHashtags}`,
);
diff --git a/frontend/src/components/projectDetail/questionsAndComments.js b/frontend/src/components/projectDetail/questionsAndComments.js
index 4fb083ef64..d2a00ed2ed 100644
--- a/frontend/src/components/projectDetail/questionsAndComments.js
+++ b/frontend/src/components/projectDetail/questionsAndComments.js
@@ -26,12 +26,14 @@ export const PostProjectComment = ({ projectId, refetchComments, contributors })
const token = useSelector((state) => state.auth.token);
const locale = useSelector((state) => state.preferences['locale']);
const [comment, setComment] = useState('');
+ const SESSION_KEY = 'project-comment';
const mutation = useMutation({
mutationFn: () => postProjectComment(projectId, comment, token, locale),
onSuccess: () => {
refetchComments();
setComment('');
+ sessionStorage.clear(SESSION_KEY);
},
});
@@ -44,6 +46,7 @@ export const PostProjectComment = ({ projectId, refetchComments, contributors })
}>
{
);
const { data: teamsData, isFetching: isTeamsLoading } = useTeamsQuery({ omitMemberList: true });
- const teamRoles = [
- { value: 'MAPPER', label: 'Mapper' },
- { value: 'VALIDATOR', label: 'Validator' },
- { value: 'PROJECT_MANAGER', label: 'Project Manager' },
- ];
+ const teamRoles = useMemo(
+ () => [
+ { value: 'MAPPER', label: 'Mapper' },
+ { value: 'VALIDATOR', label: 'Validator' },
+ { value: 'PROJECT_MANAGER', label: 'Project Manager' },
+ ],
+ [],
+ );
const getLabel = (value) => {
return teamRoles.filter((r) => r.value === value)[0].label;
};
- const editTeam = (id) => {
- const team = projectInfo.teams.filter((t) => t.teamId === id)[0];
+ const editTeam = (teamId, roleValue) => {
+ const team = projectInfo.teams.filter((t) => t.teamId === teamId && t.role === roleValue)[0];
const role = teamRoles.filter((r) => team.role === r.value)[0];
setTeamSelect((t) => {
@@ -46,8 +49,8 @@ export const TeamSelect = () => {
});
};
- const removeTeam = (id) => {
- const teams = projectInfo.teams.filter((t) => t.teamId !== id);
+ const removeTeam = (teamId, roleValue) => {
+ const teams = projectInfo.teams.filter((t) => !(t.teamId === teamId && t.role === roleValue));
setProjectInfo({ ...projectInfo, teams: teams });
};
@@ -69,7 +72,7 @@ export const TeamSelect = () => {
const updateTeam = () => {
const teams = projectInfo.teams.map((t) => {
let item = t;
- if (t.teamId === teamSelect.team.teamId) {
+ if (t.teamId === teamSelect.team.teamId && t.role === teamSelect.team.role) {
item = newTeam();
}
return item;
@@ -99,6 +102,16 @@ export const TeamSelect = () => {
];
}
+ // Filter out assigned roles and display only remaining roles in options
+ const roleList = useCallback(() => {
+ const existingRolesOfSelectdTeam = projectInfo.teams.reduce(
+ (prev, curr) => (curr.teamId === teamSelect.team?.teamId ? [...prev, curr.role] : prev),
+ [],
+ );
+
+ return teamRoles.filter((role) => !existingRolesOfSelectdTeam.includes(role.value));
+ }, [projectInfo.teams, teamRoles, teamSelect]);
+
return (
@@ -117,12 +130,15 @@ export const TeamSelect = () => {
{getLabel(t.role)}
-
editTeam(t.teamId)}>
+ editTeam(t.teamId, t.role)}
+ >
removeTeam(t.teamId)}
+ onClick={() => removeTeam(t.teamId, t.role)}
>
@@ -161,7 +177,7 @@ export const TeamSelect = () => {
classNamePrefix="react-select"
getOptionLabel={(option) => option.label}
getOptionValue={(option) => option.value}
- options={teamRoles}
+ options={roleList()}
onChange={(value) => handleSelect(value, 'role')}
className="w-40 fl mr2 z-3"
isDisabled={teamSelect.team.name === null ? true : false}
diff --git a/frontend/src/hooks/UsePermissions.js b/frontend/src/hooks/UsePermissions.js
index 53ae3d5cc4..adb376bab9 100644
--- a/frontend/src/hooks/UsePermissions.js
+++ b/frontend/src/hooks/UsePermissions.js
@@ -29,7 +29,7 @@ export function useEditProjectAllowed(project) {
export function useEditTeamAllowed(team) {
const userDetails = useSelector((state) => state.auth.userDetails);
const organisations = useSelector((state) => state.auth.organisations);
- const pmTeams = useSelector((state) => state.auth.pmTeams);
+ const tmTeams = useSelector((state) => state.auth.tmTeams);
const [isAllowed, setIsAllowed] = useState(false);
useEffect(() => {
@@ -39,9 +39,11 @@ export function useEditTeamAllowed(team) {
if (organisations && organisations.includes(team.organisation_id)) setIsAllowed(true);
// team managers can edit it
// verify from the redux store
- if (pmTeams && pmTeams.includes(team.teamId)) setIsAllowed(true);
- // and verify based on the team members list
+ // removed pm and use tm list
+ if (tmTeams && tmTeams?.includes(team?.teamId)) setIsAllowed(true);
+
if (team.members) {
+ // and verify based on the team members list
const managers = team.members
.filter((member) => member.active && member.function === 'MANAGER')
.map((member) => member.username);
@@ -49,7 +51,7 @@ export function useEditTeamAllowed(team) {
setIsAllowed(true);
}
}
- }, [pmTeams, userDetails.role, userDetails.username, organisations, team]);
+ }, [userDetails.role, userDetails.username, organisations, team, tmTeams]);
return [isAllowed];
}
diff --git a/frontend/src/hooks/tests/UseEditTeamPermissions.test.js b/frontend/src/hooks/tests/UseEditTeamPermissions.test.js
index d003846f39..29a42fc316 100644
--- a/frontend/src/hooks/tests/UseEditTeamPermissions.test.js
+++ b/frontend/src/hooks/tests/UseEditTeamPermissions.test.js
@@ -37,7 +37,7 @@ describe('test edit team permissions based on manager permissions', () => {
};
it('team manager CAN edit it - verify based on redux store', () => {
act(() => {
- store.dispatch({ type: 'SET_PM_TEAMS', teams: [1, 2, 3] });
+ store.dispatch({ type: 'SET_TM_TEAMS', teams: [1, 2, 3] });
});
const wrapper = ({ children }) => {children};
const { result } = renderHook(() => useEditTeamAllowed(team), { wrapper });
@@ -48,7 +48,7 @@ describe('test edit team permissions based on manager permissions', () => {
it('team manager CAN edit it - verify based on team members', () => {
const userDetails = { username: 'test', role: 'MAPPER' };
act(() => {
- store.dispatch({ type: 'SET_PM_TEAMS', teams: [] });
+ store.dispatch({ type: 'SET_TM_TEAMS', teams: [] });
store.dispatch({ type: 'SET_USER_DETAILS', userDetails: userDetails });
});
const wrapper = ({ children }) => {children};
@@ -60,7 +60,7 @@ describe('test edit team permissions based on manager permissions', () => {
it('MAPPER can not edit it - verify based on team members', () => {
const userDetails = { username: 'another_user', role: 'MAPPER' };
act(() => {
- store.dispatch({ type: 'SET_PM_TEAMS', teams: [] });
+ store.dispatch({ type: 'SET_TM_TEAMS', teams: [] });
store.dispatch({ type: 'SET_USER_DETAILS', userDetails: userDetails });
});
const wrapper = ({ children }) => {children};
@@ -71,7 +71,7 @@ describe('test edit team permissions based on manager permissions', () => {
it('user that is NOT a team manager can not edit it', () => {
act(() => {
- store.dispatch({ type: 'SET_PM_TEAMS', teams: [2, 3] });
+ store.dispatch({ type: 'SET_TM_TEAMS', teams: [2, 3] });
});
const wrapper = ({ children }) => {children};
const { result } = renderHook(() => useEditTeamAllowed(team), { wrapper });
diff --git a/frontend/src/store/actions/auth.js b/frontend/src/store/actions/auth.js
index ef38b19b94..bac4d9d691 100644
--- a/frontend/src/store/actions/auth.js
+++ b/frontend/src/store/actions/auth.js
@@ -8,6 +8,7 @@ export const types = {
SET_OSM: 'SET_OSM',
SET_ORGANISATIONS: 'SET_ORGANISATIONS',
SET_PM_TEAMS: 'SET_PM_TEAMS',
+ SET_TM_TEAMS: 'SET_TM_TEAMS',
UPDATE_OSM_INFO: 'UPDATE_OSM_INFO',
GET_USER_DETAILS: 'GET_USER_DETAILS',
SET_TOKEN: 'SET_TOKEN',
@@ -75,6 +76,13 @@ export function updatePMsTeams(teams) {
};
}
+export function updateTMsTeams(teams) {
+ return {
+ type: types.SET_TM_TEAMS,
+ teams: teams,
+ };
+}
+
export function updateToken(token) {
return {
type: types.SET_TOKEN,
@@ -106,7 +114,7 @@ export const setAuthDetails = (username, token, osm_oauth_token) => (dispatch) =
// UPDATES OSM INFORMATION OF THE USER
export const setUserDetails =
(username, encodedToken, update = false) =>
- (dispatch) => {
+ async (dispatch) => {
// only trigger the loader if this function is not being triggered to update the user information
if (!update) dispatch(setLoader(true));
fetchLocalJSONAPI(`users/${username}/openstreetmap/`, encodedToken)
@@ -115,31 +123,44 @@ export const setUserDetails =
console.log(error);
dispatch(setLoader(false));
});
- // GET USER DETAILS
- fetchLocalJSONAPI(`users/queries/${username}/`, encodedToken)
- .then((userDetails) => {
- dispatch(updateUserDetails(userDetails));
- // GET USER ORGS INFO
- fetchLocalJSONAPI(
- `organisations/?omitManagerList=true&manager_user_id=${userDetails.id}`,
- encodedToken,
- )
- .then((orgs) =>
- dispatch(updateOrgsInfo(orgs.organisations.map((org) => org.organisationId))),
- )
- .catch((error) => dispatch(updateOrgsInfo([])));
- fetchLocalJSONAPI(
- `teams/?omitMemberList=true&team_role=PROJECT_MANAGER&member=${userDetails.id}`,
- encodedToken,
- )
- .then((teams) => dispatch(updatePMsTeams(teams.teams.map((team) => team.teamId))))
- .catch((error) => dispatch(updatePMsTeams([])));
- dispatch(setLoader(false));
- })
- .catch((error) => {
- if (error.message === 'InvalidToken') dispatch(logout());
- dispatch(setLoader(false));
- });
+
+ try {
+ const userDetails = await fetchLocalJSONAPI(`users/queries/${username}/`, encodedToken);
+
+ dispatch(updateUserDetails(userDetails));
+
+ const userId = userDetails.id;
+
+ const orgsPromise = fetchLocalJSONAPI(
+ `organisations/?omitManagerList=true&manager_user_id=${userId}`,
+ encodedToken,
+ )
+ .then((orgs) => dispatch(updateOrgsInfo(orgs.organisations.map((o) => o.organisationId))))
+ .catch(() => dispatch(updateOrgsInfo([])));
+
+ const pmsTeamsPromise = fetchLocalJSONAPI(
+ `teams/?omitMemberList=true&team_role=PROJECT_MANAGER&member=${userId}`,
+ encodedToken,
+ )
+ .then((teams) => dispatch(updatePMsTeams(teams.teams.map((t) => t.teamId))))
+ .catch(() => dispatch(updatePMsTeams([])));
+
+ const tmsTeamsPromise = fetchLocalJSONAPI(
+ `teams/?fullMemberList=false&manager=${userId}`,
+ encodedToken,
+ )
+ .then((teams) => dispatch(updateTMsTeams(teams.teams.map((t) => t.teamId))))
+ .catch(() => dispatch(updateTMsTeams([])));
+
+ // Run all parallel requests at once
+ await Promise.all([orgsPromise, pmsTeamsPromise, tmsTeamsPromise]);
+ } catch (error) {
+ if (error.message === 'InvalidToken') {
+ dispatch(logout());
+ }
+ } finally {
+ dispatch(setLoader(false));
+ }
};
export const getUserDetails = (state) => (dispatch) => {
diff --git a/frontend/src/store/reducers/auth.js b/frontend/src/store/reducers/auth.js
index 30656f6b40..0765dc56c3 100644
--- a/frontend/src/store/reducers/auth.js
+++ b/frontend/src/store/reducers/auth.js
@@ -23,6 +23,9 @@ export function authorizationReducer(state = initialState, action) {
case types.SET_PM_TEAMS: {
return { ...state, pmTeams: action.teams };
}
+ case types.SET_TM_TEAMS: {
+ return { ...state, tmTeams: action.teams };
+ }
case types.SET_TOKEN: {
return { ...state, token: action.token };
}
diff --git a/frontend/src/views/management.js b/frontend/src/views/management.js
index 94fc4c3e84..a8a46e52e3 100644
--- a/frontend/src/views/management.js
+++ b/frontend/src/views/management.js
@@ -87,8 +87,12 @@ export const ManagementSection = (props) => {
location.pathname === '/manage',
[location.pathname],
);
- // access this page from here and restrictd on the page itslf if it has no edit access
- const isProjectEditRoute = location.pathname.startsWith('/manage/projects') && id;
+
+ // access this page from here and restricted on the page itself if it has no edit access
+ const isProjectEditRoute =
+ (location.pathname.startsWith('/manage/projects') ||
+ location.pathname.startsWith('/manage/teams')) &&
+ id;
return (
<>
diff --git a/migrations/versions/763165f937cf_.py b/migrations/versions/763165f937cf_.py
new file mode 100644
index 0000000000..2cb13d06a6
--- /dev/null
+++ b/migrations/versions/763165f937cf_.py
@@ -0,0 +1,54 @@
+"""
+
+Revision ID: 763165f937cf
+Revises: 4489b9e235f8
+Create Date: 2025-11-20 12:09:24.690604
+
+"""
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = "763165f937cf"
+down_revision = "4489b9e235f8"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ op.execute("CREATE SEQUENCE IF NOT EXISTS project_teams_id_seq;")
+ op.add_column(
+ "project_teams",
+ sa.Column(
+ "id",
+ sa.BigInteger(),
+ nullable=True,
+ server_default=sa.text("nextval('project_teams_id_seq'::regclass)"),
+ ),
+ )
+ op.execute(
+ "UPDATE project_teams SET id = nextval('project_teams_id_seq') WHERE id IS NULL;"
+ )
+ op.alter_column("project_teams", "id", nullable=False)
+ op.execute("ALTER SEQUENCE project_teams_id_seq OWNED BY project_teams.id;")
+ op.create_unique_constraint(
+ "uq_project_team_role", "project_teams", ["team_id", "project_id", "role"]
+ )
+ op.drop_constraint("project_teams_pkey", "project_teams", type_="primary")
+ op.create_primary_key("project_teams_pkey", "project_teams", ["id"])
+ op.create_index("ix_project_teams_project_id", "project_teams", ["project_id"])
+ op.create_index("ix_project_teams_team_id", "project_teams", ["team_id"])
+
+
+def downgrade():
+ op.drop_constraint("project_teams_pkey", "project_teams", type_="primary")
+ op.drop_index("ix_project_teams_project_id", table_name="project_teams")
+ op.drop_index("ix_project_teams_team_id", table_name="project_teams")
+ op.create_primary_key(
+ "project_teams_pkey", "project_teams", ["team_id", "project_id"]
+ )
+ op.drop_constraint("uq_project_team_role", "project_teams", type_="unique")
+ op.drop_column("project_teams", "id")
+ op.execute("DROP SEQUENCE IF EXISTS project_teams_id_seq;")