Skip to content

Commit

Permalink
Merge pull request #3980 from hotosm/develop
Browse files Browse the repository at this point in the history
v4.2.3 release
  • Loading branch information
willemarcel committed Dec 15, 2020
2 parents a9ad143 + 29cdfb8 commit b2847dc
Show file tree
Hide file tree
Showing 60 changed files with 883 additions and 255 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This is Free and Open Source Software. You are welcome to use the code and set u

## Get involved!

* Check our [Code of conduct](./docs/code_of_conduct.md)
* Get familiar with our [contributor guidelines](./docs/contributing.md)
* Join the [working groups](./docs/working-groups.md)
* Help us to [translate the user interface](./docs/contributing-translation.md)
Expand Down
9 changes: 8 additions & 1 deletion backend/api/projects/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,7 @@ def setup_search_dto(self):
search_dto = ProjectSearchDTO()
search_dto.preferred_locale = request.environ.get("HTTP_ACCEPT_LANGUAGE")
search_dto.mapper_level = request.args.get("mapperLevel")
search_dto.action = request.args.get("action")
search_dto.organisation_name = request.args.get("organisationName")
search_dto.organisation_id = request.args.get("organisationId")
search_dto.team_id = request.args.get("teamId")
Expand Down Expand Up @@ -602,6 +603,11 @@ def get(self):
name: country
description: Project country
type: string
- in: query
name: action
description: Filter projects by possible actions
enum: [map, validate, any]
type: string
- in: query
name: projectStatuses
description: Authenticated PMs can search for archived or draft statuses
Expand Down Expand Up @@ -949,8 +955,9 @@ def get(self, project_id):
if request.args.get("as_file")
else False
)
locale = request.environ.get("HTTP_ACCEPT_LANGUAGE")
project_dto = ProjectService.get_project_dto_for_mapper(
project_id, request.environ.get("HTTP_ACCEPT_LANGUAGE"), True
project_id, None, locale, True
)
project_dto = project_dto.to_primitive()

Expand Down
1 change: 1 addition & 0 deletions backend/models/dtos/project_dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ class ProjectSearchDTO(Model):

preferred_locale = StringType(default="en")
mapper_level = StringType(validators=[is_known_mapping_level])
action = StringType(choices=("map", "validate", "any"))
mapping_types = ListType(StringType, validators=[is_known_mapping_type])
mapping_types_exact = BooleanType(required=False)
project_statuses = ListType(StringType, validators=[is_known_project_status])
Expand Down
2 changes: 1 addition & 1 deletion backend/models/postgis/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def get_all_users(query: UserSearchQuery) -> UserSearchDTO:
]
base = base.filter(User.mapping_level.in_(mapping_level_array))
if query.username:
base = base.filter(User.username.ilike(query.username.lower() + "%"))
base = base.filter(User.username.ilike(("%" + query.username + "%")))

if query.role:
roles = query.role.split(",")
Expand Down
3 changes: 2 additions & 1 deletion backend/services/messaging/smtp_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ def _init_smtp_client():
smtp_settings = current_app.config["SMTP_SETTINGS"]
sender = smtplib.SMTP(smtp_settings["host"], port=smtp_settings["smtp_port"])
sender.starttls()
sender.login(smtp_settings["smtp_user"], smtp_settings["smtp_password"])
if smtp_settings["smtp_user"] and smtp_settings["smtp_password"]:
sender.login(smtp_settings["smtp_user"], smtp_settings["smtp_password"])

return sender

Expand Down
93 changes: 90 additions & 3 deletions backend/services/project_search_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import math
import geojson
from geoalchemy2 import shape
from sqlalchemy import func, distinct, desc, or_
from sqlalchemy import func, distinct, desc, or_, and_
from shapely.geometry import Polygon, box
from cachetools import TTLCache, cached

Expand All @@ -22,6 +22,8 @@
ProjectPriority,
UserRole,
TeamRoles,
ValidationPermission,
MappingPermission,
)
from backend.models.postgis.campaign import Campaign
from backend.models.postgis.organisation import Organisation
Expand Down Expand Up @@ -244,6 +246,11 @@ def _filter_projects(search_dto: ProjectSearchDTO, user):
query = query.filter(
Project.mapper_level == MappingLevel[search_dto.mapper_level].value
)
if search_dto.action and search_dto.action != "any":
if search_dto.action == "map":
query = ProjectSearchService.filter_projects_to_map(query, user)
if search_dto.action == "validate":
query = ProjectSearchService.filter_projects_to_validate(query, user)

if search_dto.organisation_name:
query = query.filter(Organisation.name == search_dto.organisation_name)
Expand Down Expand Up @@ -348,11 +355,91 @@ def _filter_projects(search_dto: ProjectSearchDTO, user):

return all_results, paginated_results

@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
]
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,
]
),
)
)

return query

@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"
)

@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"
)

@staticmethod
def get_projects_geojson(
search_bbox_dto: ProjectSearchBBoxDTO,
) -> geojson.FeatureCollection:
""" search for projects meeting criteria provided return as a geojson feature collection"""
"""Search for projects meeting the provided criteria. Returns a GeoJSON feature collection."""

# make a polygon from provided bounding box
polygon = ProjectSearchService._make_4326_polygon_from_bbox(
Expand Down Expand Up @@ -394,7 +481,7 @@ def get_projects_geojson(

@staticmethod
def _get_intersecting_projects(search_polygon: Polygon, author_id: int):
""" executes a database query to get the intersecting projects created by the author if provided """
"""Executes a database query to get the intersecting projects created by the author if provided """

query = db.session.query(
Project.id,
Expand Down
15 changes: 8 additions & 7 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"license": "BSD-2-Clause",
"private": false,
"dependencies": {
"@formatjs/intl-locale": "^2.4.8",
"@formatjs/intl-pluralrules": "^4.0.0",
"@formatjs/intl-relativetimeformat": "^8.0.0",
"@formatjs/intl-utils": "^3.8.4",
Expand All @@ -16,8 +17,8 @@
"@mapbox/mapbox-gl-language": "^0.10.1",
"@mapbox/togeojson": "^0.16.0",
"@reach/router": "^1.3.4",
"@sentry/react": "^5.28.0",
"@sentry/tracing": "^5.28.0",
"@sentry/react": "^5.29.0",
"@sentry/tracing": "^5.29.0",
"@turf/area": "^6.0.1",
"@turf/bbox": "^6.0.1",
"@turf/bbox-polygon": "^6.0.1",
Expand All @@ -28,15 +29,15 @@
"@webscopeio/react-textarea-autocomplete": "^4.7.2",
"axios": "^0.21.0",
"chart.js": "^2.9.4",
"dompurify": "^2.2.2",
"dompurify": "^2.2.4",
"downshift-hooks": "^0.8.1",
"final-form": "^4.20.1",
"fromentries": "^1.3.2",
"humanize-duration": "^3.24.0",
"humanize-duration": "^3.25.0",
"immutable": "^4.0.0-rc.12",
"mapbox-gl": "^1.13.0",
"mapbox-gl-draw-rectangle-mode": "^1.0.4",
"marked": "^1.2.5",
"marked": "^1.2.7",
"node-sass": "^4.14.1",
"osmtogeojson": "^3.0.0-beta.3",
"query-string": "^6.13.7",
Expand Down Expand Up @@ -95,10 +96,10 @@
"devDependencies": {
"@testing-library/jest-dom": "^5.11.6",
"@testing-library/react": "^11.2.2",
"@testing-library/react-hooks": "^3.4.2",
"@testing-library/react-hooks": "^3.7.0",
"combine-react-intl-messages": "^4.0.0",
"jest-canvas-mock": "^2.3.0",
"msw": "^0.24.1",
"msw": "^0.24.2",
"prettier": "^2.2.1",
"react-test-renderer": "^16.14.0"
},
Expand Down
20 changes: 11 additions & 9 deletions frontend/src/components/footer.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,17 @@ export function Footer({ location }: Object) {
</Link>
))}
<p className="pt5-l pt4 pb3">
{socialNetworks.map((item, n) => (
<a
key={n}
href={item.link}
className="link barlow-condensed white f4 ttu di-l dib ph2"
>
{item.icon}
</a>
))}
{socialNetworks
.filter((item) => item.link)
.map((item, n) => (
<a
key={n}
href={item.link}
className="link barlow-condensed white f4 ttu di-l dib ph2"
>
{item.icon}
</a>
))}
</p>
</div>
</div>
Expand Down
54 changes: 23 additions & 31 deletions frontend/src/components/projectCard/dueDateBox.js
Original file line number Diff line number Diff line change
@@ -1,73 +1,65 @@
import React, { useState, useEffect } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import humanizeDuration from 'humanize-duration';
import ReactTooltip from 'react-tooltip';

import { ClockIcon } from '../svgIcons';
import messages from './messages';

export function DueDateBox({ dueDate, intervalMili, align = 'right' }: Object) {
export function DueDateBox({ dueDate, intervalMili, align = 'right', tooltipMsg }: Object) {
const intl = useIntl();
const [timer, setTimer] = useState(Date.now());
useEffect(() => {
let interval;

if (intervalMili) {
interval = setInterval(() => {
setTimer(Date.now());
}, intervalMili); // 1 minute
}

return () => {
clearInterval(interval);
};
}, [intervalMili]);

if (dueDate === undefined) {
return null;
} else if (new Date(dueDate) === undefined) {
if (dueDate === undefined || new Date(dueDate) === undefined) {
return null;
}

const milliDifference = new Date(dueDate) - timer;
const langCodeOnly = intl.locale.slice(0, 2);

let options = {
language: langCodeOnly,
fallbacks: ['en'],
largest: 1,
};
let options = { language: intl.locale.slice(0, 2), fallbacks: ['en'], largest: 1 };

let className = `dib relative lh-solid f7 tr ${
align === 'right' ? 'fr' : 'fl'
} br1 link ph1 pv2 bg-grey-light blue-grey truncate mw4`;

if (intervalMili !== undefined) {
className = className.replace('mw4', '');
options = {
units: ['h', 'm'],
round: true,
};
options = { units: ['h', 'm'], round: true };
}

const milliDifference = new Date(dueDate) - timer;
if (milliDifference < 60000 * 20 && intervalMili !== undefined) {
className = className.replace('bg-grey-light', 'bg-red').replace('blue-grey', 'white');
}

if (milliDifference > 0) {
return (
<span className={className}>
<span>
<ClockIcon className="absolute pl1 top-0 pt1 left-0" />
</span>
<span className="pl3 ml1">
<FormattedMessage
className="indent"
{...messages['dueDateRelativeRemainingDays']}
values={{
daysLeftHumanize: humanizeDuration(milliDifference, options),
}}
/>
<>
<span className={className} data-tip={tooltipMsg}>
<span>
<ClockIcon className="absolute pl1 top-0 pt1 left-0" />
</span>
<span className="pl3 ml1 v-mid">
<FormattedMessage
className="indent"
{...messages['dueDateRelativeRemainingDays']}
values={{
daysLeftHumanize: humanizeDuration(milliDifference, options),
}}
/>
</span>
</span>
</span>
{tooltipMsg && <ReactTooltip place="bottom" />}
</>
);
} else {
return null;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/projectCard/projectCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export function ProjectCard({
percentMapped={percentMapped}
percentValidated={percentValidated}
/>
<div className="cf pt2 h2 truncate">
<div className="cf pt2 truncate">
<MappingLevelMessage
level={mapperLevel}
className="fl f7 pv2 ttc fw5 blue-grey truncate"
Expand Down
Loading

0 comments on commit b2847dc

Please sign in to comment.