-
-
Notifications
You must be signed in to change notification settings - Fork 277
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6612 from hotosm/develop
v4.8.2 on staging
- Loading branch information
Showing
40 changed files
with
2,642 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
import io | ||
from flask import send_file | ||
from flask_restful import Resource, request | ||
from typing import Optional | ||
|
||
|
||
from backend.services.partner_service import PartnerService | ||
from backend.exceptions import BadRequest | ||
|
||
# Replaceable by another service which implements the method: | ||
# fetch_partner_stats(id_inside_service, from_date, to_date) -> PartnerStatsDTO | ||
from backend.services.mapswipe_service import MapswipeService | ||
|
||
MAPSWIPE_GROUP_EMPTY_SUBCODE = "EMPTY_MAPSWIPE_GROUP" | ||
MAPSWIPE_GROUP_EMPTY_MESSAGE = "Mapswipe group is not set for this partner." | ||
|
||
|
||
def is_valid_group_id(group_id: Optional[str]) -> bool: | ||
return group_id is not None and len(group_id) > 0 | ||
|
||
|
||
class FilteredPartnerStatisticsAPI(Resource): | ||
def get(self, permalink: str): | ||
""" | ||
Get partner statistics by id and time range | ||
--- | ||
tags: | ||
- partners | ||
produces: | ||
- application/json | ||
parameters: | ||
- in: query | ||
name: fromDate | ||
type: string | ||
description: Fetch partner statistics from date as yyyy-mm-dd | ||
example: "2024-01-01" | ||
- in: query | ||
name: toDate | ||
type: string | ||
example: "2024-09-01" | ||
description: Fetch partner statistics to date as yyyy-mm-dd | ||
- name: partner_id | ||
in: path | ||
- name: permalink | ||
in: path | ||
description: The permalink of the partner | ||
required: true | ||
type: string | ||
responses: | ||
200: | ||
description: Partner found | ||
401: | ||
description: Unauthorized - Invalid credentials | ||
404: | ||
description: Partner not found | ||
500: | ||
description: Internal Server Error | ||
""" | ||
mapswipe = MapswipeService() | ||
from_date = request.args.get("fromDate") | ||
to_date = request.args.get("toDate") | ||
|
||
if from_date is None: | ||
raise BadRequest( | ||
sub_code="INVALID_TIME_RANGE", | ||
message="fromDate is missing", | ||
from_date=from_date, | ||
to_date=to_date, | ||
) | ||
|
||
if to_date is None: | ||
raise BadRequest( | ||
sub_code="INVALID_TIME_RANGE", | ||
message="toDate is missing", | ||
from_date=from_date, | ||
to_date=to_date, | ||
) | ||
|
||
if from_date > to_date: | ||
raise BadRequest( | ||
sub_code="INVALID_TIME_RANGE", | ||
message="fromDate should be less than toDate", | ||
from_date=from_date, | ||
to_date=to_date, | ||
) | ||
|
||
partner = PartnerService.get_partner_by_permalink(permalink) | ||
|
||
if not is_valid_group_id(partner.mapswipe_group_id): | ||
raise BadRequest( | ||
sub_code=MAPSWIPE_GROUP_EMPTY_SUBCODE, | ||
message=MAPSWIPE_GROUP_EMPTY_MESSAGE, | ||
) | ||
|
||
return ( | ||
mapswipe.fetch_filtered_partner_stats( | ||
partner.id, partner.mapswipe_group_id, from_date, to_date | ||
).to_primitive(), | ||
200, | ||
) | ||
|
||
|
||
class GroupPartnerStatisticsAPI(Resource): | ||
def get(self, permalink: str): | ||
""" | ||
Get partner statistics by id and broken down by each contributor. | ||
This API is paginated with limit and offset query parameters. | ||
--- | ||
tags: | ||
- partners | ||
produces: | ||
- application/json | ||
parameters: | ||
- in: query | ||
name: limit | ||
description: The number of partner members to fetch | ||
type: integer | ||
example: 10 | ||
- in: query | ||
name: offset | ||
description: The starting index from which to fetch partner members | ||
type: integer | ||
example: 0 | ||
- in: query | ||
name: downloadAsCSV | ||
description: Download users in this group as CSV | ||
type: boolean | ||
example: false | ||
- name: permalink | ||
in: path | ||
description: The permalink of the partner | ||
required: true | ||
type: string | ||
responses: | ||
200: | ||
description: Partner found | ||
401: | ||
description: Unauthorized - Invalid credentials | ||
404: | ||
description: Partner not found | ||
500: | ||
description: Internal Server Error | ||
""" | ||
|
||
mapswipe = MapswipeService() | ||
partner = PartnerService.get_partner_by_permalink(permalink) | ||
|
||
if not is_valid_group_id(partner.mapswipe_group_id): | ||
raise BadRequest( | ||
sub_code=MAPSWIPE_GROUP_EMPTY_SUBCODE, | ||
message=MAPSWIPE_GROUP_EMPTY_MESSAGE, | ||
) | ||
|
||
limit = int(request.args.get("limit", 10)) | ||
offset = int(request.args.get("offset", 0)) | ||
download_as_csv = bool(request.args.get("downloadAsCSV", "false") == "true") | ||
|
||
group_dto = mapswipe.fetch_grouped_partner_stats( | ||
partner.id, | ||
partner.mapswipe_group_id, | ||
limit, | ||
offset, | ||
download_as_csv, | ||
) | ||
|
||
if download_as_csv: | ||
return send_file( | ||
io.BytesIO(group_dto.to_csv().encode()), | ||
mimetype="text/csv", | ||
as_attachment=True, | ||
download_name="partner_members.csv", | ||
) | ||
|
||
return group_dto.to_primitive(), 200 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
import pandas as pd | ||
from schematics import Model | ||
from schematics.types import ( | ||
StringType, | ||
LongType, | ||
IntType, | ||
ListType, | ||
ModelType, | ||
FloatType, | ||
BooleanType, | ||
) | ||
|
||
|
||
class UserGroupMemberDTO(Model): | ||
id = StringType() | ||
user_id = StringType(serialized_name="userId") | ||
username = StringType() | ||
is_active = BooleanType(serialized_name="isActive") | ||
total_mapping_projects = IntType(serialized_name="totalMappingProjects") | ||
total_contribution_time = IntType(serialized_name="totalcontributionTime") | ||
total_contributions = IntType(serialized_name="totalcontributions") | ||
|
||
|
||
class OrganizationContributionsDTO(Model): | ||
organization_name = StringType(serialized_name="organizationName") | ||
total_contributions = IntType(serialized_name="totalcontributions") | ||
|
||
|
||
class UserContributionsDTO(Model): | ||
total_mapping_projects = IntType(serialized_name="totalMappingProjects") | ||
total_contribution_time = IntType(serialized_name="totalcontributionTime") | ||
total_contributions = IntType(serialized_name="totalcontributions") | ||
username = StringType() | ||
user_id = StringType(serialized_name="userId") | ||
|
||
|
||
class GeojsonDTO(Model): | ||
type = StringType() | ||
coordinates = ListType(FloatType) | ||
|
||
|
||
class GeoContributionsDTO(Model): | ||
geojson = ModelType(GeojsonDTO) | ||
total_contributions = IntType(serialized_name="totalcontributions") | ||
|
||
|
||
class ContributionsByDateDTO(Model): | ||
task_date = StringType(serialized_name="taskDate") | ||
total_contributions = IntType(serialized_name="totalcontributions") | ||
|
||
|
||
class ContributionTimeByDateDTO(Model): | ||
date = StringType(serialized_name="date") | ||
total_contribution_time = IntType(serialized_name="totalcontributionTime") | ||
|
||
|
||
class ContributionsByProjectTypeDTO(Model): | ||
project_type = StringType(serialized_name="projectType") | ||
project_type_display = StringType(serialized_name="projectTypeDisplay") | ||
total_contributions = IntType(serialized_name="totalcontributions") | ||
|
||
|
||
class AreaSwipedByProjectTypeDTO(Model): | ||
total_area = FloatType(serialized_name="totalArea") | ||
project_type = StringType(serialized_name="projectType") | ||
project_type_display = StringType(serialized_name="projectTypeDisplay") | ||
|
||
|
||
class GroupedPartnerStatsDTO(Model): | ||
"""General statistics of a partner and its members.""" | ||
|
||
id = LongType() | ||
provider = StringType() | ||
id_inside_provider = StringType(serialized_name="idInsideProvider") | ||
name_inside_provider = StringType(serialized_name="nameInsideProvider") | ||
description_inside_provider = StringType( | ||
serialized_name="descriptionInsideProvider" | ||
) | ||
members_count = IntType(serialized_name="membersCount") | ||
members = ListType(ModelType(UserGroupMemberDTO)) | ||
|
||
# General stats of partner | ||
total_contributors = IntType(serialized_name="totalContributors") | ||
total_contributions = IntType(serialized_name="totalcontributions") | ||
total_contribution_time = IntType(serialized_name="totalcontributionTime") | ||
|
||
# Recent contributions during the last 1 month | ||
total_recent_contributors = IntType(serialized_name="totalRecentContributors") | ||
total_recent_contributions = IntType(serialized_name="totalRecentcontributions") | ||
total_recent_contribution_time = IntType( | ||
serialized_name="totalRecentcontributionTime" | ||
) | ||
|
||
def to_csv(self): | ||
df = pd.json_normalize(self.to_primitive()["members"]) | ||
|
||
df.drop( | ||
columns=["id"], | ||
inplace=True, | ||
axis=1, | ||
) | ||
df.rename( | ||
columns={ | ||
"totalcontributionTime": "totalSwipeTimeInSeconds", | ||
"totalcontributions": "totalSwipes", | ||
}, | ||
inplace=True, | ||
) | ||
|
||
return df.to_csv(index=False) | ||
|
||
|
||
class FilteredPartnerStatsDTO(Model): | ||
"""Statistics of a partner contributions filtered by time range.""" | ||
|
||
id = LongType() | ||
provider = StringType() | ||
id_inside_provider = StringType(serialized_name="idInsideProvider") | ||
|
||
from_date = StringType(serialized_name="fromDate") | ||
to_date = StringType(serialized_name="toDate") | ||
contributions_by_user = ListType( | ||
ModelType(UserContributionsDTO), serialized_name="contributionsByUser" | ||
) | ||
contributions_by_geo = ListType( | ||
ModelType(GeoContributionsDTO), serialized_name="contributionsByGeo" | ||
) | ||
area_swiped_by_project_type = ListType( | ||
ModelType(AreaSwipedByProjectTypeDTO), serialized_name="areaSwipedByProjectType" | ||
) | ||
|
||
contributions_by_project_type = ListType( | ||
ModelType(ContributionsByProjectTypeDTO), | ||
serialized_name="contributionsByProjectType", | ||
) | ||
contributions_by_date = ListType( | ||
ModelType(ContributionsByDateDTO), serialized_name="contributionsByDate" | ||
) | ||
contributions_by_organization_name = ListType( | ||
ModelType(OrganizationContributionsDTO), | ||
serialized_name="contributionsByorganizationName", | ||
) | ||
contribution_time_by_date = ListType( | ||
ModelType(ContributionTimeByDateDTO), serialized_name="contributionTimeByDate" | ||
) |
Oops, something went wrong.