Skip to content

Commit

Permalink
Merge pull request #4178 from hotosm/develop
Browse files Browse the repository at this point in the history
v4.3.0 Release
  • Loading branch information
willemarcel authored Feb 2, 2021
2 parents 67d1929 + 91f5bd1 commit 82b473f
Show file tree
Hide file tree
Showing 90 changed files with 7,120 additions and 3,277 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ frontend/public/static/
frontend/assets/styles/
frontend/package-lock.json
frontend/.env
frontend/.eslintcache
frontend/coverage/

# Ignore dist folder as this contains the minimized build for deploy
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ RUN apk update && \
apk add \
postgresql-dev \
gcc \
g++ \
python3-dev \
musl-dev \
libffi-dev \
Expand Down
10 changes: 10 additions & 0 deletions backend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ def add_api_endpoints(app):
TasksActionsResetAllAPI,
TasksActionsSplitAPI,
)
from backend.api.tasks.statistics import (
TasksStatisticsAPI,
)

# Comments API impor
from backend.api.comments.resources import (
Expand Down Expand Up @@ -533,6 +536,13 @@ def add_api_endpoints(app):
format_url("projects/<int:project_id>/tasks/actions/split/<int:task_id>/"),
)

# Tasks Statistics endpoint
api.add_resource(
TasksStatisticsAPI,
format_url("tasks/statistics/"),
methods=["GET"],
)

# Comments REST endoints
api.add_resource(
CommentsProjectsRestAPI,
Expand Down
107 changes: 107 additions & 0 deletions backend/api/tasks/statistics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
from datetime import date, datetime, timedelta
from flask_restful import Resource, current_app, request

from backend.services.users.authentication_service import token_auth
from backend.services.stats_service import StatsService


def validate_date_input(input_date):
try:
if not isinstance(input_date, date):
input_date = datetime.strptime(input_date, "%Y-%m-%d").date()
return input_date
except (TypeError, ValueError):
raise ValueError("Invalid date value")


class TasksStatisticsAPI(Resource):
@token_auth.login_required
def get(self):
"""
Get Task Stats
---
tags:
- tasks
produces:
- application/json
parameters:
- in: header
name: Authorization
description: Base64 encoded session token
type: string
required: true
default: Token sessionTokenHere==
- in: query
name: startDate
description: Date to filter as minimum
required: true
type: string
- in: query
name: endDate
description: Date to filter as maximum. Default value is the current date.
required: false
type: string
- in: query
name: organisationName
description: Organisation name to filter by
required: false
- in: query
name: organisationId
description: Organisation ID to filter by
required: false
- in: query
name: campaign
description: Campaign name to filter by
required: false
- in: query
name: projectId
description: Project IDs to filter by
required: false
- in: query
name: country
description: Country name to filter by
required: false
responses:
200:
description: Task statistics
400:
description: Bad Request
401:
description: Request is not authenticated
500:
description: Internal Server Error
"""
try:
start_date = validate_date_input(request.args.get("startDate"))
end_date = validate_date_input(request.args.get("endDate", date.today()))
if not (start_date):
raise KeyError("Missing start date parameter")
if end_date < start_date:
raise ValueError("Start date must be earlier than end date")
if (end_date - start_date) > timedelta(days=366):
raise ValueError("Date range can not be bigger than 1 year")
organisation_id = request.args.get("organisationId", None, int)
organisation_name = request.args.get("organisationName", None, str)
campaign = request.args.get("campaign", None, str)
project_id = request.args.get("projectId")
if project_id:
project_id = map(str, project_id.split(","))
country = request.args.get("country", None, str)
task_stats = StatsService.get_task_stats(
start_date,
end_date,
organisation_id,
organisation_name,
campaign,
project_id,
country,
)
return task_stats.to_primitive(), 200
except (KeyError, ValueError) as e:
error_msg = f"Task Statistics GET - {str(e)}"
current_app.logger.critical(error_msg)
return {"Error": error_msg}, 400
except Exception as e:
error_msg = f"Task Statistics GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
return {"Error": "Unable to fetch task statistics"}, 500
21 changes: 20 additions & 1 deletion backend/models/dtos/stats_dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class UserContribution(Model):


class ProjectContributionsDTO(Model):
""" DTO for all user contributons on a project """
""" DTO for all user contributions on a project """

def __init__(self):
super().__init__()
Expand Down Expand Up @@ -141,3 +141,22 @@ def __init__(self):
# avg_completion_time = IntType(serialized_name='averageCompletionTime')
organisations = ListType(ModelType(OrganizationListStatsDTO))
campaigns = ListType(ModelType(CampaignStatsDTO))


class TaskStats(Model):
""" DTO for tasks stats for a single day """

date = DateType(required=True)
mapped = IntType(serialized_name="mapped")
validated = IntType(serialized_name="validated")
bad_imagery = IntType(serialized_name="badImagery")


class TaskStatsDTO(Model):
""" Contains all tasks stats broken down by day"""

def __init__(self):
super().__init__()
self.stats = []

stats = ListType(ModelType(TaskStats), serialized_name="taskStats")
23 changes: 13 additions & 10 deletions backend/models/postgis/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -1139,16 +1139,19 @@ def calculate_tasks_percent(
target, total_tasks, tasks_mapped, tasks_validated, tasks_bad_imagery
):
""" Calculates percentages of contributions """
if target == "mapped":
return int(
(tasks_mapped + tasks_validated)
/ (total_tasks - tasks_bad_imagery)
* 100
)
elif target == "validated":
return int(tasks_validated / (total_tasks - tasks_bad_imagery) * 100)
elif target == "bad_imagery":
return int((tasks_bad_imagery / total_tasks) * 100)
try:
if target == "mapped":
return int(
(tasks_mapped + tasks_validated)
/ (total_tasks - tasks_bad_imagery)
* 100
)
elif target == "validated":
return int(tasks_validated / (total_tasks - tasks_bad_imagery) * 100)
elif target == "bad_imagery":
return int((tasks_bad_imagery / total_tasks) * 100)
except ZeroDivisionError:
return 0

def as_dto_for_admin(self, project_id):
""" Creates a Project DTO suitable for transmitting to project admins """
Expand Down
9 changes: 9 additions & 0 deletions backend/services/campaign_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ def get_campaign(campaign_id: int) -> Campaign:

return campaign

@staticmethod
def get_campaign_by_name(campaign_name: str) -> Campaign:
campaign = Campaign.query.filter_by(name=campaign_name).first()

if campaign is None:
raise NotFound()

return campaign

@staticmethod
def delete_campaign(campaign_id: int):
"""Delete campaign for a project"""
Expand Down
4 changes: 2 additions & 2 deletions backend/services/messaging/message_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def send_message_after_validation(
):
""" Sends mapper a notification after their task has been marked valid or invalid """
if validated_by == mapped_by:
return # No need to send a message to yourself
return # No need to send a notification if you've verified your own task

user = UserService.get_user_by_id(mapped_by)
text_template = get_txt_template(
Expand Down Expand Up @@ -174,10 +174,10 @@ def _push_messages(messages):
messages_objs.append(obj)
continue
messages_objs.append(obj)

SMTPService.send_email_alert(
user.email_address,
user.username,
user.is_email_verified,
message["message"].id,
UserService.get_user_by_id(message["message"].from_user_id).username,
message["message"].project_id,
Expand Down
7 changes: 6 additions & 1 deletion backend/services/messaging/smtp_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def send_contact_admin_email(data):
def send_email_alert(
to_address: str,
username: str,
user_email_verified: bool,
message_id: int,
from_username: str,
project_id: int,
Expand All @@ -57,7 +58,11 @@ def send_email_alert(
content: str,
message_type: int,
):
"""Send an email to user to alert that they have a new message"""
"""Send an email to user to alert that they have a new message."""

if not user_email_verified:
return False

current_app.logger.debug(f"Test if email required {to_address}")
from_user_link = f"{current_app.config['APP_BASE_URL']}/users/{from_username}"
project_link = f"{current_app.config['APP_BASE_URL']}/projects/{project_id}"
Expand Down
Loading

0 comments on commit 82b473f

Please sign in to comment.