From 4a11ef7acc8a9bc1fb2486f4b287fa8155c6b762 Mon Sep 17 00:00:00 2001 From: prabinoid <38830224+prabinoid@users.noreply.github.com> Date: Mon, 25 Nov 2024 11:04:50 +0545 Subject: [PATCH 01/11] Added date joined in team members table and its related functions --- backend/models/dtos/team_dto.py | 2 ++ backend/models/postgis/team.py | 5 +++++ backend/services/team_service.py | 6 +++++- migrations/versions/8e5144b55919_.py | 25 +++++++++++++++++++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 migrations/versions/8e5144b55919_.py diff --git a/backend/models/dtos/team_dto.py b/backend/models/dtos/team_dto.py index 58f2ee692b..2c1347d2b4 100644 --- a/backend/models/dtos/team_dto.py +++ b/backend/models/dtos/team_dto.py @@ -7,6 +7,7 @@ LongType, ListType, ModelType, + UTCDateTimeType, ) from backend.models.dtos.stats_dto import Pagination @@ -64,6 +65,7 @@ class TeamMembersDTO(Model): default=False, serialized_name="joinRequestNotifications" ) picture_url = StringType(serialized_name="pictureUrl") + joined_date = UTCDateTimeType(serialized_name="joinedDate") class TeamProjectDTO(Model): diff --git a/backend/models/postgis/team.py b/backend/models/postgis/team.py index ca9ac2a8f9..c6f5c17d8f 100644 --- a/backend/models/postgis/team.py +++ b/backend/models/postgis/team.py @@ -15,6 +15,7 @@ TeamRoles, ) from backend.models.postgis.user import User +from backend.models.postgis.utils import timestamp class TeamMembers(db.Model): @@ -36,6 +37,7 @@ class TeamMembers(db.Model): team = db.relationship( "Team", backref=db.backref("members", cascade="all, delete-orphan") ) + joined_date = db.Column(db.DateTime, default=timestamp) def create(self): """Creates and saves the current model to the DB""" @@ -105,6 +107,7 @@ def create_from_dto(cls, new_team_dto: NewTeamDTO): new_member.user_id = new_team_dto.creator new_member.function = TeamMemberFunctions.MANAGER.value new_member.active = True + new_member.joined_date = timestamp() new_team.members.append(new_member) @@ -222,6 +225,7 @@ def as_dto_team_member(self, member) -> TeamMembersDTO: member_dto.picture_url = user.picture_url member_dto.active = member.active member_dto.join_request_notifications = member.join_request_notifications + member_dto.joined_date = member.joined_date return member_dto def as_dto_team_project(self, project) -> TeamProjectDTO: @@ -242,6 +246,7 @@ def _get_team_members(self): "pictureUrl": mem.member.picture_url, "function": TeamMemberFunctions(mem.function).name, "active": mem.active, + "joinedDate": mem.joined_date, } ) diff --git a/backend/services/team_service.py b/backend/services/team_service.py index 6eeaf7b5b3..b197ff35be 100644 --- a/backend/services/team_service.py +++ b/backend/services/team_service.py @@ -1,3 +1,4 @@ +from backend.models.postgis.utils import timestamp from flask import current_app from sqlalchemy import and_, or_ from markdown import markdown @@ -121,12 +122,15 @@ def add_user_to_team( ) @staticmethod - def add_team_member(team_id, user_id, function, active=False): + def add_team_member( + team_id, user_id, function, active=False, joined_date=timestamp() + ): team_member = TeamMembers() team_member.team_id = team_id team_member.user_id = user_id team_member.function = function team_member.active = active + team_member.joined_date = joined_date team_member.create() @staticmethod diff --git a/migrations/versions/8e5144b55919_.py b/migrations/versions/8e5144b55919_.py new file mode 100644 index 0000000000..30fb926ffc --- /dev/null +++ b/migrations/versions/8e5144b55919_.py @@ -0,0 +1,25 @@ +"""Add date joined in teams table +Revision ID: 8e5144b55919 +Revises: ecb6985693c0_ +Create Date: 2024-11-22 10:25:38.551015 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '8e5144b55919' +down_revision = 'ecb6985693c0_' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column( + 'team_members', + sa.Column('joined_date', sa.DateTime(), nullable=True) + ) + +def downgrade(): + op.drop_column('team_members', 'joined_date') From 5e7b06a11227aeabf52504758d51da433f028e02 Mon Sep 17 00:00:00 2001 From: royallsilwallz Date: Mon, 25 Nov 2024 17:34:02 +0545 Subject: [PATCH 02/11] Add `joinedDate` to join requests in `Manage Team` page --- .../src/components/teamsAndOrgs/members.js | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/teamsAndOrgs/members.js b/frontend/src/components/teamsAndOrgs/members.js index 8b84e783a4..ec71a11462 100644 --- a/frontend/src/components/teamsAndOrgs/members.js +++ b/frontend/src/components/teamsAndOrgs/members.js @@ -1,7 +1,7 @@ import { useState, useEffect, useCallback, useRef } from 'react'; import { Link } from 'react-router-dom'; import { useSelector } from 'react-redux'; -import { FormattedMessage } from 'react-intl'; +import { useIntl, FormattedMessage } from 'react-intl'; import AsyncSelect from 'react-select/async'; import messages from './messages'; @@ -168,6 +168,7 @@ export function JoinRequests({ joinMethod, members, }: Object) { + const intl = useIntl(); const token = useSelector((state) => state.auth.token); const { username: loggedInUsername } = useSelector((state) => state.auth.userDetails); const showJoinRequestSwitch = @@ -237,14 +238,28 @@ export function JoinRequests({
{requests.map((user) => (
-
+
- + {user.username} + + {!user.joinedDate ? ( + - + ) : ( + intl.formatDate(user.joinedDate, { + year: 'numeric', + month: 'short', + day: '2-digit', + }) + )} +
From b2ec4b2cdbacb974deab4420cb6b1533efd1668c Mon Sep 17 00:00:00 2001 From: prabinoid <38830224+prabinoid@users.noreply.github.com> Date: Tue, 26 Nov 2024 12:20:15 +0545 Subject: [PATCH 03/11] Export join requests to csv --- backend/__init__.py | 9 +++- backend/api/teams/resources.py | 99 +++++++++++++++++++++++++++++++--- 2 files changed, 99 insertions(+), 9 deletions(-) diff --git a/backend/__init__.py b/backend/__init__.py index 4e032534ae..9953912aae 100644 --- a/backend/__init__.py +++ b/backend/__init__.py @@ -328,7 +328,11 @@ def add_api_endpoints(app): from backend.api.countries.resources import CountriesRestAPI # Teams API endpoint - from backend.api.teams.resources import TeamsRestAPI, TeamsAllAPI + from backend.api.teams.resources import ( + TeamsRestAPI, + TeamsAllAPI, + TeamsJoinRequestAPI, + ) from backend.api.teams.actions import ( TeamsActionsJoinAPI, TeamsActionsAddAPI, @@ -832,6 +836,9 @@ def add_api_endpoints(app): format_url("teams//"), methods=["GET", "DELETE", "PATCH"], ) + api.add_resource( + TeamsJoinRequestAPI, format_url("teams/join_requests/"), methods=["GET"] + ) # Teams actions endpoints api.add_resource( diff --git a/backend/api/teams/resources.py b/backend/api/teams/resources.py index 06df030d27..d1cb663f37 100644 --- a/backend/api/teams/resources.py +++ b/backend/api/teams/resources.py @@ -1,16 +1,18 @@ -from flask_restful import Resource, request, current_app +import csv +import io +from distutils.util import strtobool +from datetime import datetime +from flask_restful import Resource, current_app, request +from flask import Response from schematics.exceptions import DataError -from backend.models.dtos.team_dto import ( - NewTeamDTO, - UpdateTeamDTO, - TeamSearchDTO, -) +from backend.models.dtos.team_dto import NewTeamDTO, TeamSearchDTO, UpdateTeamDTO +from backend.models.postgis.team import Team, TeamMembers +from backend.models.postgis.user import User +from backend.services.organisation_service import OrganisationService from backend.services.team_service import TeamService, TeamServiceError from backend.services.users.authentication_service import token_auth -from backend.services.organisation_service import OrganisationService from backend.services.users.user_service import UserService -from distutils.util import strtobool class TeamsRestAPI(Resource): @@ -368,3 +370,84 @@ def post(self): return {"Error": error_msg, "SubCode": "CreateTeamNotPermitted"}, 403 except TeamServiceError as e: return str(e), 400 + + +class TeamsJoinRequestAPI(Resource): + # @tm.pm_only() + @token_auth.login_required + def get(self): + """ + Downloads join requests for a specific team as a CSV. + --- + tags: + - teams + produces: + - text/csv + parameters: + - in: query + name: team_id + description: ID of the team to filter by + required: true + type: integer + default: null + responses: + 200: + description: CSV file with inactive team members + 400: + description: Missing or invalid parameters + 401: + description: Unauthorized access + 500: + description: Internal server error + """ + # Parse the team_id from query parameters + team_id = request.args.get("team_id", type=int) + if not team_id: + return {"message": "team_id is required"}, 400 + + # Query the database + try: + team_members = ( + TeamMembers.query.join(User, TeamMembers.user_id == User.id) + .join(Team, TeamMembers.team_id == Team.id) + .filter(TeamMembers.team_id == team_id, TeamMembers.active == False) + .with_entities( + User.username.label("username"), + TeamMembers.joined_date.label("joined_date"), + Team.name.label("team_name"), + ) + .all() + ) + + if not team_members: + return { + "message": "No inactive members found for the specified team" + }, 404 + + # Generate CSV in memory + csv_output = io.StringIO() + writer = csv.writer(csv_output) + writer.writerow(["Username", "Joined Date", "Team Name"]) # CSV header + + for member in team_members: + writer.writerow( + [ + member.username, + member.joined_date.strftime("%Y-%m-%d %H:%M:%S") + if member.joined_date + else "N/A", + member.team_name, + ] + ) + + # Prepare response + csv_output.seek(0) + return Response( + csv_output.getvalue(), + mimetype="text/csv", + headers={ + "Content-Disposition": f"attachment; filename=join_requests_{team_id}_{datetime.now().strftime('%Y%m%d')}.csv" + }, + ) + except Exception as e: + return {"message": f"Error occurred: {str(e)}"}, 500 From b7de17565dfb9228d03a462f1218236995464811 Mon Sep 17 00:00:00 2001 From: royallsilwallz Date: Fri, 29 Nov 2024 13:11:44 +0545 Subject: [PATCH 04/11] Add `Download CSV` feature to `Join requests` in Teams page --- .../src/components/teamsAndOrgs/members.js | 56 ++++++++++++++++++- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/teamsAndOrgs/members.js b/frontend/src/components/teamsAndOrgs/members.js index ec71a11462..34ff3a6893 100644 --- a/frontend/src/components/teamsAndOrgs/members.js +++ b/frontend/src/components/teamsAndOrgs/members.js @@ -1,10 +1,13 @@ import { useState, useEffect, useCallback, useRef } from 'react'; -import { Link } from 'react-router-dom'; +import { Link, useParams } from 'react-router-dom'; +import axios from 'axios'; import { useSelector } from 'react-redux'; import { useIntl, FormattedMessage } from 'react-intl'; import AsyncSelect from 'react-select/async'; +import toast from 'react-hot-toast'; import messages from './messages'; +import projectsMessages from '../projects/messages'; import { UserAvatar } from '../user/avatar'; import { EditModeControl } from './editMode'; import { Button } from '../button'; @@ -12,6 +15,8 @@ import { SwitchToggle } from '../formInputs'; import { fetchLocalJSONAPI, pushToLocalJSONAPI } from '../../network/genericJSONRequest'; import { Alert } from '../alert'; import { useOnClickOutside } from '../../hooks/UseOnClickOutside'; +import { API_URL } from '../../config'; +import { DownloadIcon, LoadingIcon } from '../svgIcons'; export function Members({ addMembers, @@ -169,6 +174,7 @@ export function JoinRequests({ members, }: Object) { const intl = useIntl(); + const { id } = useParams(); const token = useSelector((state) => state.auth.token); const { username: loggedInUsername } = useSelector((state) => state.auth.userDetails); const showJoinRequestSwitch = @@ -216,12 +222,56 @@ export function JoinRequests({ }); }; + const [isCSVDownloading, setIsCSVDownloading] = useState(false); + + const handleTeamRequestsDownload = async () => { + setIsCSVDownloading(true); + try { + const url = `${API_URL}teams/join_requests/?team_id=${id}`; + const response = await axios.get(url, { + headers: { Authorization: `Token ${token}` }, + responseType: 'blob', + }); + const href = URL.createObjectURL(response.data); + const link = document.createElement('a'); + link.href = href; + link.setAttribute('download', 'join_requests.csv'); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + } catch (error) { + toast.error(); + } finally { + setIsCSVDownloading(false); + } + }; + return (
-
-

+
+

+ {!!requests.length && ( + + )}
{showJoinRequestSwitch && (
From cc16b0a1525d2657a426850efda84a73e3c9cee7 Mon Sep 17 00:00:00 2001 From: prabinoid <38830224+prabinoid@users.noreply.github.com> Date: Thu, 19 Dec 2024 11:42:34 +0545 Subject: [PATCH 05/11] Team member join date set by default --- backend/api/teams/resources.py | 4 +++- backend/services/team_service.py | 6 +----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/backend/api/teams/resources.py b/backend/api/teams/resources.py index d1cb663f37..870ca88677 100644 --- a/backend/api/teams/resources.py +++ b/backend/api/teams/resources.py @@ -427,7 +427,9 @@ def get(self): # Generate CSV in memory csv_output = io.StringIO() writer = csv.writer(csv_output) - writer.writerow(["Username", "Joined Date", "Team Name"]) # CSV header + writer.writerow( + ["Username", "Date Joined (UTC)", "Team Name"] + ) # CSV header for member in team_members: writer.writerow( diff --git a/backend/services/team_service.py b/backend/services/team_service.py index b197ff35be..6eeaf7b5b3 100644 --- a/backend/services/team_service.py +++ b/backend/services/team_service.py @@ -1,4 +1,3 @@ -from backend.models.postgis.utils import timestamp from flask import current_app from sqlalchemy import and_, or_ from markdown import markdown @@ -122,15 +121,12 @@ def add_user_to_team( ) @staticmethod - def add_team_member( - team_id, user_id, function, active=False, joined_date=timestamp() - ): + def add_team_member(team_id, user_id, function, active=False): team_member = TeamMembers() team_member.team_id = team_id team_member.user_id = user_id team_member.function = function team_member.active = active - team_member.joined_date = joined_date team_member.create() @staticmethod From 7a7871b3eb1d6819b755026d664de54b36247392 Mon Sep 17 00:00:00 2001 From: prabinoid <38830224+prabinoid@users.noreply.github.com> Date: Thu, 19 Dec 2024 12:40:40 +0545 Subject: [PATCH 06/11] Author name in project csv export --- backend/services/project_search_service.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/backend/services/project_search_service.py b/backend/services/project_search_service.py index bdddc463c9..bdb7633dbc 100644 --- a/backend/services/project_search_service.py +++ b/backend/services/project_search_service.py @@ -1,4 +1,5 @@ import pandas as pd +from backend.models.postgis.user import User from flask import current_app import math import geojson @@ -92,6 +93,8 @@ def create_search_query(user=None, as_csv: bool = False): Project.country, Organisation.name.label("organisation_name"), Organisation.logo.label("organisation_logo"), + User.name.label("author_name"), + User.username.label("author_username"), Project.created.label("creation_date"), func.coalesce( func.sum(func.ST_Area(Project.geometry, True) / 1000000) @@ -99,7 +102,14 @@ def create_search_query(user=None, as_csv: bool = False): ) .filter(Project.geometry is not None) .outerjoin(Organisation, Organisation.id == Project.organisation_id) - .group_by(Organisation.id, Project.id, ProjectInfo.name) + .outerjoin(User, User.id == Project.author_id) + .group_by( + Organisation.id, + Project.id, + ProjectInfo.name, + User.username, + User.name, + ) ) else: query = ( @@ -246,6 +256,7 @@ def search_projects_as_csv(search_dto: ProjectSearchDTO, user) -> str: row["total_contributors"] = Project.get_project_total_contributions( row["id"] ) + row["author"] = row["author_name"] or row["author_username"] if is_user_admin: partners_names = ( @@ -269,6 +280,8 @@ def search_projects_as_csv(search_dto: ProjectSearchDTO, user) -> str: "tasks_validated", "total_tasks", "centroid", + "author_name", + "author_username", ] colummns_to_rename = { From 4605a06156d081f330cd174eb40e660c4cb9cade Mon Sep 17 00:00:00 2001 From: prabinoid <38830224+prabinoid@users.noreply.github.com> Date: Mon, 13 Jan 2025 13:48:01 +0545 Subject: [PATCH 07/11] black and flake8 formatting --- backend/api/teams/resources.py | 9 ++++++--- migrations/versions/8e5144b55919_.py | 10 +++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/backend/api/teams/resources.py b/backend/api/teams/resources.py index 870ca88677..85914d8bf6 100644 --- a/backend/api/teams/resources.py +++ b/backend/api/teams/resources.py @@ -410,7 +410,7 @@ def get(self): team_members = ( TeamMembers.query.join(User, TeamMembers.user_id == User.id) .join(Team, TeamMembers.team_id == Team.id) - .filter(TeamMembers.team_id == team_id, TeamMembers.active == False) + .filter(TeamMembers.team_id == team_id, ~TeamMembers.active) .with_entities( User.username.label("username"), TeamMembers.joined_date.label("joined_date"), @@ -422,7 +422,7 @@ def get(self): if not team_members: return { "message": "No inactive members found for the specified team" - }, 404 + }, 200 # Generate CSV in memory csv_output = io.StringIO() @@ -448,7 +448,10 @@ def get(self): csv_output.getvalue(), mimetype="text/csv", headers={ - "Content-Disposition": f"attachment; filename=join_requests_{team_id}_{datetime.now().strftime('%Y%m%d')}.csv" + "Content-Disposition": ( + "attachment; filename=join_requests_" + f"{team_id}_{datetime.now().strftime('%Y%m%d')}.csv" + ) }, ) except Exception as e: diff --git a/migrations/versions/8e5144b55919_.py b/migrations/versions/8e5144b55919_.py index 30fb926ffc..301c872407 100644 --- a/migrations/versions/8e5144b55919_.py +++ b/migrations/versions/8e5144b55919_.py @@ -9,17 +9,17 @@ # revision identifiers, used by Alembic. -revision = '8e5144b55919' -down_revision = 'ecb6985693c0_' +revision = "8e5144b55919" +down_revision = "ecb6985693c0_" branch_labels = None depends_on = None def upgrade(): op.add_column( - 'team_members', - sa.Column('joined_date', sa.DateTime(), nullable=True) + "team_members", sa.Column("joined_date", sa.DateTime(), nullable=True) ) + def downgrade(): - op.drop_column('team_members', 'joined_date') + op.drop_column("team_members", "joined_date") From 29c2b95c4730dec75b68a95d993d8a056cfa72aa Mon Sep 17 00:00:00 2001 From: Ramya Ragupathy Date: Wed, 15 Jan 2025 15:51:54 +0530 Subject: [PATCH 08/11] 2025 roadmap update --- README.md | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 6ff2b82e80..6bce05bf0b 100644 --- a/README.md +++ b/README.md @@ -19,23 +19,36 @@ This is Free and Open Source Software. You are welcome to use the code and set u ## Product Roadmap +[Discussion](https://github.com/hotosm/tasking-manager/discussions/6688) | [Roadmap Kanban](https://github.com/orgs/hotosm/projects/41/views/1) -Status | Feature --------|--------- -✅ | Up-to-date OSM Statistics: Integrated with [ohsome Now](https://stats.now.ohsome.org/) for real-time data insights. Released in [v4.6.2](https://github.com/hotosm/tasking-manager/releases/tag/v4.6.2). -✅ | Downloadable OSM Exports: Export data directly from each project. Available in[ v4.7.0](https://github.com/hotosm/tasking-manager/releases/tag/v4.7.0). -✅ | Live Data Quality Monitoring: Monitor project data quality in real-time. Introduced in [v4.7.2](https://github.com/hotosm/tasking-manager/releases/tag/v4.7.2). +✅ Completed: Finished, available on [production instance](https://tasks.hotosm.org) + +🔄 In Progress: Task or milestone is actively being worked on + +📅 Planned: Task or milestone is scheduled for a future date + + + +Status | Feature | Release +-------|---------|--------- +✅ | Up-to-date OSM Statistics: Integrated with [ohsome Now](https://stats.now.ohsome.org/) for real-time data insights.| Released in [v4.6.2](https://github.com/hotosm/tasking-manager/releases/tag/v4.6.2). +✅ | Downloadable OSM Exports: Export data directly from each project. | Available in[ v4.7.0](https://github.com/hotosm/tasking-manager/releases/tag/v4.7.0). +✅ | Live Data Quality Monitoring: Monitor project data quality in real-time. | Introduced in [v4.7.2](https://github.com/hotosm/tasking-manager/releases/tag/v4.7.2). ✅ | Rapid Editor Upgrade: Enhanced mapping experience with the latest rapid editor updates. ✅ | Public-Facing Partner Pages: Create and display dedicated pages for partners running remote mapathons. -⚙️| Downloadable Project List View: Allow users to explore projects via a downloadable list. [View issue](https://github.com/hotosm/tasking-manager/issues/3394). -⚙️| MapSwipe Stats Integration: Display MapSwipe statistics on Partner Pages. -⚙️| iD Editor Latest Features: Integrate the newest features of the iD editor. -⚙️| FastAPI Migration: Improve performance and scalability of Tasking Manager to handle large scale validation and mapping efforts. -| | Latest Translations Update: Keep all content current with the latest translations. -| | OSM Practice Projects: Enable users to engage in OSM practice projects within Tasking Manager workflow. -| | Improved Project Sorting & Filtering: Enhance the user experience with better sorting and filtering options. -| | UI/UX Enhancements: Continuous improvements to the user interface and experience. +✅ | Downloadable Project List View: Allow users to explore projects via a downloadable list. [View issue](https://github.com/hotosm/tasking-manager/issues/3394). +✅ | MapSwipe Stats Integration: Display MapSwipe statistics on Partner Pages. +✅ | iD Editor Latest Features: Integrate the newest features of the iD editor. +🔄 | FastAPI Migration: Improve performance and scalability of Tasking Manager to handle large scale validation and mapping efforts. +🔄 | Super Mapper: Redefine Mapper Level Milestones +🔄 | OSM Practice Projects: Enable users to engage in OSM practice projects within Tasking Manager workflow. +📅 | Expanding Project Types beyond basemap features +📅 | AI Integration: task assignment, difficulty estimation, and validation +📅 | External tools Integration: MapSwipe, uMap, Maproulette +📅 | Latest Translations Update: Keep all content current with the latest translations. +📅 | Improved Project Sorting & Filtering: Enhance the user experience with better sorting and filtering options. +📅 | UI/UX Enhancements: Continuous improvements to the user interface and experience. From 50d315784012252e97541282e1537924280e4c24 Mon Sep 17 00:00:00 2001 From: royallsilwallz Date: Tue, 17 Dec 2024 11:08:12 +0545 Subject: [PATCH 09/11] Add `HOT Global Validators Team` by default for specific validation permissions - Validation Permissions includes `Only team members` & `Only intermediate and advanced team members` - Change teams fetching to react-query - Related to #6628 --- .../projectEdit/permissionsBlock.js | 59 ++++++++++++++++--- .../src/components/projectEdit/teamSelect.js | 3 +- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/projectEdit/permissionsBlock.js b/frontend/src/components/projectEdit/permissionsBlock.js index dc9aa7fcd3..c058cb8ab4 100644 --- a/frontend/src/components/projectEdit/permissionsBlock.js +++ b/frontend/src/components/projectEdit/permissionsBlock.js @@ -1,10 +1,60 @@ -import { useContext } from 'react'; +import { useContext, useEffect, useRef } from 'react'; import { FormattedMessage } from 'react-intl'; + import messages from './messages.js'; import { StateContext, styleClasses } from '../../views/projectEdit'; +import { useTeamsQuery } from '../../api/teams'; + +const globalValidatorPermissions = ['TEAMS', 'TEAMS_LEVEL']; +const hotGlobalValidatorTeamName = 'HOT Global Validators'; export const PermissionsBlock = ({ permissions, type }: Object) => { const { projectInfo, setProjectInfo } = useContext(StateContext); + const { data: teamsData } = useTeamsQuery({ omitMemberList: true }); + const isGlobalValidatorAlreadyPresent = useRef(false); + + // check if global validator already present on teams + useEffect(() => { + isGlobalValidatorAlreadyPresent.current = projectInfo.teams.some( + (team) => team.name === hotGlobalValidatorTeamName, + ); + }, []); // eslint-disable-line -- run only on first render + + const handlePermissionChange = (value) => { + let teams = projectInfo.teams; + // validation permission case + if (type === 'validationPermission') { + const isGlobalValidatorCase = globalValidatorPermissions.includes(value); + // add `HOT Global Validators` by default case + if (isGlobalValidatorCase) { + const globalValidatorTeam = teamsData?.teams?.find( + (team) => team.name === hotGlobalValidatorTeamName, + ); + if ( + globalValidatorTeam && + // check if hotGlobalValidator already present + !projectInfo.teams.some((team) => team.name === hotGlobalValidatorTeamName) + ) { + const hotGlobalValidatorTeam = { + teamId: globalValidatorTeam.teamId, + name: globalValidatorTeam.name, + role: 'VALIDATOR', + }; + // add hotGlobalValidator to teams + teams = [hotGlobalValidatorTeam, ...projectInfo.teams]; + } + // remove hotGlobalValidator from team if not HOT Global Validator case + } else if (!isGlobalValidatorAlreadyPresent.current) { + teams = projectInfo.teams.filter((team) => team.name !== hotGlobalValidatorTeamName); + } + } + // set project info + setProjectInfo({ + ...projectInfo, + [type]: value, + teams, + }); + }; return (
@@ -27,12 +77,7 @@ export const PermissionsBlock = ({ permissions, type }: Object) => { - setProjectInfo({ - ...projectInfo, - [type]: permission.value, - }) - } + onChange={() => handlePermissionChange(permission.value)} type="radio" className={`radio-input input-reset pointer v-mid dib h2 w2 mr2 br-100 ba b--blue-light`} /> diff --git a/frontend/src/components/projectEdit/teamSelect.js b/frontend/src/components/projectEdit/teamSelect.js index 3a97b3fafc..c1ac578e0b 100644 --- a/frontend/src/components/projectEdit/teamSelect.js +++ b/frontend/src/components/projectEdit/teamSelect.js @@ -8,6 +8,7 @@ import { Button } from '../button'; import { StateContext } from '../../views/projectEdit'; import { PencilIcon, WasteIcon, ExternalLinkIcon } from '../svgIcons'; import { useFetchWithAbort } from '../../hooks/UseFetch'; +import { useTeamsQuery } from '../../api/teams'; export const TeamSelect = () => { const intl = useIntl(); @@ -24,7 +25,7 @@ export const TeamSelect = () => { const [, isOrganisationsLoading, organisationsData] = useFetchWithAbort( 'organisations/?omitManagerList=true', ); - const [, isTeamsLoading, teamsData] = useFetchWithAbort('teams/?omitMemberList=true'); + const { data: teamsData, isFetching: isTeamsLoading } = useTeamsQuery({ omitMemberList: true }); const teamRoles = [ { value: 'MAPPER', label: 'Mapper' }, From a3c189ba445a3c4b31042b4ef8d2ad65b62af29f Mon Sep 17 00:00:00 2001 From: royallsilwallz Date: Tue, 21 Jan 2025 11:09:04 +0545 Subject: [PATCH 10/11] Fix ohsomeNow url link on barchart click in Partner Stats page - Break ohsomeNow env vars into `OHSOME_STATS_BASE_URL` & `OHSOME_STATS_API_URL` - Update `example.env` --- example.env | 3 ++- frontend/src/api/stats.js | 10 +++++----- frontend/src/components/partners/partnersActivity.js | 4 ++-- frontend/src/components/teamsAndOrgs/featureStats.js | 4 ++-- frontend/src/config/index.js | 4 +++- frontend/src/config/tests/config.test.js | 4 ++-- frontend/src/network/tests/server-handlers.js | 10 +++++----- frontend/src/views/partnersStats.js | 4 ++-- 8 files changed, 23 insertions(+), 20 deletions(-) diff --git a/example.env b/example.env index 9e8ace083a..90d4ebe4a4 100644 --- a/example.env +++ b/example.env @@ -87,7 +87,8 @@ OSM_REGISTER_URL=https://www.openstreetmap.org/user/new # API base URL and token(used to retrieve user stats only) for ohsomeNow Stats # -OHSOME_STATS_BASE_URL=https://stats.now.ohsome.org/api +OHSOME_STATS_BASE_URL=https://stats.now.ohsome.org +OHSOME_STATS_API_URL=https://stats.now.ohsome.org/api OHSOME_STATS_TOKEN=testSuperSecretTestToken # Secret (required) diff --git a/frontend/src/api/stats.js b/frontend/src/api/stats.js index 1016afac77..fafbc7022d 100644 --- a/frontend/src/api/stats.js +++ b/frontend/src/api/stats.js @@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query'; import { fetchExternalJSONAPI } from '../network/genericJSONRequest'; import api from './apiClient'; -import { OHSOME_STATS_BASE_URL, defaultChangesetComment } from '../config'; +import { OHSOME_STATS_API_URL, defaultChangesetComment } from '../config'; const ohsomeProxyAPI = (url) => { const token = localStorage.getItem('token'); @@ -39,7 +39,7 @@ export const useProjectStatisticsQuery = (projectId) => { export const useOsmStatsQuery = () => { const fetchOsmStats = ({ signal }) => { - return api().get(`${OHSOME_STATS_BASE_URL}/stats/${defaultChangesetComment}-%2A`, { + return api().get(`${OHSOME_STATS_API_URL}/stats/${defaultChangesetComment}-%2A`, { signal, }); }; @@ -54,7 +54,7 @@ export const useOsmStatsQuery = () => { export const useOsmHashtagStatsQuery = (defaultComment) => { const fetchOsmStats = ({ signal }) => { - return api().get(`${OHSOME_STATS_BASE_URL}/stats/${defaultComment[0].replace('#', '')}`, { + return api().get(`${OHSOME_STATS_API_URL}/stats/${defaultComment[0].replace('#', '')}`, { signal, }); }; @@ -71,7 +71,7 @@ export const useOsmHashtagStatsQuery = (defaultComment) => { export const useUserOsmStatsQuery = (id) => { const fetchUserOsmStats = () => { return ohsomeProxyAPI( - `${OHSOME_STATS_BASE_URL}/topic/poi,highway,building,waterway/user?userId=${id}`, + `${OHSOME_STATS_API_URL}/topic/poi,highway,building,waterway/user?userId=${id}`, ); }; @@ -87,7 +87,7 @@ export const useUserOsmStatsQuery = (id) => { export const useOsmStatsMetadataQuery = () => { const fetchOsmStatsMetadata = () => { - return fetchExternalJSONAPI(`${OHSOME_STATS_BASE_URL}/metadata`); + return fetchExternalJSONAPI(`${OHSOME_STATS_API_URL}/metadata`); }; return useQuery({ diff --git a/frontend/src/components/partners/partnersActivity.js b/frontend/src/components/partners/partnersActivity.js index bb29c32ffa..4399396c56 100644 --- a/frontend/src/components/partners/partnersActivity.js +++ b/frontend/src/components/partners/partnersActivity.js @@ -4,7 +4,7 @@ import ReactPlaceholder from 'react-placeholder'; import PartnersProgresBar from './partnersProgresBar'; import messages from './messages'; -import { OHSOME_STATS_BASE_URL } from '../../config'; +import { OHSOME_STATS_API_URL } from '../../config'; export const Activity = ({ partner }) => { const [data, setData] = useState(null); @@ -22,7 +22,7 @@ export const Activity = ({ partner }) => { ?.map((tag) => tag.trim().replace('#', '').toLowerCase()) ?.join(','); const response = await fetch( - OHSOME_STATS_BASE_URL + `/stats/hashtags/${primaryHashtag},${secondaryHashtags}`, + OHSOME_STATS_API_URL + `/stats/hashtags/${primaryHashtag},${secondaryHashtags}`, ); if (response.ok) { diff --git a/frontend/src/components/teamsAndOrgs/featureStats.js b/frontend/src/components/teamsAndOrgs/featureStats.js index 5522e0f66a..a0467c26e9 100644 --- a/frontend/src/components/teamsAndOrgs/featureStats.js +++ b/frontend/src/components/teamsAndOrgs/featureStats.js @@ -4,7 +4,7 @@ import { FormattedMessage } from 'react-intl'; import messages from './messages'; import userDetailMessages from '../userDetail/messages'; -import { OHSOME_STATS_BASE_URL, defaultChangesetComment } from '../../config'; +import { OHSOME_STATS_API_URL, defaultChangesetComment } from '../../config'; import { RoadIcon, HomeIcon, WavesIcon, MarkerIcon } from '../svgIcons'; import { StatsCard } from '../statsCard'; import StatsInfoFooter from '../statsInfoFooter'; @@ -14,7 +14,7 @@ export const FeatureStats = () => { const getStats = async () => { try { const response = await axios.get( - `${OHSOME_STATS_BASE_URL}/stats/${defaultChangesetComment}-%2A`, + `${OHSOME_STATS_API_URL}/stats/${defaultChangesetComment}-%2A`, ); const { edits, buildings, roads } = response.data.result; setStats({ diff --git a/frontend/src/config/index.js b/frontend/src/config/index.js index bbcebb2f59..861baef968 100644 --- a/frontend/src/config/index.js +++ b/frontend/src/config/index.js @@ -4,7 +4,9 @@ export const API_URL = process.env.REACT_APP_API_URL ? new URL('/api/' + API_VERSION + '/', process.env.REACT_APP_API_URL) : 'http://127.0.0.1:5000/api/' + API_VERSION + '/'; export const OHSOME_STATS_BASE_URL = - process.env.REACT_APP_OHSOME_STATS_BASE_URL || 'https://stats.now.ohsome.org/api'; + process.env.REACT_APP_OHSOME_STATS_BASE_URL || 'https://stats.now.ohsome.org'; +export const OHSOME_STATS_API_URL = + process.env.REACT_APP_OHSOME_STATS_API_URL || 'https://stats.now.ohsome.org/api'; // APPLICATION SETTINGS export const DEFAULT_LOCALE = process.env.REACT_APP_DEFAULT_LOCALE || 'en'; export const ENVIRONMENT = process.env.REACT_APP_ENVIRONMENT || ''; diff --git a/frontend/src/config/tests/config.test.js b/frontend/src/config/tests/config.test.js index 67e100878f..1d3330ec11 100644 --- a/frontend/src/config/tests/config.test.js +++ b/frontend/src/config/tests/config.test.js @@ -7,8 +7,8 @@ it('exports API_URL', () => { it('exports API_VERSION', () => { expect(['object', 'string']).toContain(typeof config.API_VERSION); }); -it('exports OHSOME_STATS_BASE_URL', () => { - expect(typeof config.OHSOME_STATS_BASE_URL).toBe('string'); +it('exports OHSOME_STATS_API_URL', () => { + expect(typeof config.OHSOME_STATS_API_URL).toBe('string'); }); it('exports ORG_URL', () => { expect(typeof config.ORG_URL).toBe('string'); diff --git a/frontend/src/network/tests/server-handlers.js b/frontend/src/network/tests/server-handlers.js index a17e768062..7abc498457 100644 --- a/frontend/src/network/tests/server-handlers.js +++ b/frontend/src/network/tests/server-handlers.js @@ -69,7 +69,7 @@ import { ohsomeNowMetadata, } from './mockData/miscellaneous'; import tasksGeojson from '../../utils/tests/snippets/tasksGeometry'; -import { API_URL, OHSOME_STATS_BASE_URL, defaultChangesetComment } from '../../config'; +import { API_URL, OHSOME_STATS_API_URL, defaultChangesetComment } from '../../config'; import { notifications, ownCountUnread } from './mockData/notifications'; import { authLogin, setUser, userRegister } from './mockData/auth'; import { @@ -349,16 +349,16 @@ const handlers = [ return res(ctx.json(systemStats)); }), // EXTERNAL API - rest.get(`${OHSOME_STATS_BASE_URL}/stats/${defaultChangesetComment}-%2A`, (req, res, ctx) => { + rest.get(`${OHSOME_STATS_API_URL}/stats/${defaultChangesetComment}-%2A`, (req, res, ctx) => { return res(ctx.json(homepageStats)); }), - rest.get(`${OHSOME_STATS_BASE_URL}/hot-tm-user`, (req, res, ctx) => { + rest.get(`${OHSOME_STATS_API_URL}/hot-tm-user`, (req, res, ctx) => { return res(ctx.json(ohsomeNowUserStats)); }), - rest.get(`${OHSOME_STATS_BASE_URL}/stats/:projectId`, (req, res, ctx) => { + rest.get(`${OHSOME_STATS_API_URL}/stats/:projectId`, (req, res, ctx) => { return res(ctx.json(osmStatsProject)); }), - rest.get(`${OHSOME_STATS_BASE_URL}/metadata`, (req, res, ctx) => { + rest.get(`${OHSOME_STATS_API_URL}/metadata`, (req, res, ctx) => { return res(ctx.json(ohsomeNowMetadata)); }), rest.get('http://127.0.0.1:8111/version', (req, res, ctx) => { diff --git a/frontend/src/views/partnersStats.js b/frontend/src/views/partnersStats.js index 66a9d4c560..2da331044f 100644 --- a/frontend/src/views/partnersStats.js +++ b/frontend/src/views/partnersStats.js @@ -9,7 +9,7 @@ import { useFetch } from '../hooks/UseFetch'; import { Leaderboard } from '../components/partners/leaderboard'; import { PartnersMapswipeStats } from './partnersMapswipeStats'; import { Resources } from '../components/partners/partnersResources'; -import { OHSOME_STATS_BASE_URL } from '../config'; +import { OHSOME_STATS_API_URL } from '../config'; import { Button } from '../components/button'; import { TwitterIcon, FacebookIcon, InstagramIcon } from '../components/svgIcons'; @@ -45,7 +45,7 @@ export const PartnersStats = () => { hashtag = hashtag.slice(1); } hashtag = hashtag.toLowerCase(); - const response = await fetch(OHSOME_STATS_BASE_URL + '/stats/hashtags/' + hashtag); + const response = await fetch(OHSOME_STATS_API_URL + '/stats/hashtags/' + hashtag); if (response.ok) { const jsonData = await response.json(); if (jsonData.result !== undefined && Object.keys(jsonData.result).length !== 0) From cb84987b572dc4bb9a03786f2b50d681d13b36ec Mon Sep 17 00:00:00 2001 From: royallsilwallz Date: Wed, 22 Jan 2025 13:13:35 +0545 Subject: [PATCH 11/11] Fix ohsomeNow url link query param for `hashtag` --- frontend/src/components/partners/partnersProgresBar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/partners/partnersProgresBar.js b/frontend/src/components/partners/partnersProgresBar.js index 6931d71fd2..c278cb9ddc 100644 --- a/frontend/src/components/partners/partnersProgresBar.js +++ b/frontend/src/components/partners/partnersProgresBar.js @@ -19,7 +19,7 @@ const ProgressBar = ({ className, firstBarValue, secondBarValue = 0, children, d target={'_blank'} rel="noreferrer" className="white no-underline" - href={OHSOME_STATS_BASE_URL + '/dashboard#hashtags=' + data.primary} + href={OHSOME_STATS_BASE_URL + '/dashboard#hashtag=' + data.primary} > {'#' + data.primary}{' '}