Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for extra_properties in per task instructions #659

Merged
merged 4 commits into from
Aug 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion client/app/admin/edit-project/edit-project.html
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,14 @@ <h1 class="section__aside-title">{{ 'In this area' | translate }}</h1>
ng-model="info.perTaskInstructions" rows="4">
</textarea>
<p><strong>{{ 'Tip' | translate }}:</strong> {{ 'You can use Markdown. (HTML is not allowed)' | translate }}</p>
<p>Put here anything that can be useful to users while taking a task. {x}, {y} and {z} will be replaced by the corresponding parameters for each task.
<p ng-if="editProjectCtrl.project.taskCreationMode == 'GRID' ">
Put here anything that can be useful to users while taking a task. {x}, {y} and {z} will be replaced by the corresponding parameters for each task.
{x}, {y} and {z} parameters can only be be used on tasks generated in the Tasking Manager and not on imported tasks.
For example: « This task involves loading extra data. Click [here](http://localhost:8111/import?new_layer=true&url=http://www.domain.com/data/{x}/{y}/{z}/routes_2009.osm) to load the data into JOSM ».</p>
<p ng-if="editProjectCtrl.project.taskCreationMode == 'ARBITRARY'">
Put here anything that can be useful to users while taking a task. If you have added extra properties within the GeoJSON of the task, they can be referenced by surrounding them in curly braces. For eg. if you have a property called "import_url" in your GeoJSON, you can reference it like:
<code>This task involves loading extra data. Click [here](http://localhost:8111/import?new_layer=true&url={import_url}) to load the data into JOSM</code>
</p>
<button class="button button--secondary button--small"
ng-click="editProjectCtrl.showPreviewPerTaskInstructions = !editProjectCtrl.showPreviewPerTaskInstructions">
{{ 'Preview' | translate }}
Expand Down
11 changes: 11 additions & 0 deletions devops/tm2-pg-migration/migrationscripts.sql
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@ INSERT INTO hotnew.projects(

select setval('hotnew.projects_id_seq',(select max(id) from hotnew.projects));

-- Set the task_creation_mode to 'arbitrary' when project's zoom was None in
-- TM2 or 'grid' when it was not None
Update hotnew.projects
set task_creation_mode = 1
from hotold.projects as p
where p.id = hotnew.projects.id and p.zoom is NULL;

Update hotnew.projects
set task_creation_mode = 0
from hotold.projects as p
where p.id = hotnew.projects.id and p.zoom is not NULL;

-- Project info & translations
-- Skip any records relating to projects that have not been imported
Expand Down
43 changes: 43 additions & 0 deletions migrations/versions/deec8123583d_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""empty message

Revision ID: deec8123583d
Revises: ac55902fcc3d
Create Date: 2018-08-07 23:09:58.621826

"""
from alembic import op
import sqlalchemy as sa
from server.models.postgis.project import Project
from server.models.postgis.task import Task


# revision identifiers, used by Alembic.
revision = 'deec8123583d'
down_revision = 'ac55902fcc3d'
branch_labels = None
depends_on = None

projects = Project.__table__
tasks = Task.__table__

def upgrade():
conn = op.get_bind()

for project in conn.execute(projects.select()):
zooms = conn.execute(
sa.sql.expression.select([tasks.c.zoom]).distinct(tasks.c.zoom)
.where(tasks.c.project_id == project.id))
zooms = zooms.fetchall()

if len(zooms) == 1 and zooms[0] == (None,):
op.execute(
projects.update().where(projects.c.id == project.id)
.values(task_creation_mode=1))
else:
op.execute(
projects.update().where(projects.c.id == project.id)
.values(task_creation_mode=0))


def downgrade():
pass
13 changes: 12 additions & 1 deletion server/models/dtos/project_dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from schematics.types.compound import ListType, ModelType
from server.models.dtos.user_dto import is_known_mapping_level
from server.models.dtos.stats_dto import Pagination
from server.models.postgis.statuses import ProjectStatus, ProjectPriority, MappingTypes
from server.models.postgis.statuses import ProjectStatus, ProjectPriority, MappingTypes, TaskCreationMode


def is_known_project_status(value):
Expand Down Expand Up @@ -42,6 +42,15 @@ def is_known_mapping_type(value):
f'{MappingTypes.LAND_USE.name}, {MappingTypes.OTHER.name}')


def is_known_task_creation_mode(value):
""" Validates Task Creation Mode is known value """
try:
TaskCreationMode[value.upper()]
except KeyError:
raise ValidationError(f'Unknown taskCreationMode: {value} Valid values are {TaskCreationMode.GRID.name}, '
f'{TaskCreationMode.ARBITRARY.name}')


class DraftProjectDTO(Model):
""" Describes JSON model used for creating draft project """
cloneFromProjectId = IntType(serialized_name='cloneFromProjectId')
Expand Down Expand Up @@ -93,6 +102,8 @@ class ProjectDTO(Model):
last_updated = DateTimeType(serialized_name='lastUpdated')
author = StringType()
active_mappers = IntType(serialized_name='activeMappers')
task_creation_mode = StringType(required=True, serialized_name='taskCreationMode',
validators=[is_known_task_creation_mode], serialize_when_none=False)


class ProjectSearchDTO(Model):
Expand Down
4 changes: 3 additions & 1 deletion server/models/postgis/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from server.models.dtos.project_dto import ProjectDTO, DraftProjectDTO, ProjectSummary, PMDashboardDTO
from server.models.postgis.priority_area import PriorityArea, project_priority_areas
from server.models.postgis.project_info import ProjectInfo
from server.models.postgis.statuses import ProjectStatus, ProjectPriority, MappingLevel, TaskStatus, MappingTypes
from server.models.postgis.statuses import ProjectStatus, ProjectPriority, MappingLevel, TaskStatus, MappingTypes, TaskCreationMode
from server.models.postgis.tags import Tags
from server.models.postgis.task import Task
from server.models.postgis.user import User
Expand Down Expand Up @@ -56,6 +56,7 @@ class Project(db.Model):
license_id = db.Column(db.Integer, db.ForeignKey('licenses.id', name='fk_licenses'))
geometry = db.Column(Geometry('MULTIPOLYGON', srid=4326))
centroid = db.Column(Geometry('POINT', srid=4326))
task_creation_mode = db.Column(db.Integer, default=TaskCreationMode.GRID.value, nullable=False)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: this column already exists in the database (added through the migrations), but it wasn't mapped into the DB models.


# Tags
mapping_types = db.Column(ARRAY(db.Integer), index=True)
Expand Down Expand Up @@ -350,6 +351,7 @@ def _get_project_and_base_dto(self):
base_dto.last_updated = self.last_updated
base_dto.author = User().get_by_id(self.author_id).username
base_dto.active_mappers = Project.get_active_mappers(self.id)
base_dto.task_creation_mode = TaskCreationMode(self.task_creation_mode).name

if self.private:
# If project is private it should have a list of allowed users
Expand Down
6 changes: 6 additions & 0 deletions server/models/postgis/statuses.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ class ProjectPriority(Enum):
LOW = 3


class TaskCreationMode(Enum):
""" Enum to describe task creation mode """
GRID = 0
ARBITRARY = 1


class TaskStatus(Enum):
""" Enum describing available Task Statuses """
READY = 0
Expand Down
32 changes: 20 additions & 12 deletions server/models/postgis/task.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import bleach
import datetime
import geojson
import json
from enum import Enum
from geoalchemy2 import Geometry
from server import db
Expand Down Expand Up @@ -142,6 +143,7 @@ class Task(db.Model):
x = db.Column(db.Integer)
y = db.Column(db.Integer)
zoom = db.Column(db.Integer)
extra_properties = db.Column(db.Unicode)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: this column already exists in the database (added through the migrations), but it wasn't mapped into the DB models.

# Tasks are not splittable if created from an arbitrary grid or were clipped to the edge of the AOI
splittable = db.Column(db.Boolean, default=True)
geometry = db.Column(Geometry('MULTIPOLYGON', srid=4326))
Expand Down Expand Up @@ -198,6 +200,10 @@ def from_geojson_feature(cls, task_id, task_feature):
except KeyError as e:
raise InvalidData(f'Task: Expected property not found: {str(e)}')

if 'extra_properties' in task_feature.properties:
task.extra_properties = json.dumps(
task_feature.properties['extra_properties'])

task.id = task_id
task_geojson = geojson.dumps(task_geometry)
task.geometry = ST_SetSRID(ST_GeomFromGeoJSON(task_geojson), 4326)
Expand Down Expand Up @@ -446,18 +452,20 @@ def format_per_task_instructions(self, instructions) -> str:
if not instructions:
return '' # No instructions so return empty string

# If there's no dynamic URL (e.g. url containing '{x}, {y} and {z}' pattern)
# - ALWAYS return instructions unaltered

if not all(item in instructions for item in ['{x}','{y}','{z}']):
return instructions

# If there is a dyamic URL only return instructions if task is splittable, since we have the X, Y, Z
if not self.splittable:
return 'No extra instructions available for this task'
properties = {}

instructions = instructions.replace('{x}', str(self.x))
instructions = instructions.replace('{y}', str(self.y))
instructions = instructions.replace('{z}', str(self.zoom))
if self.x:
properties['x'] = str(self.x)
if self.y:
properties['y'] = str(self.y)
if self.zoom:
properties['z'] = str(self.zoom)
if self.extra_properties:
properties.update(json.loads(self.extra_properties))

try:
instructions = instructions.format(**properties)
except KeyError:
pass
return instructions

4 changes: 3 additions & 1 deletion server/services/grid/grid_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,13 @@ def tasks_from_aoi_features(feature_collection: str) -> geojson.FeatureCollectio
feature.geometry = shapely.geometry.mapping(feature.geometry)

# set default properties
# and put any already existing properties in `extra_properties`
feature.properties = {
'x': None,
'y': None,
'zoom': None,
'splittable': False
'splittable': False,
'extra_properties': feature.properties
}

tasks.append(feature)
Expand Down
2 changes: 2 additions & 0 deletions server/services/project_admin_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from server.models.dtos.project_dto import DraftProjectDTO, ProjectDTO, ProjectCommentsDTO
from server.models.postgis.project import Project, Task, ProjectStatus
from server.models.postgis.statuses import TaskCreationMode
from server.models.postgis.task import TaskHistory
from server.models.postgis.utils import NotFound, InvalidData, InvalidGeoJson
from server.services.grid.grid_service import GridService
Expand Down Expand Up @@ -49,6 +50,7 @@ def create_draft_project(draft_project_dto: DraftProjectDTO) -> int:
# if arbitrary_tasks requested, create tasks from aoi otherwise use tasks in DTO
if draft_project_dto.has_arbitrary_tasks:
tasks = GridService.tasks_from_aoi_features(draft_project_dto.area_of_interest)
draft_project.task_creation_mode = TaskCreationMode.ARBITRARY.value
else:
tasks = draft_project_dto.tasks
ProjectAdminService._attach_tasks_to_project(draft_project, tasks)
Expand Down
87 changes: 5 additions & 82 deletions tests/server/helpers/test_files/tasks_from_aoi_features.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,6 @@
1275016.6455451597,
2828781.542271093
],
[
1275087.6232993654,
2829087.2903841813
],
[
1275275.3796814454,
2829087.2903841813
],
[
1275275.3796814454,
2828783.0982895694
Expand All @@ -38,82 +30,13 @@
"splittable": false,
"x": null,
"y": null,
"zoom": null
},
"type": "Feature"
},
{
"geometry": {
"coordinates": [
[
[
[
1275581.1277945302,
2829306.621247852
],
[
1275827.894530152,
2829303.193932765
],
[
1275872.7042755112,
2829087.2903841813
],
[
1275581.1277945302,
2829087.2903841813
],
[
1275581.1277945302,
2829306.621247852
]
]
]
],
"type": "MultiPolygon"
},
"properties": {
"splittable": false,
"x": null,
"y": null,
"zoom": null
},
"type": "Feature"
},
{
"geometry": {
"coordinates": [
[
[
[
1275886.8759076186,
2829019.008869979
],
[
1275932.9954440442,
2828796.798620377
],
[
1275886.8759076186,
2828795.8377961316
],
[
1275886.8759076186,
2829019.008869979
]
]
]
],
"type": "MultiPolygon"
},
"properties": {
"splittable": false,
"x": null,
"y": null,
"zoom": null
"zoom": null,
"extra_properties": {
"foo": "bar"
}
},
"type": "Feature"
}
],
"type": "FeatureCollection"
}
}
36 changes: 36 additions & 0 deletions tests/server/helpers/test_files/test_arbitrary.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"areaOfInterest": {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
1275275.3796814454,
2828783.0982895694
],
[
1275200.6907704484,
2828781.542271093
],
[
1275016.6455451597,
2828781.542271093
],
[
1275275.3796814454,
2828783.0982895694
]
]
]
},
"properties": {
"foo": "bar"
}
}
]
}
}
Loading