Skip to content

Commit

Permalink
Merge pull request #4562 from hotosm/develop
Browse files Browse the repository at this point in the history
v4.4.5
  • Loading branch information
willemarcel authored Apr 27, 2021
2 parents 7fccfbd + 182f57b commit ffd1373
Show file tree
Hide file tree
Showing 77 changed files with 2,265 additions and 1,289 deletions.
4 changes: 2 additions & 2 deletions backend/api/campaigns/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,8 @@ def post(self):
try:
campaign = CampaignService.create_campaign(campaign_dto)
return {"campaignId": campaign.id}, 200
except ValueError:
error_msg = "Campaign POST - name already exists"
except ValueError as e:
error_msg = f"Campaign POST - {str(e)}"
return {"Error": error_msg}, 409
except Exception as e:
error_msg = f"Campaign POST - unhandled error: {str(e)}"
Expand Down
27 changes: 25 additions & 2 deletions backend/api/projects/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ def delete(self, project_id):

class ProjectSearchBase(Resource):
@token_auth.login_required(optional=True)
def setup_search_dto(self):
def setup_search_dto(self) -> ProjectSearchDTO:
search_dto = ProjectSearchDTO()
search_dto.preferred_locale = request.environ.get("HTTP_ACCEPT_LANGUAGE")
search_dto.mapper_level = request.args.get("mapperLevel")
Expand All @@ -497,6 +497,10 @@ def setup_search_dto(self):
search_dto.omit_map_results = strtobool(
request.args.get("omitMapResults", "false")
)
search_dto.last_updated_gte = request.args.get("lastUpdatedFrom")
search_dto.last_updated_lte = request.args.get("lastUpdatedTo")
search_dto.created_gte = request.args.get("createdFrom")
search_dto.created_lte = request.args.get("createdTo")

# See https://github.com/hotosm/tasking-manager/pull/922 for more info
try:
Expand Down Expand Up @@ -612,6 +616,22 @@ def get(self):
name: projectStatuses
description: Authenticated PMs can search for archived or draft statuses
type: string
- in: query
name: lastUpdatedFrom
description: Filter projects whose last update date is equal or greater than a date
type: string
- in: query
name: lastUpdatedTo
description: Filter projects whose last update date is equal or lower than a date
type: string
- in: query
name: createdFrom
description: Filter projects whose creation date is equal or greater than a date
type: string
- in: query
name: createdTo
description: Filter projects whose creation date is equal or lower than a date
type: string
- in: query
name: interests
type: string
Expand Down Expand Up @@ -666,8 +686,11 @@ def get(self):
return results_dto.to_primitive(), 200
except NotFound:
return {"mapResults": {}, "results": []}, 200
except (KeyError, ValueError) as e:
error_msg = f"Projects GET - {str(e)}"
return {"Error": error_msg}, 400
except Exception as e:
error_msg = f"Project GET - unhandled error: {str(e)}"
error_msg = f"Projects GET - unhandled error: {str(e)}"
current_app.logger.critical(error_msg)
return {"Error": "Unable to fetch projects"}, 500

Expand Down
9 changes: 8 additions & 1 deletion backend/models/dtos/campaign_dto.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
from schematics import Model
from schematics.types import StringType, IntType, ListType, ModelType
from backend.models.dtos.organisation_dto import OrganisationDTO
from schematics.exceptions import ValidationError


def is_existent(value):
if value.strip() == "":
raise ValidationError(u"Empty campaign name string")
return value


class NewCampaignDTO(Model):
""" Describes JSON model to create a campaign """

name = StringType(serialize_when_none=False)
name = StringType(serialize_when_none=False, validators=[is_existent])
logo = StringType(serialize_when_none=False)
url = StringType(serialize_when_none=False)
description = StringType(serialize_when_none=False)
Expand Down
4 changes: 4 additions & 0 deletions backend/models/dtos/project_dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,10 @@ class ProjectSearchDTO(Model):
favorited_by = IntType(required=False)
managed_by = IntType(required=False)
omit_map_results = BooleanType(required=False)
last_updated_lte = StringType(required=False)
last_updated_gte = StringType(required=False)
created_lte = StringType(required=False)
created_gte = StringType(required=False)

def __hash__(self):
""" Make object hashable so we can cache user searches"""
Expand Down
2 changes: 1 addition & 1 deletion backend/models/postgis/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -803,7 +803,7 @@ def get_project_stats(self) -> ProjectStatsDTO:
project_stats.time_to_finish_mapping = time_to_finish_mapping
project_stats.time_to_finish_validating = (
self.total_tasks - (self.tasks_validated + self.tasks_bad_imagery)
) * project_stats.average_validation_time + time_to_finish_mapping
) * project_stats.average_validation_time

return project_stats

Expand Down
7 changes: 5 additions & 2 deletions backend/services/campaign_service.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from backend import db
from flask import current_app
from sqlalchemy.exc import IntegrityError
from psycopg2.errors import UniqueViolation, NotNullViolation
from backend.models.dtos.campaign_dto import (
CampaignDTO,
NewCampaignDTO,
Expand Down Expand Up @@ -100,8 +101,10 @@ def create_campaign(campaign_dto: NewCampaignDTO):
db.session.commit()
except IntegrityError as e:
current_app.logger.info("Integrity error: {}".format(e.args[0]))
raise ValueError()

if isinstance(e.orig, UniqueViolation):
raise ValueError("Campaign name already exists") from e
if isinstance(e.orig, NotNullViolation):
raise ValueError("Campaign name cannot be null") from e
return campaign

@staticmethod
Expand Down
17 changes: 17 additions & 0 deletions backend/services/project_search_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from cachetools import TTLCache, cached

from backend import db
from backend.api.utils import validate_date_input
from backend.models.dtos.project_dto import (
ProjectSearchDTO,
ProjectSearchResultsDTO,
Expand Down Expand Up @@ -319,6 +320,22 @@ def _filter_projects(search_dto: ProjectSearchDTO, user):
sq.c.country.ilike("%{}%".format(search_dto.country))
).filter(Project.id == sq.c.id)

if search_dto.last_updated_gte:
last_updated_gte = validate_date_input(search_dto.last_updated_gte)
query = query.filter(Project.last_updated >= last_updated_gte)

if search_dto.last_updated_lte:
last_updated_lte = validate_date_input(search_dto.last_updated_lte)
query = query.filter(Project.last_updated <= last_updated_lte)

if search_dto.created_gte:
created_gte = validate_date_input(search_dto.created_gte)
query = query.filter(Project.created >= created_gte)

if search_dto.created_lte:
created_lte = validate_date_input(search_dto.created_lte)
query = query.filter(Project.created <= created_lte)

order_by = search_dto.order_by
if search_dto.order_by_type == "DESC":
order_by = desc(search_dto.order_by)
Expand Down
38 changes: 19 additions & 19 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
"license": "BSD-2-Clause",
"private": false,
"dependencies": {
"@formatjs/intl-locale": "^2.4.22",
"@formatjs/intl-pluralrules": "^4.0.14",
"@formatjs/intl-relativetimeformat": "^8.1.5",
"@formatjs/intl-locale": "^2.4.23",
"@formatjs/intl-pluralrules": "^4.0.16",
"@formatjs/intl-relativetimeformat": "^8.1.6",
"@formatjs/macro": "^0.2.8",
"@hotosm/id": "^2.19.6",
"@hotosm/iso-countries-languages": "^1.1.0",
Expand All @@ -16,8 +16,8 @@
"@mapbox/mapbox-gl-geocoder": "^4.7.0",
"@mapbox/mapbox-gl-language": "^0.10.1",
"@reach/router": "^1.3.4",
"@sentry/react": "^6.2.5",
"@sentry/tracing": "^6.2.5",
"@sentry/react": "^6.3.1",
"@sentry/tracing": "^6.3.1",
"@tmcw/togeojson": "^4.4.0",
"@turf/area": "^6.3.0",
"@turf/bbox": "^6.3.0",
Expand All @@ -30,12 +30,12 @@
"@webscopeio/react-textarea-autocomplete": "^4.7.3",
"axios": "^0.21.1",
"chart.js": "^2.9.4",
"date-fns": "^2.20.2",
"date-fns": "^2.21.1",
"dompurify": "^2.2.7",
"downshift-hooks": "^0.8.1",
"final-form": "^4.20.2",
"fromentries": "^1.3.2",
"humanize-duration": "^3.25.1",
"humanize-duration": "^3.25.2",
"immutable": "^4.0.0-rc.12",
"mapbox-gl": "^1.13.1",
"mapbox-gl-draw-rectangle-mode": "^1.0.4",
Expand All @@ -47,28 +47,28 @@
"react-calendar-heatmap": "^1.8.1",
"react-chartjs-2": "^2.11.1",
"react-click-outside": "^3.0.1",
"react-datepicker": "^3.7.0",
"react-datepicker": "^3.8.0",
"react-dom": "^17.0.2",
"react-dropzone": "^11.3.2",
"react-final-form": "^6.5.3",
"react-intl": "^5.15.8",
"react-intl": "^5.17.1",
"react-meta-elements": "^1.0.0",
"react-placeholder": "^4.1.0",
"react-redux": "^7.2.3",
"react-redux": "^7.2.4",
"react-scripts": "^4.0.3",
"react-select": "^4.3.0",
"react-tooltip": "^4.2.17",
"react-tooltip": "^4.2.18",
"reactjs-popup": "^1.5.0",
"redux": "^4.0.5",
"redux": "^4.1.0",
"redux-thunk": "^2.3.0",
"sass": "^1.32.8",
"sass": "^1.32.10",
"short-number": "^1.0.7",
"shpjs": "^3.6.3",
"slug": "^4.0.3",
"slug": "^4.0.4",
"tachyons": "^4.12.0",
"use-query-params": "^1.2.2",
"webfontloader": "^1.6.28",
"workbox-core": "^6.1.2",
"workbox-core": "^6.1.5",
"workbox-expiration": "^6.1.5",
"workbox-precaching": "^6.1.5",
"workbox-recipes": "^6.1.5",
Expand Down Expand Up @@ -104,13 +104,13 @@
]
},
"devDependencies": {
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/jest-dom": "^5.12.0",
"@testing-library/react": "^11.2.6",
"@testing-library/react-hooks": "^5.1.1",
"@testing-library/user-event": "^13.1.2",
"@testing-library/react-hooks": "^5.1.2",
"@testing-library/user-event": "^13.1.5",
"combine-react-intl-messages": "^4.0.0",
"jest-canvas-mock": "^2.3.1",
"msw": "^0.28.1",
"msw": "^0.28.2",
"prettier": "^2.2.1",
"react-select-event": "^5.3.0",
"react-test-renderer": "^17.0.2",
Expand Down
35 changes: 35 additions & 0 deletions frontend/src/components/alert/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { BanIcon, CheckIcon, InfoIcon, AlertIcon } from '../svgIcons';

export const Alert = ({ type = 'info', compact = false, inline = false, children }) => {
const icons = {
info: InfoIcon,
success: CheckIcon,
warning: AlertIcon,
error: BanIcon,
};
const Icon = icons[type];

const color = {
info: 'b--blue bg-lightest-blue',
success: 'b--dark-green bg-washed-green',
warning: 'b--gold bg-washed-yellow',
error: 'b--dark-red bg-washed-red',
};
const iconColor = {
info: 'blue',
success: 'dark-green',
warning: 'gold',
error: 'dark-red',
};

return (
<div
className={`${inline ? 'di' : 'db'} blue-dark bl bw2 br2 ${compact ? 'pa2' : 'pa3'} ${
color[type]
}`}
>
<Icon className={`h1 w1 v-top mr2 ${iconColor[type]}`} />
{children}
</div>
);
};
62 changes: 62 additions & 0 deletions frontend/src/components/alert/tests/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { Alert } from '../index';

describe('Alert Component', () => {
it('with error type', () => {
const { container } = render(<Alert type="error">An error message</Alert>);
expect(container.querySelector('svg')).toBeInTheDocument(); // ban icon
expect(container.querySelector('.dark-red')).toBeInTheDocument();
expect(container.querySelector('div').className).toBe(
'db blue-dark bl bw2 br2 pa3 b--dark-red bg-washed-red',
);
expect(screen.queryByText(/An error message/)).toBeInTheDocument();
});

it('with success type', () => {
const { container } = render(
<Alert type="success" inline={true}>
Success message comes here
</Alert>,
);
expect(container.querySelector('svg')).toBeInTheDocument();
expect(container.querySelector('.dark-green')).toBeInTheDocument();
const divClassName = container.querySelector('div').className;
expect(divClassName).toContain('b--dark-green bg-washed-green');
expect(divClassName).toContain('di');
expect(divClassName).not.toContain('db');
expect(screen.queryByText(/Success message comes here/)).toBeInTheDocument();
});

it('with info type', () => {
const { container } = render(
<Alert type="info" compact={true}>
Information
</Alert>,
);
expect(container.querySelector('svg')).toBeInTheDocument();
expect(container.querySelector('.blue')).toBeInTheDocument();
const divClassName = container.querySelector('div').className;
expect(divClassName).toContain('b--blue bg-lightest-blue');
expect(divClassName).toContain('db');
expect(divClassName).not.toContain('di');
expect(divClassName).toContain('pa2');
expect(divClassName).not.toContain('pa3');
expect(screen.queryByText(/Information/)).toBeInTheDocument();
});

it('with warning type', () => {
const { container } = render(
<Alert type="warning" inline={true} compact={true}>
It's only a warning...
</Alert>,
);
expect(container.querySelector('svg')).toBeInTheDocument();
expect(container.querySelector('.gold')).toBeInTheDocument();
const divClassName = container.querySelector('div').className;
expect(divClassName).toContain('b--gold bg-washed-yellow');
expect(divClassName).toContain('di');
expect(divClassName).toContain('pa2');
expect(screen.queryByText(/It's only a warning.../)).toBeInTheDocument();
});
});
Loading

0 comments on commit ffd1373

Please sign in to comment.