コード例 #1
0
    def test_update_after_flagging_bad_imagery(self):
        # Arrange
        test_project = Project()
        test_project.tasks_bad_imagery = 0

        # Act
        StatsService._set_counters_after_bad_imagery(test_project)

        # Assert
        self.assertEqual(test_project.tasks_bad_imagery, 1)
コード例 #2
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')))

    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 = 1

    # 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_project.tasks.append(test_task)
    test_project.create()

    return test_project, test_user
コード例 #3
0
    def search_projects(
            search_dto: ProjectSearchDTO) -> ProjectSearchResultsDTO:
        """ Searches all projects for matches to the criteria provided by the user """

        all_results, paginated_results = ProjectSearchService._filter_projects(
            search_dto)

        if paginated_results.total == 0:
            raise NotFound()

        features = []
        for project in all_results:
            # This loop creates a geojson feature collection so you can see all active projects on the map
            properties = {
                "projectId": project.id,
                "priority": ProjectPriority(project.priority).name
            }
            centroid = project.centroid
            feature = geojson.Feature(geometry=geojson.loads(project.centroid),
                                      properties=properties)
            features.append(feature)
        feature_collection = geojson.FeatureCollection(features)
        dto = ProjectSearchResultsDTO()
        dto.map_results = feature_collection

        for project in paginated_results.items:
            # This loop loads the paginated text results
            # TODO would be nice to get this for an array rather than individually would be more efficient
            project_info_dto = ProjectInfo.get_dto_for_locale(
                project.id, search_dto.preferred_locale,
                project.default_locale)

            list_dto = ListSearchResultDTO()
            list_dto.project_id = project.id
            list_dto.locale = project_info_dto.locale
            list_dto.name = project_info_dto.name
            list_dto.priority = ProjectPriority(project.priority).name
            list_dto.mapper_level = MappingLevel(project.mapper_level).name
            list_dto.short_description = project_info_dto.short_description
            list_dto.organisation_tag = project.organisation_tag
            list_dto.campaign_tag = project.campaign_tag
            list_dto.percent_mapped = Project.calculate_tasks_percent(
                'mapped', project.total_tasks, project.tasks_mapped,
                project.tasks_validated, project.tasks_bad_imagery)
            list_dto.percent_validated = Project.calculate_tasks_percent(
                'validated', project.total_tasks, project.tasks_mapped,
                project.tasks_validated, project.tasks_bad_imagery)
            list_dto.status = ProjectStatus(project.status).name
            list_dto.active_mappers = Project.get_active_mappers(project.id)

            dto.results.append(list_dto)

        dto.pagination = Pagination(paginated_results)
        return dto
コード例 #4
0
    def test_update_after_mapping_increments_counter(self):
        # Arrange
        test_project = Project()
        test_project.tasks_mapped = 0

        test_user = User()
        test_user.tasks_mapped = 0

        # Act
        StatsService._set_counters_after_mapping(test_project, test_user)

        # Assert
        self.assertEqual(test_project.tasks_mapped, 1)
        self.assertEqual(test_user.tasks_mapped, 1)
コード例 #5
0
    def test_update_after_mapping_increments_counter(self):
        # Arrange
        test_project = Project()
        test_project.tasks_mapped = 0

        test_user = User()
        test_user.tasks_mapped = 0

        # Act
        StatsService._update_tasks_stats(test_project, test_user,
                                         TaskStatus.READY, TaskStatus.MAPPED)

        # Assert
        self.assertEqual(test_project.tasks_mapped, 1)
        self.assertEqual(test_user.tasks_mapped, 1)
コード例 #6
0
    def test_update_after_flagging_bad_imagery(self):
        # Arrange
        test_project = Project()
        test_project.tasks_bad_imagery = 0

        test_user = User()
        test_user.tasks_invalidated = 0

        # Act
        StatsService._update_tasks_stats(test_project, test_user,
                                         TaskStatus.READY,
                                         TaskStatus.BADIMAGERY)

        # Assert
        self.assertEqual(test_project.tasks_bad_imagery, 1)
コード例 #7
0
    def _attach_tasks_to_project(draft_project: Project, tasks_geojson):
        """
        Validates then iterates over the array of tasks and attach them to the draft project
        :param draft_project: Draft project in scope
        :param tasks_geojson: GeoJSON feature collection of mapping tasks
        :raises InvalidGeoJson, InvalidData
        """
        tasks = geojson.loads(json.dumps(tasks_geojson))

        if type(tasks) is not geojson.FeatureCollection:
            raise InvalidGeoJson(
                'Tasks: Invalid GeoJson must be FeatureCollection')

        is_valid_geojson = geojson.is_valid(tasks)
        if is_valid_geojson['valid'] == 'no':
            raise InvalidGeoJson(
                f"Tasks: Invalid FeatureCollection - {is_valid_geojson['message']}"
            )

        task_count = 1
        for feature in tasks['features']:
            try:
                task = Task.from_geojson_feature(task_count, feature)
            except (InvalidData, InvalidGeoJson) as e:
                raise e

            draft_project.tasks.append(task)
            task_count += 1

        task_count -= 1  # Remove last increment before falling out loop
        draft_project.total_tasks = task_count
コード例 #8
0
    def test_update_after_invalidating_bad_imagery_task_sets_counters_correctly(self, last_status):
        # Arrange
        test_project = Project()
        test_project.tasks_bad_imagery = 1

        test_user = User()
        test_user.tasks_invalidated = 0

        last_status.return_value = TaskStatus.BADIMAGERY

        # Act
        StatsService._set_counters_after_invalidated(1, test_project, test_user)

        # Assert
        self.assertEqual(test_project.tasks_bad_imagery, 0)
        self.assertEqual(test_user.tasks_invalidated, 1)
コード例 #9
0
    def test_update_after_invalidating_mapped_task_sets_counters_correctly(self, last_status):
        # Arrange
        test_project = Project()
        test_project.tasks_mapped = 1

        test_user = User()
        test_user.tasks_invalidated = 0

        last_status.return_value = TaskStatus.MAPPED

        # Act
        StatsService._set_counters_after_invalidated(1, test_project, test_user)

        # Assert
        self.assertEqual(test_project.tasks_mapped, 0)
        self.assertEqual(test_user.tasks_invalidated, 1)
コード例 #10
0
    def get_project_by_id(project_id: int) -> Project:
        project = Project.get(project_id)

        if project is None:
            raise NotFound()

        return project
コード例 #11
0
    def test_split_task_helper(self, mock_task_get, mock_task_get_max_task_id_for_project,
                               mock_task_create, mock_task_delete, mock_project_get, mock_project_save,
                               mock_project_tasks, mock_instructions):
        if self.skip_tests:
            return

        # arrange
        task_stub = Task()
        task_stub.id = 1
        task_stub.project_id = 1
        task_stub.task_status = 1
        task_stub.locked_by = 1234
        task_stub.lock_holder = 1234
        task_stub.splittable = True
        task_stub.x = 1010
        task_stub.y = 1399
        task_stub.zoom = 11
        mock_task_get.return_value = task_stub
        mock_task_get_max_task_id_for_project.return_value = 1
        mock_project_get.return_value = Project()
        mock_project_tasks.return_value = [task_stub]
        splitTaskDTO = SplitTaskDTO()
        splitTaskDTO.user_id = 1234
        splitTaskDTO.project_id = 1
        splitTaskDTO.task_id = 1

        # act
        result = SplitService.split_task(splitTaskDTO)

        # assert
        self.assertEqual(4, len(result.tasks))
コード例 #12
0
    def test_update_after_invalidating_validated_task_sets_counters_correctly(
            self):
        # Arrange
        test_project = Project()
        test_project.tasks_validated = 1

        test_user = User()
        test_user.tasks_invalidated = 0

        # Act
        StatsService._update_tasks_stats(test_project, test_user,
                                         TaskStatus.VALIDATED,
                                         TaskStatus.INVALIDATED)

        # Assert
        self.assertEqual(test_project.tasks_validated, 0)
        self.assertEqual(test_user.tasks_invalidated, 1)
コード例 #13
0
    def test_user_correctly_identified_as_pm(self, mock_user):
        # Arrange
        test_proj = Project()
        test_user = User()
        test_user.role = UserRole.PROJECT_MANAGER.value

        mock_user.return_value = test_user

        # Act / Assert
        self.assertTrue(UserService.is_user_a_project_manager(123))
        self.assertTrue(test_proj)
コード例 #14
0
    def create_draft_project(draft_project_dto: DraftProjectDTO) -> int:
        """
        Validates and then persists draft projects in the DB
        :param draft_project_dto: Draft Project DTO with data from API
        :raises InvalidGeoJson
        :returns ID of new draft project
        """
        try:
            area_of_interest = AreaOfInterest(
                draft_project_dto.area_of_interest)
        except InvalidGeoJson as e:
            raise e

        draft_project = Project()
        draft_project.create_draft_project(draft_project_dto, area_of_interest)

        # 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)
        else:
            tasks = draft_project_dto.tasks
        ProjectAdminService._attach_tasks_to_project(draft_project, tasks)

        draft_project.create()
        return draft_project.id
コード例 #15
0
    def test_get_intersecting_projects(self, get_dto_for_locale,
                                       _get_intersecting_projects,
                                       get_user_by_username,
                                       validate_bbox_area,
                                       _make_4326_polygon_from_bbox):
        if self.skip_tests:
            return

        # arrange _make_4326_polygon_from_bbox mock
        _make_4326_polygon_from_bbox.return_value = Polygon([
            (34.68826225820438, -12.59912449955007),
            (34.68826225820438, -11.57858317689196),
            (32.50198296132938, -11.57858317689196),
            (32.50198296132938, -12.59912449955007),
            (34.68826225820438, -12.59912449955007)
        ])

        # arrange validate_bbox_area mock
        validate_bbox_area.return_value = True

        # arrange get_user_by_username mock
        get_user_by_username.return_value = User(id=3488526)

        # arrange _get_intersecting_projects mock
        polygon = json.dumps(get_canned_json('search_bbox_feature.json'))
        project = Project(id=2274,
                          status=0,
                          default_locale='en',
                          geometry=polygon)
        projects = [project]
        _get_intersecting_projects.return_value = projects

        # arrange get_dto_for_locale mock
        get_dto_for_locale.return_value = ProjectInfo(
            name='PEPFAR Kenya: Homa Bay')

        # arrange dto
        dto = ProjectSearchBBoxDTO()
        dto.bbox = map(float, '34.404,-1.034, 34.717,-0.624'.split(','))
        dto.preferred_locale = 'en'
        dto.input_srid = 4326
        dto.project_author = 3488526
        dto.validate()

        # arrange expected result
        expected = json.dumps(get_canned_json('search_bbox_result.json'))

        # act
        result = ProjectSearchService.get_projects_geojson(dto)

        # assert
        self.assertEqual(str(expected), str(expected))
コード例 #16
0
    def test_project_can_be_cloned(self):

        if self.skip_tests:
            return

        # Arrange
        self.update_project_with_info()

        # Act
        original_id = copy.copy(self.test_project.id)
        cloned_project = Project.clone(original_id, self.test_user.id)

        self.assertTrue(cloned_project)
        self.assertEqual(cloned_project.project_info[0].name,
                         'Thinkwhere Test')

        # Tidy Up
        cloned_project.delete()
        original_project = Project.get(
            original_id
        )  # SQLAlchemy is hanging on to a ref to the old project
        original_project.delete()
コード例 #17
0
    def get_project_stats(project_id: int, preferred_locale: str) -> ProjectSummary:
        """ Gets stats for the specified project """
        project = db.session.query(Project.id,
                                   Project.status,
                                   Project.campaign_tag,
                                   Project.total_tasks,
                                   Project.tasks_mapped,
                                   Project.tasks_validated,
                                   Project.tasks_bad_imagery,
                                   Project.created,
                                   Project.last_updated,
                                   Project.default_locale,
                                   AreaOfInterest.centroid.ST_AsGeoJSON().label('geojson'))\
            .join(AreaOfInterest).filter(Project.id == project_id).one_or_none()

        pm_project = Project.get_project_summary(project, preferred_locale)
        return pm_project
コード例 #18
0
    def test_split_task_helper(self, mock_task_get,
                               mock_task_get_max_task_id_for_project,
                               mock_task_create, mock_task_delete,
                               mock_project_get, mock_project_save,
                               mock_project_tasks, mock_instructions):
        if self.skip_tests:
            return

        # arrange
        task_stub = Task()
        task_stub.id = 1
        task_stub.project_id = 1
        task_stub.task_status = 1
        task_stub.locked_by = 1234
        task_stub.lock_holder = 1234
        task_stub.is_square = True
        task_stub.x = 16856
        task_stub.y = 17050
        task_stub.zoom = 15
        task_stub.geometry = shape.from_shape(
            Polygon([(5.1855468740711421, 7.2970875628719796),
                     (5.1855468740711421, 7.3079847788619219),
                     (5.1965332021941588, 7.3079847788619219),
                     (5.1965332021941588, 7.2970875628719796),
                     (5.1855468740711421, 7.2970875628719796)]))
        mock_task_get.return_value = task_stub
        mock_task_get_max_task_id_for_project.return_value = 1
        mock_project_get.return_value = Project()
        mock_project_tasks.return_value = [task_stub]
        splitTaskDTO = SplitTaskDTO()
        splitTaskDTO.user_id = 1234
        splitTaskDTO.project_id = 1
        splitTaskDTO.task_id = 1

        # act
        result = SplitService.split_task(splitTaskDTO)

        # assert
        self.assertEqual(4, len(result.tasks))
コード例 #19
0
    def transfer_project_to(project_id: int, transfering_user_id: int, username: str):
        """ Transfers project from old owner (transfering_user_id) to new owner (username) """
        project = Project.get(project_id)

        transfering_user = UserService.get_user_by_id(transfering_user_id)
        new_owner = UserService.get_user_by_username(username)
        is_pm = new_owner.role in (UserRole.PROJECT_MANAGER.value, UserRole.ADMIN.value)

        if not is_pm:
            raise Exception("User must be a project manager")

        if transfering_user.role == UserRole.PROJECT_MANAGER.value:
            if project.author_id == transfering_user_id:
                project.author_id = new_owner.id
                project.save()
            else:
                raise Exception("Invalid owner_id")
        elif transfering_user.role == UserRole.ADMIN.value:
            project.author_id = new_owner.id
            project.save()
        else:
            raise Exception("Normal users cannot transfer projects")
コード例 #20
0
 def get_all_campaign_tags(preferred_locale):
     """ Get all campaign tags"""
     return Project.get_all_campaign_tag(preferred_locale=preferred_locale)
コード例 #21
0
    def split_task(split_task_dto: SplitTaskDTO) -> TaskDTOs:
        """
        Replaces a task square with 4 smaller tasks at the next OSM tile grid zoom level
        Validates that task is:
         - locked for mapping by current user
         - splittable (splittable property is True)
        :param split_task_dto:
        :return: new tasks in a DTO
        """
        # get the task to be split
        original_task = Task.get(split_task_dto.task_id,
                                 split_task_dto.project_id)
        if original_task is None:
            raise NotFound()

        # check it's splittable
        if not original_task.splittable:
            raise SplitServiceError('Task is not splittable')

        # check its locked for mapping by the current user
        if TaskStatus(
                original_task.task_status) != TaskStatus.LOCKED_FOR_MAPPING:
            raise SplitServiceError(
                'Status must be LOCKED_FOR_MAPPING to split')

        if original_task.locked_by != split_task_dto.user_id:
            raise SplitServiceError(
                'Attempting to split a task owned by another user')

        # create new geometries from the task geometry
        try:
            new_tasks_geojson = SplitService._create_split_tasks(
                original_task.x, original_task.y, original_task.zoom)
        except Exception as e:
            raise SplitServiceError(f'Error splitting task{str(e)}')

        # create new tasks from the new geojson
        i = Task.get_max_task_id_for_project(split_task_dto.project_id)
        new_tasks_dto = []
        for new_task_geojson in new_tasks_geojson:
            # insert new tasks into database
            i = i + 1
            new_task = Task.from_geojson_feature(i, new_task_geojson)
            new_task.project_id = split_task_dto.project_id
            new_task.task_status = TaskStatus.READY.value
            new_task.create()
            new_task.task_history.extend(original_task.copy_task_history())
            if new_task.task_history:
                new_task.clear_task_lock()  # since we just copied the lock
            new_task.set_task_history(TaskAction.STATE_CHANGE,
                                      split_task_dto.user_id, None,
                                      TaskStatus.SPLIT)
            new_task.set_task_history(TaskAction.STATE_CHANGE,
                                      split_task_dto.user_id, None,
                                      TaskStatus.READY)
            new_task.task_status = TaskStatus.READY.value
            new_task.update()
            new_tasks_dto.append(
                new_task.as_dto_with_instructions(
                    split_task_dto.preferred_locale))

        # delete original task from the database
        original_task.delete()

        # update project task counts
        project = Project.get(split_task_dto.project_id)
        project.total_tasks = project.tasks.count()
        # update bad imagery because we may have split a bad imagery tile
        project.tasks_bad_imagery = project.tasks.filter(
            Task.task_status == TaskStatus.BADIMAGERY.value).count()
        project.save()

        # return the new tasks in a DTO
        task_dtos = TaskDTOs()
        task_dtos.tasks = new_tasks_dto
        return task_dtos
コード例 #22
0
    def test_tasks_state_representation(self):

        # Arrange
        test_project = Project()
        test_project.tasks_mapped = 0
        test_project.tasks_validated = 0
        test_project.tasks_bad_imagery = 0

        test_mapper = User()
        test_mapper.tasks_mapped = 0
        test_mapper.tasks_validated = 0
        test_mapper.tasks_invalidated = 0

        test_validator = User()
        test_validator.tasks_mapped = 0
        test_validator.tasks_validated = 0
        test_validator.tasks_invalidated = 0

        test_admin = User()
        test_admin.tasks_mapped = 0
        test_admin.tasks_validated = 0
        test_admin.tasks_invalidated = 0

        # Mapper marks task as mapped
        StatsService._update_tasks_stats(test_project, test_mapper,
                                         TaskStatus.READY, TaskStatus.MAPPED)

        # Validator marks task as bad imagery
        StatsService._update_tasks_stats(test_project, test_validator,
                                         TaskStatus.MAPPED,
                                         TaskStatus.BADIMAGERY)

        # Admin undos marking task as bad imagery
        StatsService._update_tasks_stats(test_project, test_admin,
                                         TaskStatus.BADIMAGERY,
                                         TaskStatus.MAPPED, 'undo')

        # Validator marks task as invalid
        StatsService._update_tasks_stats(test_project, test_validator,
                                         TaskStatus.MAPPED,
                                         TaskStatus.INVALIDATED)

        # Mapper marks task as mapped
        StatsService._update_tasks_stats(test_project, test_mapper,
                                         TaskStatus.INVALIDATED,
                                         TaskStatus.MAPPED)

        # Admin undos marking task as mapped (test_mapper is given to the function though, as the author of the
        # last_change - compare with MappingServer.undo_mapping() method)
        StatsService._update_tasks_stats(test_project, test_mapper,
                                         TaskStatus.MAPPED,
                                         TaskStatus.INVALIDATED, 'undo')

        # Mapper marks task as mapped
        StatsService._update_tasks_stats(test_project, test_mapper,
                                         TaskStatus.INVALIDATED,
                                         TaskStatus.MAPPED)

        # Validator marks task as valid
        StatsService._update_tasks_stats(test_project, test_validator,
                                         TaskStatus.MAPPED,
                                         TaskStatus.VALIDATED)

        # Assert
        self.assertEqual(test_project.tasks_mapped, 0)
        self.assertEqual(test_project.tasks_validated, 1)
        self.assertEqual(test_project.tasks_bad_imagery, 0)
        self.assertEqual(test_mapper.tasks_mapped, 2)
        self.assertEqual(test_mapper.tasks_validated, 0)
        self.assertEqual(test_mapper.tasks_invalidated, 0)
        self.assertEqual(test_validator.tasks_mapped, 0)
        self.assertEqual(test_validator.tasks_validated, 1)
        self.assertEqual(test_validator.tasks_invalidated, 1)
        self.assertEqual(test_admin.tasks_mapped, 0)
        self.assertEqual(test_admin.tasks_validated, 0)
        self.assertEqual(test_admin.tasks_invalidated, 0)
コード例 #23
0
 def get_projects_for_admin(admin_id: int, preferred_locale: str):
     """ Get all projects for provided admin """
     return Project.get_projects_for_admin(admin_id, preferred_locale)
コード例 #24
0
 def get_all_organisation_tags(preferred_locale):
     """ Get all org tags"""
     return Project.get_all_organisations_tag(preferred_locale=preferred_locale)
コード例 #25
0
 def _set_default_changeset_comment(draft_project: Project):
     """ Sets the default changesset comment when project created """
     default_comment = current_app.config['DEFAULT_CHANGESET_COMMENT']
     draft_project.changeset_comment = f'{default_comment}-{draft_project.id}'
     draft_project.save()
コード例 #26
0
    def create_draft_project(draft_project_dto: DraftProjectDTO) -> int:
        """
        Validates and then persists draft projects in the DB
        :param draft_project_dto: Draft Project DTO with data from API
        :raises InvalidGeoJson
        :returns ID of new draft project
        """
        # First things first, we need to validate that the author_id is a PM. issue #1715
        if not UserService.is_user_a_project_manager(
                draft_project_dto.user_id):
            raise (ProjectAdminServiceError(
                f'User {UserService.get_user_by_id(draft_project_dto.user_id).username} is not a project manager'
            ))

        # If we're cloning we'll copy all the project details from the clone, otherwise create brand new project
        if draft_project_dto.cloneFromProjectId:
            draft_project = Project.clone(draft_project_dto.cloneFromProjectId,
                                          draft_project_dto.user_id)
        else:
            draft_project = Project()
            draft_project.create_draft_project(draft_project_dto)

        draft_project.set_project_aoi(draft_project_dto)

        # 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)

        if draft_project_dto.cloneFromProjectId:
            draft_project.save()  # Update the clone
        else:
            draft_project.create()  # Create the new project

        draft_project.set_default_changeset_comment()
        return draft_project.id
コード例 #27
0
    def create_draft_project(draft_project_dto: DraftProjectDTO) -> int:
        """
        Validates and then persists draft projects in the DB
        :param draft_project_dto: Draft Project DTO with data from API
        :raises InvalidGeoJson
        :returns ID of new draft project
        """
        # If we're cloning we'll copy all the project details from the clone, otherwise create brand new project
        if draft_project_dto.cloneFromProjectId:
            draft_project = Project.clone(draft_project_dto.cloneFromProjectId,
                                          draft_project_dto.user_id)
        else:
            draft_project = Project()
            draft_project.create_draft_project(draft_project_dto)

        draft_project.set_project_aoi(draft_project_dto)

        # 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)

        if draft_project_dto.cloneFromProjectId:
            draft_project.save()  # Update the clone
        else:
            draft_project.create()  # Create the new project

        draft_project.set_default_changeset_comment()
        return draft_project.id