Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate Partner statistics from mapswipe #6568

Open
wants to merge 100 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
b98b8d5
Add swipe icon
VinayakRugvedi Sep 2, 2024
0edfd88
Add StatsCardWithDelta component
VinayakRugvedi Sep 2, 2024
0364a0e
feat: create view for partners mapswipe stats and show info banner
VinayakRugvedi Sep 3, 2024
cdbb046
Create a dir for partner mapswipe stats view, add Overview component …
VinayakRugvedi Sep 3, 2024
dd13fa3
feat: show overview stats with mock data
VinayakRugvedi Sep 3, 2024
cff6745
feat: create group members table with mock data
VinayakRugvedi Sep 4, 2024
fb9ab7b
Update messages with group members columns, empty and error state
VinayakRugvedi Sep 4, 2024
bfecd8f
feat: show group members table section with mock data
VinayakRugvedi Sep 4, 2024
3159e1f
feat: create contributions grid component
VinayakRugvedi Sep 5, 2024
0d792a7
feat: show contributions grid section
VinayakRugvedi Sep 5, 2024
b587494
feat: create timeSpentContributingByDay bar chart component
VinayakRugvedi Sep 7, 2024
a6d0b86
feat: show time spent contributing by day section
VinayakRugvedi Sep 7, 2024
2a7b268
feat: create timeSpentContributing area chart component
VinayakRugvedi Sep 10, 2024
7782747
feat: show time spent contributing section with mock data
VinayakRugvedi Sep 10, 2024
84311c5
feat: create swipes by project type doughnut chart component with moc…
VinayakRugvedi Sep 10, 2024
469e3a2
feat: create swipes by organisation doughnut chart component with moc…
VinayakRugvedi Sep 11, 2024
90b500e
feat: show swipes by project type and by organisation section
VinayakRugvedi Sep 11, 2024
1a4d748
feat: create projectTypeAreaStats component with mock data
VinayakRugvedi Sep 11, 2024
2ca8b51
feat: show swipes, time spent, and project type area stats section + …
VinayakRugvedi Sep 11, 2024
0098693
Add gray color to CHART_COLOURS, use CHART_COLOURS in chart components
VinayakRugvedi Sep 11, 2024
cebb5b7
Rename StatsCardWithDelta -> StatsCardWithFooter, handle delta, and a…
VinayakRugvedi Sep 12, 2024
e3c8e27
Add geo JSON mock data
VinayakRugvedi Sep 13, 2024
935206e
feat: create contributions heatmap component with mock data
VinayakRugvedi Sep 13, 2024
31ac125
feat: show ContributionsHeatmap section
VinayakRugvedi Sep 13, 2024
5e35a61
chore: install h3-js as a dependancy
VinayakRugvedi Sep 13, 2024
709c35e
Handle page loading with placeholder
VinayakRugvedi Sep 18, 2024
ec13e37
feat: add dropdown to update x axis time frame
VinayakRugvedi Sep 18, 2024
72f9c13
Handle page error UI and update messages
VinayakRugvedi Sep 18, 2024
fe7f007
Add queries for mapswipe user group stats
bshankar Sep 3, 2024
a92be28
Add DTO for partner stats
bshankar Sep 11, 2024
7e55857
[WIP] Implement a caching service for fetching partner stats
bshankar Sep 11, 2024
21ebdb4
Tweak partner stats caching: TTL 24h, max size: 16
bshankar Sep 12, 2024
6a81cd8
Add recent contributions to the partner stats DTO
bshankar Sep 12, 2024
d4b577e
Add API endpoint to fetch partner statistics
bshankar Sep 12, 2024
6ade934
Split partner statistics service into general and filtered
bshankar Sep 12, 2024
6fcb1b2
Split partner statistics API into group, filtered endpoints
bshankar Sep 12, 2024
67ed4f5
Add user memberships to PartnerStatisticsDTO
bshankar Sep 13, 2024
1b292eb
Partner statistics: Improve API docs
bshankar Sep 13, 2024
c1254ac
Fix: dicts returned by partner stats query builders
bshankar Sep 13, 2024
0c169c1
Fix: Provide content-type to mapswipe requests
bshankar Sep 13, 2024
1d1dcd1
Fix: Partner stats: request body format
bshankar Sep 13, 2024
3d21e5c
Fix: Move users to GroupPartnerStatisticsDTO
bshankar Sep 13, 2024
0951da4
Fix: DTO fields and refactor grouped partner statistics API
bshankar Sep 13, 2024
30963da
Fix: Partner statistics API limit should be int
bshankar Sep 16, 2024
4d7d2a2
Add API to download grouped statistics as CSV
bshankar Sep 17, 2024
14b3465
Rename partner stats API -> general-statistics
bshankar Sep 17, 2024
4a3297d
Implement API to fetch filtered statistics over time range
bshankar Sep 17, 2024
49c1b8f
Enable caching mapswipe requests
bshankar Sep 18, 2024
68d79c8
Fix: flake8 line too long error
bshankar Sep 18, 2024
0333dae
Fix: Handle API partner stats api call without group id
bshankar Sep 18, 2024
345e14a
Fix: Remove token auth for general, filtered partner stats
bshankar Sep 19, 2024
0dae784
Use permalink instead of id for partner statistics
bshankar Sep 19, 2024
c27ecbc
Add zoom minus svg icon
VinayakRugvedi Sep 19, 2024
05a3c02
Update colours and handle warnings
VinayakRugvedi Sep 19, 2024
92860d3
feat: add zoom handler icons, update zoom behaviour, and update styling
VinayakRugvedi Sep 19, 2024
8887ad9
Add mapswipe tab
VinayakRugvedi Sep 19, 2024
43ac918
feat: integrate general statistics API, handle loading and empty state
VinayakRugvedi Sep 19, 2024
ac1db7c
feat: add API call to filtered statistics
VinayakRugvedi Sep 19, 2024
97afe9c
feat: integrate API data for ContributionsGrid component
VinayakRugvedi Sep 19, 2024
fc57141
feat: integrate API data with heatmap component, handle hex res chang…
VinayakRugvedi Sep 20, 2024
d2f7d65
[WIP] Integrate API data into group members table
bshankar Sep 20, 2024
9ce197b
feat: Integrate API into area swiped pie chart
bshankar Sep 20, 2024
13c417a
feat: Integrate API into swipes by organization pie chart
bshankar Sep 20, 2024
9e3131b
Export formatSecondsToTwoUnits and getShortNumber functions
VinayakRugvedi Sep 20, 2024
af5489d
Add empty set mathematical icon
VinayakRugvedi Sep 20, 2024
da5635e
feat: integrate API data for time spent contributing and refactor code
VinayakRugvedi Sep 20, 2024
cede94a
feat: integrate API data with timeSpentContributingByDay bar chart
VinayakRugvedi Sep 20, 2024
d30eaaf
fix: Remove redundant query param
bshankar Sep 20, 2024
f3e8e2a
feat: Integrate API data into filtered statistics cards
bshankar Sep 20, 2024
6b5b519
fix: Pass getShortNumber when data not yet available
bshankar Sep 22, 2024
9fd130e
fix: Hide empty data in area swiped by project type doughnut
bshankar Sep 22, 2024
c6c3055
feat: swipes by org doughnut: group low swipers into others
bshankar Sep 22, 2024
3a0af08
fix: handle pagination and loading placeholder for group members table
VinayakRugvedi Sep 22, 2024
a812356
fix: Handle empty states in the response
bshankar Sep 22, 2024
ce97d06
feat: implement download members as CSV + add caching to table
VinayakRugvedi Sep 22, 2024
8eb282d
fix: partner statistics APIs: detect invalid group id
bshankar Sep 22, 2024
acd86af
fix: Backend sonarcube suggestions
bshankar Sep 22, 2024
aceed74
fix: update the way data is extracted from API
VinayakRugvedi Sep 22, 2024
a4f6852
refactor: remove comments, add functions to get data, clean imports
VinayakRugvedi Sep 22, 2024
95a0e3b
fix: update the way data is being extracted from API
VinayakRugvedi Sep 22, 2024
9586ec8
Add empty state to pie charts, prettify code
VinayakRugvedi Sep 22, 2024
ad9dc64
Remove redundant mapswipe json response files
bshankar Sep 23, 2024
503952a
refactor: clean imports, add proptypes, and address linting warnings
VinayakRugvedi Sep 23, 2024
8645e4c
Rename swipesByOrganisation to swipesByOrganization
VinayakRugvedi Sep 24, 2024
5f241b1
feat: replace manual zoom controls with native navigation controls
VinayakRugvedi Sep 24, 2024
4d6eada
Add formatting to time on table, reduce size of doughnut chart
VinayakRugvedi Sep 24, 2024
475b66a
fix: ensure the right labels and colors are used when partial data is…
VinayakRugvedi Sep 24, 2024
302d623
fix: Make CSV output user friendly
bshankar Sep 24, 2024
240afd8
refactor: use better standard conventions
VinayakRugvedi Sep 27, 2024
de1b114
fix: handle card height, handle line curve better in time spent chart
VinayakRugvedi Oct 1, 2024
85d1f28
Add people icon
VinayakRugvedi Oct 3, 2024
25755f2
Revamp styling and replace StatsCardWithFooter with custom JSX
VinayakRugvedi Oct 9, 2024
c0d46cf
Revamp styling of swipes and time spent contributing cards
VinayakRugvedi Oct 2, 2024
fdf5fba
Add ChecksGrid icon
VinayakRugvedi Oct 3, 2024
3934ecd
Add ColumnsGap icon
VinayakRugvedi Oct 3, 2024
9b1d53a
Add Fullscreen icon
VinayakRugvedi Oct 3, 2024
6d5e972
Revamp styling of projectTypeAreaStats and replace StatsCardWithFoote…
VinayakRugvedi Oct 6, 2024
a903cfe
Add support for shorter formatting
VinayakRugvedi Oct 6, 2024
f0a7061
Leverage shorter formatting of seconds to time
VinayakRugvedi Oct 9, 2024
a357484
Use shorter formatting for time spent contributing
VinayakRugvedi Oct 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading