Skip to content

Commit

Permalink
Merge pull request #39 from naxa-developers/naxa/deploy/feature/liveM…
Browse files Browse the repository at this point in the history
…onitoring

Naxa/deploy/feature/live monitoring
  • Loading branch information
mahesh-naxa authored Jan 9, 2024
2 parents 732153b + 5bcf8cd commit 2f85db7
Show file tree
Hide file tree
Showing 12 changed files with 172 additions and 25 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
command: |
yarn --version
cd ${CIRCLE_WORKING_DIRECTORY}/frontend
yarn install
yarn install --network-concurrency 1 # lodash which has prepare script fails. Hotfix: https://github.com/yarnpkg/yarn/issues/6312
- save_cache:
key: yarn-deps-{{ checksum "frontend/yarn.lock" }}
paths:
Expand Down
13 changes: 13 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# These are supported funding model platforms

github: hotosm
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
5 changes: 5 additions & 0 deletions backend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ def add_api_endpoints(app):
ProjectsQueriesPriorityAreasAPI,
ProjectsQueriesFeaturedAPI,
ProjectQueriesSimilarProjectsAPI,
ProjectQueriesActiveProjectsAPI,
)
from backend.api.projects.activities import (
ProjectsActivitiesAPI,
Expand Down Expand Up @@ -428,6 +429,10 @@ def add_api_endpoints(app):
ProjectQueriesSimilarProjectsAPI,
format_url("projects/queries/<int:project_id>/similar-projects/"),
)
api.add_resource(
ProjectQueriesActiveProjectsAPI,
format_url("projects/queries/active/"),
)

# Projects' addtional resources
api.add_resource(
Expand Down
45 changes: 45 additions & 0 deletions backend/api/projects/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -1164,3 +1164,48 @@ def get(self, project_id):
project_id, authenticated_user_id, preferred_locale, limit
)
return projects_dto.to_primitive(), 200


class ProjectQueriesActiveProjectsAPI(Resource):
@token_auth.login_required(optional=True)
def get(self):
"""
Get active projects
---
tags:
- projects
produces:
- application/json
parameters:
- in: header
name: Authorization
description: Base64 encoded session token
required: false
type: string
default: Token sessionTokenHere==
- name: interval
in: path
description: Time interval in hours to get active project
required: false
type: integer
default: 24
responses:
200:
description: Active projects geojson
404:
description: Project not found or project is not published
500:
description: Internal Server Error
"""
interval = request.args.get("interval", "24")
if not interval.isdigit():
return {
"Error": "Interval must be a number greater than 0 and less than or equal to 24"
}, 400
interval = int(interval)
if interval <= 0 or interval > 24:
return {
"Error": "Interval must be a number greater than 0 and less than or equal to 24"
}, 400
projects_dto = ProjectService.get_active_projects(interval)
return projects_dto, 200
36 changes: 36 additions & 0 deletions backend/services/project_service.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import threading
from cachetools import TTLCache, cached
from flask import current_app
import geojson
from datetime import datetime, timedelta, timezone

from backend.exceptions import NotFound
from backend.models.dtos.mapping_dto import TaskDTOs
Expand Down Expand Up @@ -615,3 +617,37 @@ def send_email_on_project_progress(project_id):
project_completion,
),
).start()

@staticmethod
def get_active_projects(interval):
action_date = datetime.now(timezone.utc) - timedelta(hours=interval)
result = (
TaskHistory.query.with_entities(TaskHistory.project_id)
.distinct()
.filter(TaskHistory.action_date >= action_date)
.all()
)
project_ids = [row.project_id for row in result]
projects = (
Project.query.with_entities(
Project.id,
Project.mapping_types,
Project.geometry.ST_AsGeoJSON().label("geometry"),
)
.filter(
Project.status == ProjectStatus.PUBLISHED.value,
Project.id.in_(project_ids),
)
.all()
)
features = []
for project in projects:
properties = {
"project_id": project.id,
"mapping_types": project.mapping_types,
}
feature = geojson.Feature(
geometry=geojson.loads(project.geometry), properties=properties
)
features.append(feature)
return geojson.FeatureCollection(features)
8 changes: 6 additions & 2 deletions example.env
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,12 @@ TM_DEFAULT_LOCALE=en
# Sentry.io DSN Config (optional)
# TM_SENTRY_BACKEND_DSN=https://foo.ingest.sentry.io/1234567
# TM_SENTRY_FRONTEND_DSN=https://bar.ingest.sentry.io/8901234
#

# Underpass API URL (for project live monitoring feature)
UNDERPASS_URL=https://underpass.hotosm.org


EXPORT TOOL Integration with 0(Disable) and 1(Enable) and S3 URL for Export Tool
#EXPORT TOOL Integration with 0(Disable) and 1(Enable) and S3 URL for Export Tool
#EXPORT_TOOL_S3_URL=https://foorawdataapi.s3.amazonaws.com
#ENABLE_EXPORT_TOOL=0
#ENABLE_EXPORT_TOOL=0
13 changes: 13 additions & 0 deletions frontend/src/api/projects.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import axios from 'axios';
import { subMonths, format } from 'date-fns';
import { useQuery } from '@tanstack/react-query';
import { useSelector } from 'react-redux';
Expand Down Expand Up @@ -187,6 +188,18 @@ export const submitValidationTask = (projectId, payload, token, locale) => {
);
};

export const useAvailableCountriesQuery = () => {
const fetchGeojsonData = () => {
return axios.get(`https://underpass.live/availability.json`);
};

return useQuery({
queryKey: ['priority-geojson'],
queryFn: fetchGeojsonData,
select: (res) => res.data,
});
};

const backendToQueryConversion = {
difficulty: 'difficulty',
campaign: 'campaign',
Expand Down
30 changes: 22 additions & 8 deletions frontend/src/components/projectDetail/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ import { ProjectInfoPanel } from './infoPanel';
import { OSMChaButton } from './osmchaButton';
import { LiveViewButton } from './liveViewButton';
import { useSetProjectPageTitleTag } from '../../hooks/UseMetaTags';
import { useProjectContributionsQuery, useProjectTimelineQuery } from '../../api/projects';
import {
useProjectContributionsQuery,
useProjectTimelineQuery,
useAvailableCountriesQuery,
} from '../../api/projects';
import { Alert } from '../alert';

import './styles.scss';
Expand Down Expand Up @@ -154,6 +158,16 @@ export const ProjectDetail = (props) => {
</Link>
);

const { data } = useAvailableCountriesQuery();

// check if the project has live monitoring feature enabled
// based on the country list provided by available.json
const hasLiveMonitoringFeature = !data
? false
: props.project.countryTag.some((country) =>
data.countries.some((item) => country.toLowerCase() === item.toLowerCase()),
);

return (
<div className={`${props.className || 'blue-dark'}`}>
<div className="db flex-l tasks-map-height">
Expand Down Expand Up @@ -348,13 +362,13 @@ export const ProjectDetail = (props) => {
className="bg-white blue-dark ba b--grey-light pa3"
/>

{/* TODO: Enable/disable this button depending of
Underpass availability for the project's area.
https://underpass.hotosm.org/priority.geojson */}
<LiveViewButton
projectId={props.project.projectId}
className="bg-white blue-dark ba b--grey-light pa3"
/>
{/* show live view button only when the project has live monitoring feature */}
{hasLiveMonitoringFeature && (
<LiveViewButton
projectId={props.project.projectId}
className="bg-white blue-dark ba b--grey-light pa3"
/>
)}

<DownloadAOIButton
projectId={props.project.projectId}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/projectDetail/liveViewButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { FormattedMessage } from 'react-intl';
import messages from './messages';
import { CustomButton } from '../button';

export const LiveViewButton = ({ projectId, className, compact = false }: Object) => (
export const LiveViewButton = ({ projectId, className, compact = false }) => (
<Link to={`/projects/${projectId}/live`} className="pr2">
{
<CustomButton className={className}>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const API_URL = process.env.REACT_APP_API_URL
export const OHSOME_STATS_BASE_URL =
process.env.REACT_APP_OHSOME_STATS_BASE_URL || 'https://stats.now.ohsome.org/api';
export const OHSOME_STATS_TOKEN = process.env.REACT_APP_OHSOME_STATS_TOKEN || '';
export const UNDERPASS_URL = process.env.UNDERPASS_URL || 'https://underpass.hotosm.org';

// APPLICATION SETTINGS
export const DEFAULT_LOCALE = process.env.REACT_APP_DEFAULT_LOCALE || 'en';
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/views/projectLiveMonitoring.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import React, { useState, useRef, useEffect } from 'react';
import ReactPlaceholder from 'react-placeholder';
import centroid from '@turf/centroid';
import {
UnderpassFeatureList,
UnderpassMap,
Expand All @@ -10,14 +12,12 @@ import { ProjectHeader } from '../components/projectDetail/header';
import { useSetTitleTag } from '../hooks/UseMetaTags';
import { useParams } from 'react-router-dom';
import { useFetch } from '../hooks/UseFetch';
import ReactPlaceholder from 'react-placeholder';
import centroid from '@turf/centroid';
import './projectLiveMonitoring.css';
import { MAPBOX_TOKEN } from '../config';

const config = {
API_URL: 'http://localhost:8000',
MAPBOX_TOKEN:
'pk.eyJ1IjoiZW1pNDIwIiwiYSI6ImNqZW9leG5pZTAxYWwyeG83bHU0eHM0ZXcifQ.YWmk4Rp8FBGCLmpx_huJYw',
API_URL: `https://underpass.live:8000`,
MAPBOX_TOKEN: MAPBOX_TOKEN,
};

const statusList = {
Expand Down
32 changes: 24 additions & 8 deletions tests/backend/integration/api/tasks/test_statistics.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime
from datetime import datetime, timedelta

from backend.models.postgis.task import Task, TaskStatus
from backend.services.campaign_service import CampaignService, CampaignProjectDTO
Expand Down Expand Up @@ -105,7 +105,11 @@ def test_returns_200_if_valid_date_range(self):
response = self.client.get(
self.url,
headers={"Authorization": self.user_session_token},
query_string={"startDate": "2023-01-01"},
query_string={
"startDate": (datetime.now() - timedelta(days=6 * 30)).strftime(
"%Y-%m-%d"
)
},
)
# Assert
self.assertEqual(response.status_code, 200)
Expand All @@ -130,7 +134,9 @@ def test_filters_task_by_project(self):
self.url,
headers={"Authorization": self.user_session_token},
query_string={
"startDate": "2023-01-01",
"startDate": (datetime.now() - timedelta(days=6 * 30)).strftime(
"%Y-%m-%d"
),
"projectId": self.test_project_1.id,
},
)
Expand All @@ -152,7 +158,9 @@ def test_filters_by_multiple_projects(self):
self.url,
headers={"Authorization": self.user_session_token},
query_string={
"startDate": "2023-01-01",
"startDate": (datetime.now() - timedelta(days=6 * 30)).strftime(
"%Y-%m-%d"
),
"projectId": f"{self.test_project_1.id}, {self.test_project_2.id}",
},
)
Expand All @@ -173,7 +181,9 @@ def test_filters_by_organisation_id(self):
self.url,
headers={"Authorization": self.user_session_token},
query_string={
"startDate": "2023-01-01",
"startDate": (datetime.now() - timedelta(days=6 * 30)).strftime(
"%Y-%m-%d"
),
"organisationId": test_organisation.id,
},
)
Expand All @@ -194,7 +204,9 @@ def test_filters_by_organisation_name(self):
self.url,
headers={"Authorization": self.user_session_token},
query_string={
"startDate": "2023-01-01",
"startDate": (datetime.now() - timedelta(days=6 * 30)).strftime(
"%Y-%m-%d"
),
"organisationName": test_organisation.name,
},
)
Expand All @@ -217,7 +229,9 @@ def test_filters_by_campaign(self):
self.url,
headers={"Authorization": self.user_session_token},
query_string={
"startDate": "2023-01-01",
"startDate": (datetime.now() - timedelta(days=6 * 30)).strftime(
"%Y-%m-%d"
),
"campaign": test_campaign.name,
},
)
Expand All @@ -239,7 +253,9 @@ def test_filters_by_country(self):
self.url,
headers={"Authorization": self.user_session_token},
query_string={
"startDate": "2023-01-01",
"startDate": (datetime.now() - timedelta(days=6 * 30)).strftime(
"%Y-%m-%d"
),
"country": "Nepal",
},
)
Expand Down

0 comments on commit 2f85db7

Please sign in to comment.