Skip to content

Commit

Permalink
Merge pull request #6612 from hotosm/develop
Browse files Browse the repository at this point in the history
v4.8.2 on staging
  • Loading branch information
ramyaragupathy authored Nov 4, 2024
2 parents bd9689f + 1867b92 commit 92fed5d
Show file tree
Hide file tree
Showing 40 changed files with 2,642 additions and 20 deletions.
10 changes: 5 additions & 5 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ workflows:
name: Deploy backend production
gitsha: $CIRCLE_SHA1
stack_name: "tm4-production"
host_ami: "/aws/service/canonical/ubuntu/server/20.04/stable/current/amd64/hvm/ebs-gp2/ami-id"
host_ami: "/aws/service/debian/release/11/20240813-1838/amd64"
backend_instance_type: c6a.large
pg_version: "13.10"
pg_param_group: "default.postgres13"
Expand Down Expand Up @@ -357,7 +357,7 @@ workflows:
name: Deploy backend production
gitsha: $CIRCLE_SHA1
stack_name: "tm4-production"
host_ami: "/aws/service/canonical/ubuntu/server/20.04/stable/current/amd64/hvm/ebs-gp2/ami-id"
host_ami: "/aws/service/debian/release/11/20240813-1838/amd64"
backend_instance_type: c6a.large
pg_version: "13.10"
pg_param_group: "default.postgres13"
Expand All @@ -384,7 +384,7 @@ workflows:
name: Deploy TeachOSM Backend
gitsha: $CIRCLECI_SHA1
stack_name: "teachosm"
host_ami: "/aws/service/canonical/ubuntu/server/20.04/stable/current/amd64/hvm/ebs-gp2/ami-id"
host_ami: "/aws/service/debian/release/11/20240813-1838/amd64"
requires:
- backend-functional-tests
context: tasking-manager-teachosm
Expand Down Expand Up @@ -422,7 +422,7 @@ workflows:
name: Deploy staging backend
gitsha: $CIRCLE_SHA1
stack_name: "staging"
host_ami: "/aws/service/canonical/ubuntu/server/20.04/stable/current/amd64/hvm/ebs-gp2/ami-id"
host_ami: "/aws/service/debian/release/11/20240813-1838/amd64"
pg_version: "14.8"
pg_param_group: "default.postgres14"
db_instance_type: "db.t4g.small"
Expand Down Expand Up @@ -473,7 +473,7 @@ workflows:
name: Deploy development backend
gitsha: $CIRCLE_SHA1
stack_name: "dev"
host_ami: "/aws/service/canonical/ubuntu/server/20.04/stable/current/amd64/hvm/ebs-gp2/ami-id"
host_ami: "/aws/service/debian/release/11/20240813-1838/amd64"
pg_version: "14.10"
pg_param_group: "default.postgres14"
db_instance_type: "db.t4g.small"
Expand Down
16 changes: 16 additions & 0 deletions backend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,12 @@ def add_api_endpoints(app):
PartnersByProjectAPI,
)

# Partner statistics API
from backend.api.partners.statistics import (
GroupPartnerStatisticsAPI,
FilteredPartnerStatisticsAPI,
)

# Tasks API import
from backend.api.tasks.resources import (
TasksRestAPI,
Expand Down Expand Up @@ -590,6 +596,16 @@ def add_api_endpoints(app):
format_url("partners/<int:partner_id>/"),
methods=["GET", "DELETE", "PUT"],
)
api.add_resource(
GroupPartnerStatisticsAPI,
format_url("/partners/<string:permalink>/general-statistics"),
methods=["GET"],
)
api.add_resource(
FilteredPartnerStatisticsAPI,
format_url("/partners/<string:permalink>/filtered-statistics"),
methods=["GET"],
)
api.add_resource(
PartnerPermalinkRestAPI,
format_url("partners/<string:permalink>/"),
Expand Down
174 changes: 174 additions & 0 deletions backend/api/partners/statistics.py
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
145 changes: 145 additions & 0 deletions backend/models/dtos/partner_stats_dto.py
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"
)
Loading

0 comments on commit 92fed5d

Please sign in to comment.