Beispiel #1
0
def create_canned_project() -> Tuple[Project, User]:
    """ Generates a canned project in the DB to help with integration tests """
    test_aoi_geojson = geojson.loads(json.dumps(get_canned_json("test_aoi.json")))

    task_feature = geojson.loads(json.dumps(get_canned_json("splittable_task.json")))
    task_non_square_feature = geojson.loads(
        json.dumps(get_canned_json("non_square_task.json"))
    )

    test_user = create_canned_user()

    test_project_dto = DraftProjectDTO()
    test_project_dto.project_name = "Test"
    test_project_dto.user_id = test_user.id
    test_project_dto.area_of_interest = test_aoi_geojson
    test_project = Project()
    test_project.create_draft_project(test_project_dto)
    test_project.set_project_aoi(test_project_dto)
    test_project.total_tasks = 2

    # Setup test task
    test_task = Task.from_geojson_feature(1, task_feature)
    test_task.task_status = TaskStatus.MAPPED.value
    test_task.mapped_by = test_user.id
    test_task.is_square = True

    test_task2 = Task.from_geojson_feature(2, task_non_square_feature)
    test_task2.task_status = TaskStatus.READY.value
    test_task2.is_square = False

    test_project.tasks.append(test_task)
    test_project.tasks.append(test_task2)
    test_project.create()

    return test_project, test_user
 def test_split_geom_raise_grid_service_error_when_task_not_usable(self):
     if self.skip_tests:
         return
     with self.assertRaises(SplitServiceError):
         task_stub = Task()
         task_stub.is_square = True
         SplitService._create_split_tasks("foo", "bar", "dum", task_stub)
Beispiel #3
0
    def test_cant_add_task_if_feature_geometry_is_invalid(self):
        # Arrange
        invalid_feature = geojson.loads(
            '{"geometry": {"coordinates": [[[[-4.0237, 56.0904], [-3.9111, 56.1715],'
            '[-3.8122, 56.098], [-4.0237]]]], "type": "MultiPolygon"}, "properties":'
            '{"x": 2402, "y": 1736, "zoom": 12}, "type": "Feature"}')

        with self.assertRaises(InvalidGeoJson):
            Task.from_geojson_feature(1, invalid_feature)
    def test_cant_add_task_if_feature_has_missing_properties(self):
        # Missing zoom
        invalid_properties = geojson.loads(
            '{"geometry": {"coordinates": [[[[-4.0237, 56.0904], [-3.9111, 56.1715],'
            '[-3.8122, 56.098], [-4.0237, 56.0904]]]], "type": "MultiPolygon"},'
            '"properties": {"x": 2402, "y": 1736}, "type": "Feature"}')

        with self.assertRaises(InvalidData):
            Task.from_geojson_feature(1, invalid_properties)
Beispiel #5
0
    def test_cant_add_task_if_not_supplied_feature_type(self):
        # Arrange
        invalid_feature = geojson.MultiPolygon([[(2.38, 57.322),
                                                 (23.194, -20.28),
                                                 (-120.43, 19.15),
                                                 (2.38, 10.33)]])
        # Arrange

        with self.assertRaises(InvalidGeoJson):
            Task.from_geojson_feature(1, invalid_feature)
Beispiel #6
0
    def test_lock_task_for_mapping_adds_locked_history(self):
        # Arrange
        test_task = Task()

        # Act
        test_task.set_task_history(action=TaskAction.LOCKED_FOR_MAPPING,
                                   user_id=123454)

        # Assert
        self.assertEqual(TaskAction.LOCKED_FOR_MAPPING.name,
                         test_task.task_history[0].action)
Beispiel #7
0
    def test_reset_task_clears_any_existing_locks(self, mock_set_task_history,
                                                  mock_record_auto_unlock,
                                                  mock_update):
        user_id = 123

        test_task = Task()
        test_task.task_status = TaskStatus.LOCKED_FOR_MAPPING
        test_task.reset_task(user_id)

        mock_record_auto_unlock.assert_called()
        self.assertEqual(test_task.task_status, TaskStatus.READY.value)
Beispiel #8
0
    def test_reset_task_sets_to_ready_status(self, mock_set_task_history,
                                             mock_update):
        user_id = 123

        test_task = Task()
        test_task.task_status = TaskStatus.MAPPED
        test_task.reset_task(user_id)

        mock_set_task_history.assert_called_with(TaskAction.STATE_CHANGE,
                                                 user_id, None,
                                                 TaskStatus.READY)
        mock_update.assert_called()
        self.assertEqual(test_task.task_status, TaskStatus.READY.value)
Beispiel #9
0
def auto_unlock_tasks():
    with application.app_context():
        # Identify distinct project IDs that were touched in the last 2 hours
        query = (TaskHistory.query.with_entities(
            TaskHistory.project_id
        ).filter(
            func.DATE(TaskHistory.action_date) > datetime.datetime.utcnow() -
            datetime.timedelta(minutes=130)).distinct())
        projects = query.all()
        # For each project update task history for tasks that were not manually unlocked
        for project in projects:
            project_id = project[0]
            Task.auto_unlock_tasks(project_id)
Beispiel #10
0
    def generate_osm_xml(project_id: int, task_ids_str: str) -> str:
        """ Generate xml response suitable for loading into JOSM.  A sample output file is in
            /backend/helpers/testfiles/osm-sample.xml """
        # Note XML created with upload No to ensure it will be rejected by OSM if uploaded by mistake
        root = ET.Element(
            "osm",
            attrib=dict(version="0.6",
                        upload="never",
                        creator="HOT Tasking Manager"),
        )

        if task_ids_str:
            task_ids = map(int, task_ids_str.split(","))
            tasks = Task.get_tasks(project_id, task_ids)
            if not tasks or len(tasks) == 0:
                raise NotFound()
        else:
            tasks = Task.get_all_tasks(project_id)
            if not tasks or len(tasks) == 0:
                raise NotFound()

        fake_id = -1  # We use fake-ids to ensure XML will not be validated by OSM
        for task in tasks:
            task_geom = shape.to_shape(task.geometry)
            way = ET.SubElement(
                root,
                "way",
                attrib=dict(id=str((task.id * -1)),
                            action="modify",
                            visible="true"),
            )
            for poly in task_geom:
                for point in poly.exterior.coords:
                    ET.SubElement(
                        root,
                        "node",
                        attrib=dict(
                            action="modify",
                            visible="true",
                            id=str(fake_id),
                            lon=str(point[0]),
                            lat=str(point[1]),
                        ),
                    )
                    ET.SubElement(way, "nd", attrib=dict(ref=str(fake_id)))
                    fake_id -= 1

        xml_gpx = ET.tostring(root, encoding="utf8")
        return xml_gpx
    def test_split_geom_returns_split_geometries(self):

        # arrange
        x = 2021
        y = 2798
        zoom = 12
        task_stub = Task()
        task_stub.is_square = True

        expected = geojson.loads(json.dumps(get_canned_json("split_task.json")))

        # act
        result = SplitService._create_split_tasks(x, y, zoom, task_stub)

        # assert
        self.assertEqual(str(expected), str(result))
Beispiel #12
0
    def as_dto_for_mapping(
        self, authenticated_user_id: int = None, locale: str = "en", abbrev: bool = True
    ) -> Optional[ProjectDTO]:
        """ Creates a Project DTO suitable for transmitting to mapper users """
        # Check for project visibility settings
        is_allowed_user = True
        if self.status == ProjectStatus.DRAFT.value:
            if not self.check_draft_project_visibility(authenticated_user_id):
                is_allowed_user = False
        if self.private:
            is_allowed_user = False
            if authenticated_user_id:
                user = User.get_by_id(authenticated_user_id)

                if (
                    UserRole(user.role) == UserRole.ADMIN
                    or authenticated_user_id == self.author_id
                ):
                    is_allowed_user = True
                for user in self.allowed_users:
                    if user.id == authenticated_user_id:
                        is_allowed_user = True
                        break

        if is_allowed_user:
            project, project_dto = self._get_project_and_base_dto()
            if abbrev is False:
                project_dto.tasks = Task.get_tasks_as_geojson_feature_collection(
                    self.id, None
                )
            else:
                project_dto.tasks = Task.get_tasks_as_geojson_feature_collection_no_geom(
                    self.id
                )
            project_dto.project_info = ProjectInfo.get_dto_for_locale(
                self.id, locale, project.default_locale
            )
            if project.organisation_id:
                project_dto.organisation = project.organisation.id
                project_dto.organisation_name = project.organisation.name
                project_dto.organisation_logo = project.organisation.logo

            project_dto.project_info_locales = ProjectInfo.get_dto_for_all_locales(
                self.id
            )
            return project_dto
Beispiel #13
0
    def tasks_as_geojson(
        self, task_ids_str: str, order_by=None, order_by_type="ASC", status=None
    ):
        """ Creates a geojson of all areas """
        project_tasks = Task.get_tasks_as_geojson_feature_collection(
            self.id, task_ids_str, order_by, order_by_type, status
        )

        return project_tasks
    def lock_tasks_for_validation(
            validation_dto: LockForValidationDTO) -> TaskDTOs:
        """
        Lock supplied tasks for validation
        :raises ValidatorServiceError
        """
        # Loop supplied tasks to check they can all be locked for validation
        tasks_to_lock = []
        for task_id in validation_dto.task_ids:
            task = Task.get(task_id, validation_dto.project_id)

            if task is None:
                raise NotFound(f"Task {task_id} not found")

            if TaskStatus(task.task_status) not in [
                    TaskStatus.MAPPED,
                    TaskStatus.INVALIDATED,
                    TaskStatus.BADIMAGERY,
            ]:
                raise ValidatorServiceError(
                    f"Task {task_id} is not MAPPED, BADIMAGERY or INVALIDATED")
            user_can_validate = ValidatorService._user_can_validate_task(
                validation_dto.user_id, task.mapped_by)
            if not user_can_validate:
                raise ValidatorServiceError(
                    "Tasks cannot be validated by the same user who marked task as mapped or badimagery"
                )

            tasks_to_lock.append(task)

        user_can_validate, error_reason = ProjectService.is_user_permitted_to_validate(
            validation_dto.project_id, validation_dto.user_id)

        if not user_can_validate:
            if error_reason == ValidatingNotAllowed.USER_NOT_ACCEPTED_LICENSE:
                raise UserLicenseError(
                    "User must accept license to map this task")
            elif error_reason == ValidatingNotAllowed.USER_ALREADY_HAS_TASK_LOCKED:
                raise ValidatorServiceError("User already has a task locked")
            else:
                raise ValidatorServiceError(
                    f"Validation not allowed because: {error_reason}")

        # Lock all tasks for validation
        dtos = []
        for task in tasks_to_lock:
            task.lock_task_for_validating(validation_dto.user_id)
            dtos.append(
                task.as_dto_with_instructions(validation_dto.preferred_locale))

        task_dtos = TaskDTOs()
        task_dtos.tasks = dtos

        return task_dtos
Beispiel #15
0
    def get_task(task_id: int, project_id: int) -> Task:
        """
        Get task from DB
        :raises: NotFound
        """
        task = Task.get(task_id, project_id)

        if task is None:
            raise NotFound()

        return task
Beispiel #16
0
    def as_dto_for_mapping(self,
                           authenticated_user_id: int = None,
                           locale: str = "en",
                           abbrev: bool = True) -> Optional[ProjectDTO]:
        """ Creates a Project DTO suitable for transmitting to mapper users """
        project, project_dto = self._get_project_and_base_dto()
        if abbrev is False:
            project_dto.tasks = Task.get_tasks_as_geojson_feature_collection(
                self.id, None)
        else:
            project_dto.tasks = Task.get_tasks_as_geojson_feature_collection_no_geom(
                self.id)
        project_dto.project_info = ProjectInfo.get_dto_for_locale(
            self.id, locale, project.default_locale)
        if project.organisation_id:
            project_dto.organisation = project.organisation.id
            project_dto.organisation_name = project.organisation.name
            project_dto.organisation_logo = project.organisation.logo

        project_dto.project_info_locales = ProjectInfo.get_dto_for_all_locales(
            self.id)
        return project_dto
Beispiel #17
0
    def is_user_permitted_to_validate(project_id, user_id):
        """ Check if the user is allowed to validate on the project in scope """
        if UserService.is_user_blocked(user_id):
            return False, ValidatingNotAllowed.USER_NOT_ON_ALLOWED_LIST

        project = ProjectService.get_project_by_id(project_id)
        if project.license_id:
            if not UserService.has_user_accepted_license(user_id, project.license_id):
                return False, ValidatingNotAllowed.USER_NOT_ACCEPTED_LICENSE
        validation_permission = project.validation_permission

        # is_admin or is_author or is_org_manager or is_manager_team
        is_manager_permission = False
        if ProjectAdminService.is_user_action_permitted_on_project(user_id, project_id):
            is_manager_permission = True

        # Draft (public/private) accessible only for is_manager_permission
        if (
            ProjectStatus(project.status) == ProjectStatus.DRAFT
            and not is_manager_permission
        ):
            return False, ValidatingNotAllowed.PROJECT_NOT_PUBLISHED

        is_restriction = None
        if not is_manager_permission and validation_permission:
            is_restriction = ProjectService.evaluate_validation_permission(
                project_id, user_id, validation_permission
            )

        tasks = Task.get_locked_tasks_for_user(user_id)
        if len(tasks.locked_tasks) > 0:
            return False, ValidatingNotAllowed.USER_ALREADY_HAS_TASK_LOCKED

        is_allowed_user = None
        if project.private and not is_manager_permission:
            # Check if user is in allowed user list
            is_allowed_user = ProjectService.is_user_in_the_allowed_list(
                project.allowed_users, user_id
            )

            if is_allowed_user:
                return True, "User allowed to validate"

        if not is_manager_permission and is_restriction:
            return is_restriction
        elif project.private and not (
            is_manager_permission or is_allowed_user or not is_restriction
        ):
            return False, ValidatingNotAllowed.USER_NOT_ON_ALLOWED_LIST

        return True, "User allowed to validate"
Beispiel #18
0
    def test_per_task_instructions_returns_instructions_with_extra_properties(
            self):
        # Arrange
        test_task = Task()
        test_task.extra_properties = '{"foo": "bar"}'
        test_task.x = 1
        test_task.y = 2
        test_task.zoom = 3
        test_task.is_square = True

        # Act
        instructions = test_task.format_per_task_instructions(
            "Foo is replaced by {foo}")

        # Assert
        self.assertEqual(instructions, "Foo is replaced by bar")
Beispiel #19
0
    def is_user_permitted_to_validate(project_id, user_id):
        """ Check if the user is allowed to validate on the project in scope """
        if UserService.is_user_blocked(user_id):
            return False, ValidatingNotAllowed.USER_NOT_ON_ALLOWED_LIST

        project = ProjectService.get_project_by_id(project_id)
        if project.license_id:
            if not UserService.has_user_accepted_license(user_id, project.license_id):
                return False, ValidatingNotAllowed.USER_NOT_ACCEPTED_LICENSE

        validation_permission = project.validation_permission

        is_manager_permission = False
        # is_admin or is_author or is_org_manager or is_manager_team
        if ProjectAdminService.is_user_action_permitted_on_project(user_id, project_id):
            is_manager_permission = True

        if (
            ProjectStatus(project.status) != ProjectStatus.PUBLISHED
            and not is_manager_permission
        ):
            return False, ValidatingNotAllowed.PROJECT_NOT_PUBLISHED

        tasks = Task.get_locked_tasks_for_user(user_id)

        if len(tasks.locked_tasks) > 0:
            return False, ValidatingNotAllowed.USER_ALREADY_HAS_TASK_LOCKED

        if project.private and not is_manager_permission:
            # Check user is in allowed users
            try:
                next(user for user in project.allowed_users if user.id == user_id)
            except StopIteration:
                return False, ValidatingNotAllowed.USER_NOT_ON_ALLOWED_LIST

            is_restriction = ProjectService.evaluate_validation_permission(
                project_id, user_id, validation_permission
            )
            if is_restriction:
                return is_restriction

        if project.validation_permission and not is_manager_permission:
            is_restriction = ProjectService.evaluate_validation_permission(
                project_id, user_id, validation_permission
            )
            if is_restriction:
                return is_restriction

        return True, "User allowed to validate"
Beispiel #20
0
    def test_per_task_instructions_with_underscores_formatted_correctly(self):
        test_task = Task()
        test_task.x = 1
        test_task.y = 2
        test_task.zoom = 3
        test_task.is_square = True

        # Act
        instructions = test_task.format_per_task_instructions(
            "Test Url is http://test.com/{x}_{y}_{z}")

        # Assert
        self.assertEqual(instructions, "Test Url is http://test.com/1_2_3")
Beispiel #21
0
    def add_task_comment(task_comment: TaskCommentDTO) -> TaskDTO:
        """ Adds the comment to the task history """
        task = Task.get(task_comment.task_id, task_comment.project_id)
        if task is None:
            raise MappingServiceError(f"Task {task_comment.task_id} not found")

        task.set_task_history(TaskAction.COMMENT, task_comment.user_id,
                              task_comment.comment)
        # Parse comment to see if any users have been @'d
        MessageService.send_message_after_comment(task_comment.user_id,
                                                  task_comment.comment,
                                                  task.id,
                                                  task_comment.project_id)
        task.update()
        return task.as_dto_with_instructions(task_comment.preferred_locale)
Beispiel #22
0
    def delete_tasks(project_id: int, tasks_ids):
        # Validate project exists.
        project = Project.get(project_id)
        if project is None:
            raise NotFound({"project": project_id})

        tasks = [{"id": i, "obj": Task.get(i, project_id)} for i in tasks_ids]

        # In case a task is not found.
        not_found = [t["id"] for t in tasks if t["obj"] is None]
        if len(not_found) > 0:
            raise NotFound({"tasks": not_found})

        # Delete task one by one.
        [t["obj"].delete() for t in tasks]
Beispiel #23
0
    def test_record_auto_unlock_adds_autounlocked_action(
        self,
        mock_update,
        mock_set_task_history,
        mock_get_last_action,
        mock_get_last_status,
    ):
        mock_history = MagicMock()
        mock_last_action = MagicMock()
        mock_last_action.action = "LOCKED_FOR_MAPPING"
        mock_get_last_action.return_value = mock_last_action
        mock_get_last_status.return_value = TaskStatus.READY
        mock_set_task_history.return_value = mock_history

        test_task = Task()
        test_task.locked_by = "testuser"
        lock_duration = "02:00"
        test_task.record_auto_unlock(lock_duration)

        mock_set_task_history.assert_called_with(
            action=TaskAction.AUTO_UNLOCKED_FOR_MAPPING, user_id="testuser")
        self.assertEqual(mock_history.action_text, lock_duration)
        self.assertEqual(test_task.locked_by, None)
        mock_last_action.delete.assert_called()
Beispiel #24
0
    def get_task_details_for_logged_in_user(user_id: int, preferred_locale: str):
        """ if the user is working on a task in the project return it """
        tasks = Task.get_locked_tasks_details_for_user(user_id)

        if len(tasks) == 0:
            raise NotFound()

        # TODO put the task details in to a DTO
        dtos = []
        for task in tasks:
            dtos.append(task.as_dto_with_instructions(preferred_locale))

        task_dtos = TaskDTOs()
        task_dtos.tasks = dtos

        return task_dtos
Beispiel #25
0
    def test_per_task_instructions_returns_instructions_when_no_dynamic_url_and_task_not_splittable(
        self, ):
        # Arrange
        test_task = Task()
        test_task.x = 1
        test_task.y = 2
        test_task.zoom = 3
        test_task.is_square = False

        # Act
        instructions = test_task.format_per_task_instructions("Use map box")

        # Assert
        self.assertEqual(instructions, "Use map box")
Beispiel #26
0
    def test_per_task_instructions_formatted_correctly(self):
        # Arrange
        test_task = Task()
        test_task.x = 1
        test_task.y = 2
        test_task.zoom = 3
        test_task.is_square = True

        # Act
        instructions = test_task.format_per_task_instructions(
            "Test Url is http://test.com/{x}/{y}/{z}")

        # Assert
        self.assertEqual(instructions, "Test Url is http://test.com/1/2/3")
    def get_tasks_locked_by_user(project_id: int, unlock_tasks, user_id: int):
        """
        Returns tasks specified by project id and unlock_tasks list if found and locked for validation by user,
        otherwise raises ValidatorServiceError, NotFound
        :param project_id:
        :param unlock_tasks: List of tasks to be unlocked
        :param user_id:
        :return: List of Tasks
        :raises ValidatorServiceError
        :raises NotFound
        """
        tasks_to_unlock = []
        # Loop supplied tasks to check they can all be unlocked
        for unlock_task in unlock_tasks:
            task = Task.get(unlock_task.task_id, project_id)

            if task is None:
                raise NotFound(f"Task {unlock_task.task_id} not found")

            current_state = TaskStatus(task.task_status)
            if current_state != TaskStatus.LOCKED_FOR_VALIDATION:
                raise ValidatorServiceError(
                    f"Task {unlock_task.task_id} is not LOCKED_FOR_VALIDATION"
                )

            if task.locked_by != user_id:
                raise ValidatorServiceError(
                    "Attempting to unlock a task owned by another user"
                )

            if hasattr(unlock_task, "status"):
                # we know what status we ate going to be setting to on unlock
                new_status = TaskStatus[unlock_task.status]
            else:
                new_status = None

            tasks_to_unlock.append(
                dict(
                    task=task,
                    new_state=new_status,
                    comment=unlock_task.comment,
                    issues=unlock_task.issues,
                )
            )

        return tasks_to_unlock
    def test_split_non_square_task(self, mock_task):

        # Lock task for mapping
        task = Task.get(2, self.test_project.id)
        task.lock_task_for_mapping(self.test_user.id)

        split_task_dto = SplitTaskDTO()
        split_task_dto.user_id = self.test_user.id
        split_task_dto.project_id = self.test_project.id
        split_task_dto.task_id = 2

        # Split tasks
        expected = geojson.loads(
            json.dumps(get_canned_json("non_square_split_results.json"))
        )
        result = SplitService._create_split_tasks(task.x, task.y, task.zoom, task)

        self.assertEqual(str(expected), str(result))
    def is_user_permitted_to_map(project_id: int, user_id: int):
        """ Check if the user is allowed to map the on the project in scope """
        if UserService.is_user_blocked(user_id):
            return False, MappingNotAllowed.USER_NOT_ON_ALLOWED_LIST

        project = ProjectService.get_project_by_id(project_id)
        mapping_permission = project.mapping_permission

        if ProjectStatus(
                project.status
        ) != ProjectStatus.PUBLISHED and not ProjectAdminService.is_user_action_permitted_on_project(
                user_id, project_id):
            return False, MappingNotAllowed.PROJECT_NOT_PUBLISHED
        tasks = Task.get_locked_tasks_for_user(user_id)
        if len(tasks.locked_tasks) > 0:
            return False, MappingNotAllowed.USER_ALREADY_HAS_TASK_LOCKED
        if project.private:
            # Check user is in allowed users
            try:
                next(user for user in project.allowed_users
                     if user.id == user_id)
            except StopIteration:
                return False, MappingNotAllowed.USER_NOT_ON_ALLOWED_LIST
            is_restriction = ProjectService.evaluate_mapping_permission(
                project_id, user_id, mapping_permission)
            if is_restriction:
                return is_restriction

        if project.mapping_permission:
            is_restriction = ProjectService.evaluate_mapping_permission(
                project_id, user_id, mapping_permission)
            if is_restriction:
                return is_restriction

        if project.license_id:
            if not UserService.has_user_accepted_license(
                    user_id, project.license_id):
                return False, MappingNotAllowed.USER_NOT_ACCEPTED_LICENSE

        return True, "User allowed to map"
Beispiel #30
0
    def generate_gpx(project_id: int, task_ids_str: str, timestamp=None):
        """
        Creates a GPX file for supplied tasks.  Timestamp is for unit testing only.
        You can use the following URL to test locally:
        http://www.openstreetmap.org/edit?editor=id&#map=11/31.50362930069913/34.628906243797054&comment=CHANGSET_COMMENT&gpx=http://localhost:5000/api/v2/projects/{project_id}/tasks/queries/gpx%3Ftasks=2
        """

        if timestamp is None:
            timestamp = datetime.datetime.utcnow()

        root = ET.Element(
            "gpx",
            attrib=dict(
                xmlns="http://www.topografix.com/GPX/1/1",
                version="1.1",
                creator="HOT Tasking Manager",
            ),
        )

        # Create GPX Metadata element
        metadata = ET.Element("metadata")
        link = ET.SubElement(
            metadata,
            "link",
            attrib=dict(href="https://github.com/hotosm/tasking-manager"),
        )
        ET.SubElement(link, "text").text = "HOT Tasking Manager"
        ET.SubElement(metadata, "time").text = timestamp.isoformat()
        root.append(metadata)

        # Create trk element
        trk = ET.Element("trk")
        root.append(trk)
        ET.SubElement(
            trk, "name"
        ).text = f"Task for project {project_id}. Do not edit outside of this area!"

        # Construct trkseg elements
        if task_ids_str is not None:
            task_ids = map(int, task_ids_str.split(","))
            tasks = Task.get_tasks(project_id, task_ids)
            if not tasks or len(tasks) == 0:
                raise NotFound()
        else:
            tasks = Task.get_all_tasks(project_id)
            if not tasks or len(tasks) == 0:
                raise NotFound()

        for task in tasks:
            task_geom = shape.to_shape(task.geometry)
            for poly in task_geom:
                trkseg = ET.SubElement(trk, "trkseg")
                for point in poly.exterior.coords:
                    ET.SubElement(
                        trkseg,
                        "trkpt",
                        attrib=dict(lon=str(point[0]), lat=str(point[1])),
                    )

                    # Append wpt elements to end of doc
                    wpt = ET.Element("wpt",
                                     attrib=dict(lon=str(point[0]),
                                                 lat=str(point[1])))
                    root.append(wpt)

        xml_gpx = ET.tostring(root, encoding="utf8")
        return xml_gpx