Skip to content

Commit

Permalink
Merge pull request #6591 from hotosm/fastapi-refactor
Browse files Browse the repository at this point in the history
fix: Project action filter and user stats
  • Loading branch information
prabinoid authored Oct 8, 2024
2 parents 1cd2c43 + 7f16ef3 commit 4fda6e3
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 108 deletions.
9 changes: 6 additions & 3 deletions backend/api/users/statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,8 @@ async def get(
description: Internal Server Error
"""
try:
if start_date:
start_date = validate_date_input(start_date)
if request.query_params.get("startDate"):
start_date = validate_date_input(request.query_params.get("startDate"))
else:
return JSONResponse(
content={
Expand All @@ -151,7 +151,10 @@ async def get(
},
status_code=400,
)
end_date = validate_date_input(end_date)
if request.query_params.get("endDate"):
end_date = validate_date_input(request.query_params.get("endDate"))
else:
end_date: str = date.today()
if end_date < start_date:
raise ValueError(
"InvalidDateRange- Start date must be earlier than end date"
Expand Down
9 changes: 9 additions & 0 deletions backend/models/dtos/stats_dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,9 @@ def __init__(self):
organisations: Optional[List[OrganizationListStatsDTO]] = None
campaigns: Optional[List[CampaignStatsDTO]] = None

class Config:
populate_by_name = True


class TaskStats(BaseModel):
"""DTO for tasks stats for a single day"""
Expand All @@ -192,6 +195,9 @@ class TaskStats(BaseModel):
validated: int = Field(alias="validated")
bad_imagery: int = Field(alias="badImagery")

class Config:
populate_by_name = True


class GenderStatsDTO(BaseModel):
"""DTO for genre stats of users."""
Expand All @@ -218,3 +224,6 @@ class TaskStatsDTO(BaseModel):
"""Contains all tasks stats broken down by day"""

stats: List[TaskStats] = Field([], alias="taskStats")

class Config:
populate_by_name = True
214 changes: 109 additions & 105 deletions backend/services/project_search_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import math
import geojson
from geoalchemy2 import shape
from sqlalchemy import or_, and_
from shapely.geometry import Polygon, box
from cachetools import TTLCache, cached
from loguru import logger
Expand Down Expand Up @@ -136,8 +135,11 @@ async def create_search_query(db, user=None):
params["private"] = False
params["project_ids"] = list(project_ids)

# if filters:
# query += " AND " + " AND ".join(filters)

if filters:
query += " AND " + " AND ".join(filters)
query += " AND (" + " AND ".join(filters) + ")"

return query, params

Expand Down Expand Up @@ -181,29 +183,6 @@ async def create_result_dto(
list_dto.campaigns = await Project.get_project_campaigns(project.id, db)
return list_dto

# @staticmethod
# def get_total_contributions(paginated_results):
# paginated_projects_ids = [p.id for p in paginated_results]

# # We need to make a join to return projects without contributors.
# project_contributors_count = (
# session.query(Project).with_entities(
# Project.id, func.count(distinct(TaskHistory.user_id)).label("total")
# )
# .filter(Project.id.in_(paginated_projects_ids))
# .outerjoin(
# TaskHistory,
# and_(
# TaskHistory.project_id == Project.id,
# TaskHistory.action != "COMMENT",
# ),
# )
# .group_by(Project.id)
# .all()
# )

# return [p.total for p in project_contributors_count]

@staticmethod
async def get_total_contributions(
project_ids: List[int], db: Database
Expand Down Expand Up @@ -280,7 +259,6 @@ async def _filter_projects(search_dto: ProjectSearchDTO, user, db: Database):
base_query, params = await ProjectSearchService.create_search_query(db, user)
# Initialize filter list and parameters dictionary
filters = []

# Filters based on search_dto
if search_dto.preferred_locale:
filters.append("pi.locale IN (:preferred_locale, 'en')")
Expand Down Expand Up @@ -332,27 +310,32 @@ async def _filter_projects(search_dto: ProjectSearchDTO, user, db: Database):

if search_dto.action and search_dto.action != "any":
if search_dto.action == "map":
filters.append(
"p.id IN (SELECT project_id FROM project_actions WHERE action = 'map')"
mapping_project_ids = await ProjectSearchService.filter_projects_to_map(
user, db
)
filters.append("p.id = ANY(:mapping_project_ids)")
params["mapping_project_ids"] = tuple(mapping_project_ids)

elif search_dto.action == "validate":
filters.append(
"p.id IN (SELECT project_id FROM project_actions WHERE action = 'validate')"
validation_project_ids = (
await ProjectSearchService.filter_projects_to_validate(user, db)
)
filters.append("p.id = ANY(:validation_project_ids)")
params["validation_project_ids"] = tuple(validation_project_ids)

if search_dto.organisation_name:
filters.append("o.name = :organisation_name")
params["organisation_name"] = search_dto.organisation_name

if search_dto.organisation_id:
filters.append("o.id = :organisation_id")
params["organisation_id"] = search_dto.organisation_id
params["organisation_id"] = int(search_dto.organisation_id)

if search_dto.team_id:
filters.append(
"p.id IN (SELECT project_id FROM project_teams WHERE team_id = :team_id)"
)
params["team_id"] = search_dto.team_id
params["team_id"] = int(search_dto.team_id)

if search_dto.campaign:
filters.append(
Expand Down Expand Up @@ -466,8 +449,6 @@ async def _filter_projects(search_dto: ProjectSearchDTO, user, db: Database):

# Append the ORDER BY clause
sql_query += order_by_clause

# Pagination
page = search_dto.page
per_page = 14
offset = (page - 1) * per_page
Expand All @@ -485,84 +466,107 @@ async def _filter_projects(search_dto: ProjectSearchDTO, user, db: Database):
return all_results, paginated_results, pagination_dto

@staticmethod
def filter_by_user_permission(query, user, permission: str):
"""Filter projects a user can map or validate, based on their permissions."""
if user and user.role != UserRole.ADMIN.value:
if permission == "validation_permission":
permission_class = ValidationPermission
team_roles = [
TeamRoles.VALIDATOR.value,
TeamRoles.PROJECT_MANAGER.value,
]
else:
permission_class = MappingPermission
team_roles = [
TeamRoles.MAPPER.value,
TeamRoles.VALIDATOR.value,
TeamRoles.PROJECT_MANAGER.value,
]

selection = []
# get ids of projects assigned to the user's teams
[
[
selection.append(team_project.project_id)
for team_project in user_team.team.projects
if team_project.project_id not in selection
and team_project.role in team_roles
]
for user_team in user.teams
async def filter_by_user_permission(db: Database, user, permission: str):
"""Add permission filter to the project query based on user permissions."""

# Set the permission class and team roles based on the type of permission
if permission == "validation_permission":
permission_class = ValidationPermission
team_roles = [
TeamRoles.VALIDATOR.value,
TeamRoles.PROJECT_MANAGER.value,
]
if user.mapping_level == MappingLevel.BEGINNER.value:
# if user is beginner, get only projects with ANY or TEAMS mapping permission
# in the later case, only those that are associated with user teams
query = query.filter(
or_(
and_(
Project.id.in_(selection),
getattr(Project, permission)
== permission_class.TEAMS.value,
),
getattr(Project, permission) == permission_class.ANY.value,
)
)
else:
# if user is intermediate or advanced, get projects with ANY or LEVEL permission
# and projects associated with user teams
query = query.filter(
or_(
Project.id.in_(selection),
getattr(Project, permission).in_(
[
permission_class.ANY.value,
permission_class.LEVEL.value,
]
),
)
else:
permission_class = MappingPermission
team_roles = [
TeamRoles.MAPPER.value,
TeamRoles.VALIDATOR.value,
TeamRoles.PROJECT_MANAGER.value,
]

subquery = """
AND EXISTS (
SELECT 1
FROM project_teams pt
JOIN teams t ON t.id = pt.team_id
WHERE pt.project_id = p.id
AND t.id IN (
SELECT tm.team_id
FROM team_members tm
WHERE tm.user_id = :user_id AND tm.active = true
)
AND pt.role = ANY(:team_roles)
)
"""

return query
if user.mapping_level == MappingLevel.BEGINNER.value:
subquery += f"""
AND (p.{permission} IN (:teams_permission, :any_permission))
"""
params = {
"user_id": user.id,
"team_roles": tuple(team_roles),
"teams_permission": permission_class.TEAMS.value,
"any_permission": permission_class.ANY.value,
}
else:
subquery += f"""
AND (p.{permission} IN (:any_permission, :level_permission))
"""
params = {
"user_id": user.id,
"team_roles": tuple(team_roles),
"any_permission": permission_class.ANY.value,
"level_permission": permission_class.LEVEL.value,
}
return subquery, params

@staticmethod
def filter_projects_to_map(query, user):
"""Filter projects that needs mapping and can be mapped by the current user."""
query = query.filter(
Project.tasks_mapped + Project.tasks_validated
< Project.total_tasks - Project.tasks_bad_imagery
)
return ProjectSearchService.filter_by_user_permission(
query, user, "mapping_permission"
)
async def filter_projects_to_map(user, db: Database):
"""Filter projects that need mapping and can be mapped by the current user."""
query = """
SELECT DISTINCT p.id
FROM projects p
WHERE (p.tasks_mapped + p.tasks_validated) < (p.total_tasks - p.tasks_bad_imagery)
"""
params = {}
if user and user.role != UserRole.ADMIN.value:
(
subquery,
subquery_params,
) = await ProjectSearchService.filter_by_user_permission(
db, user, "mapping_permission"
)
query += subquery
params.update(subquery_params)

# Execute the query with parameters
project_records = await db.fetch_all(query, params)
return [record["id"] for record in project_records] if project_records else []

@staticmethod
def filter_projects_to_validate(query, user):
"""Filter projects that needs validation and can be validated by the current user."""
query = query.filter(
Project.tasks_validated < Project.total_tasks - Project.tasks_bad_imagery
)
return ProjectSearchService.filter_by_user_permission(
query, user, "validation_permission"
)
async def filter_projects_to_validate(user, db: Database):
"""Filter projects that need validation and can be validated by the current user."""
# Base query to get unique project IDs that need validation
query = """
SELECT DISTINCT p.id
FROM projects p
WHERE p.tasks_validated < (p.total_tasks - p.tasks_bad_imagery)
"""

params = {}
if user and user.role != UserRole.ADMIN.value:
(
subquery,
subquery_params,
) = await ProjectSearchService.filter_by_user_permission(
db, user, "validation_permission"
)
query += subquery
params.update(subquery_params)

project_records = await db.fetch_all(query, params)
return [record["id"] for record in project_records] if project_records else []

@staticmethod
async def get_projects_geojson(
Expand Down

0 comments on commit 4fda6e3

Please sign in to comment.