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

fix: project post api #6589

Merged
merged 2 commits into from
Oct 7, 2024
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
13 changes: 3 additions & 10 deletions backend/api/projects/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@ async def get_project(
request.headers.get("accept-language"),
abbreviated,
)
print(project_dto)
if project_dto:
if as_file:
project_dto = json.dumps(project_dto, default=str)
Expand Down Expand Up @@ -301,7 +300,7 @@ async def patch(
project_id: int,
user: AuthUserDTO = Depends(login_required),
db: Database = Depends(get_db),
project_dto: ProjectDTO = None,
project_dto: dict = None,
):
"""
Updates a Tasking-Manager project
Expand Down Expand Up @@ -434,14 +433,8 @@ async def patch(
},
status_code=403,
)
try:
project_dto.project_id = project_id
except Exception as e:
logger.error(f"Error validating request: {str(e)}")
return JSONResponse(
content={"Error": "Unable to update project", "SubCode": "InvalidData"},
status_code=400,
)
project_dto = ProjectDTO(**project_dto)
project_dto.project_id = project_id

try:
await ProjectAdminService.update_project(project_dto, user.id, db)
Expand Down
5 changes: 4 additions & 1 deletion backend/models/dtos/project_dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,9 @@ class ProjectInfoDTO(BaseModel):
default="", serialization_alias="perTaskInstructions"
)

class Config:
populate_by_name = True


class CustomEditorDTO(BaseModel):
"""DTO to define a custom editor"""
Expand All @@ -213,7 +216,7 @@ class ProjectDTO(BaseModel):
tasks: Optional[dict] = None
default_locale: str = Field(alias="defaultLocale")
project_info: Optional[ProjectInfoDTO] = Field(None, alias="projectInfo")
project_info_locales: Optional[List["ProjectInfoDTO"]] = Field(
project_info_locales: Optional[List[ProjectInfoDTO]] = Field(
None, alias="projectInfoLocales"
)
difficulty: str = Field(alias="difficulty")
Expand Down
27 changes: 18 additions & 9 deletions backend/models/postgis/custom_editors.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from sqlalchemy import Column, Integer, String, ForeignKey
from backend.models.dtos.project_dto import CustomEditorDTO
from databases import Database
from sqlalchemy import Column, ForeignKey, Integer, String, delete, update

from backend.db import Base, get_session
from backend.models.dtos.project_dto import CustomEditorDTO

session = get_session()

Expand Down Expand Up @@ -29,24 +31,31 @@ def get_by_project_id(project_id: int):
return session.get(CustomEditor, project_id)

@classmethod
def create_from_dto(cls, project_id: int, dto: CustomEditorDTO):
async def create_from_dto(cls, project_id: int, dto: CustomEditorDTO, db: Database):
"""Creates a new CustomEditor from dto, used in project edit"""
new_editor = cls()
new_editor.project_id = project_id
new_editor.update_editor(dto)
new_editor = await new_editor.update_editor(dto, db)
return new_editor

def update_editor(self, dto: CustomEditorDTO):
async def update_editor(self, dto: CustomEditorDTO, db: Database):
"""Upates existing CustomEditor form DTO"""
self.name = dto.name
self.description = dto.description
self.url = dto.url
self.save()

def delete(self):
query = (
update(CustomEditor.__table__)
.where(CustomEditor.id == self.id)
.values(name=self.name, description=self.description, url=self.url)
)
await db.execute(query)

async def delete(self, db: Database):
"""Deletes the current model from the DB"""
session.delete(self)
session.commit()
await db.execute(
delete(CustomEditor.__table__).where(CustomEditor.id == self.id)
)

def as_dto(self) -> CustomEditorDTO:
"""Returns the CustomEditor as a DTO"""
Expand Down
21 changes: 16 additions & 5 deletions backend/models/postgis/priority_area.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import geojson
import json
from sqlalchemy import Column, Integer, ForeignKey, Table

import geojson
from databases import Database
from geoalchemy2 import Geometry
from backend.models.postgis.utils import InvalidGeoJson, ST_SetSRID, ST_GeomFromGeoJSON
from sqlalchemy import Column, ForeignKey, Integer, Table

from backend.db import Base, get_session
from backend.models.postgis.utils import InvalidGeoJson

session = get_session()

Expand All @@ -25,7 +28,7 @@ class PriorityArea(Base):
geometry = Column(Geometry("POLYGON", srid=4326))

@classmethod
def from_dict(cls, area_poly: dict):
async def from_dict(cls, area_poly: dict, db: Database):
"""Create a new Priority Area from dictionary"""
pa_geojson = geojson.loads(json.dumps(area_poly))

Expand All @@ -39,7 +42,15 @@ def from_dict(cls, area_poly: dict):

pa = cls()
valid_geojson = geojson.dumps(pa_geojson)
pa.geometry = ST_SetSRID(ST_GeomFromGeoJSON(valid_geojson), 4326)
query = """
SELECT ST_AsText(
ST_SetSRID(
ST_GeomFromGeoJSON(:geojson), 4326
)
) AS geometry_wkt;
"""
result = await db.fetch_one(query=query, values={"geojson": valid_geojson})
pa.geometry = result["geometry_wkt"] if result else None
return pa

def get_as_geojson(self):
Expand Down
78 changes: 58 additions & 20 deletions backend/models/postgis/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import geojson
from geoalchemy2 import Geometry, WKTElement
from geoalchemy2.shape import to_shape
from sqlalchemy import orm, func, select, delete, update
from sqlalchemy import orm, func, select, delete, update, inspect
from shapely.geometry import shape
from sqlalchemy.dialects.postgresql import ARRAY
import requests
Expand Down Expand Up @@ -348,17 +348,31 @@ async def create(self, project_name: str, db: Database):
)
)

await db.execute(
Task.__table__.insert().values(
project_id=project,
for task in self.tasks:
await db.execute(
Task.__table__.insert().values(
id=task.id,
project_id=project,
x=task.x,
y=task.y,
zoom=task.zoom,
is_square=task.is_square,
task_status=TaskStatus.READY.value,
extra_properties=task.extra_properties,
geometry=task.geometry,
)
)
)

return project

def save(self):
async def save(self, db: Database):
"""Save changes to db"""
session.commit()
columns = {
c.key: getattr(self, c.key) for c in inspect(self).mapper.column_attrs
}
await db.execute(
Project.__table__.update().where(Project.id == self.id).values(**columns)
)

@staticmethod
async def clone(project_id: int, author_id: int, db: Database):
Expand Down Expand Up @@ -492,7 +506,11 @@ async def update(self, project_dto: ProjectDTO, db: Database):
self.osmcha_filter_id = None

if project_dto.organisation:
org = Organisation.get(project_dto.organisation)
organization = await db.fetch_one(
"SELECT * FROM organisations WHERE id = :id",
values={"id": project_dto.organisation},
)
org = Organisation(**organization)
if org is None:
raise NotFound(
sub_code="ORGANISATION_NOT_FOUND",
Expand Down Expand Up @@ -535,18 +553,22 @@ async def update(self, project_dto: ProjectDTO, db: Database):

role = TeamRoles[team_dto.role].value
project_team = ProjectTeams(project=self, team=team, role=role)
session.add(project_team)
await project_team.create(db)

# Set Project Info for all returned locales
for dto in project_dto.project_info_locales:
project_info = self.project_info.filter_by(locale=dto.locale).one_or_none()
project_info = await db.fetch_one(
select(ProjectInfo).where(
ProjectInfo.project_id == self.id, ProjectInfo.locale == dto.locale
)
)
if project_info is None:
new_info = ProjectInfo.create_from_dto(
dto
new_info = await ProjectInfo.create_from_dto(
dto, self.id, db
) # Can't find info so must be new locale
self.project_info.append(new_info)
else:
project_info.update_from_dto(dto)
await ProjectInfo.update_from_dto(ProjectInfo(**project_info), dto, db)

self.priority_areas = [] # Always clear Priority Area prior to updating
if project_dto.priority_areas:
Expand All @@ -556,15 +578,17 @@ async def update(self, project_dto: ProjectDTO, db: Database):

if project_dto.custom_editor:
if not self.custom_editor:
new_editor = CustomEditor.create_from_dto(
self.id, project_dto.custom_editor
new_editor = await CustomEditor.create_from_dto(
self.id, project_dto.custom_editor, db
)
self.custom_editor = new_editor
else:
self.custom_editor.update_editor(project_dto.custom_editor)
await CustomEditor.update_editor(
self.custom_editor, project_dto.custom_editor, db
)
else:
if self.custom_editor:
self.custom_editor.delete()
await CustomEditor.delete(self.custom_editor, db)

# handle campaign update
try:
Expand All @@ -575,7 +599,9 @@ async def update(self, project_dto: ProjectDTO, db: Database):
current_ids = [c.id for c in self.campaign]
current_ids.sort()
if new_ids != current_ids:
self.campaign = Campaign.query.filter(Campaign.id.in_(new_ids)).all()
self.campaign = await db.fetch_all(
select(Campaign).filter(Campaign.id.in_(new_ids))
)

if project_dto.mapping_permission:
self.mapping_permission = MappingPermission[
Expand All @@ -596,13 +622,25 @@ async def update(self, project_dto: ProjectDTO, db: Database):
current_ids = [c.id for c in self.interests]
current_ids.sort()
if new_ids != current_ids:
self.interests = Interest.query.filter(Interest.id.in_(new_ids)).all()
self.interests = await db.fetch_all(
select(Interest).filter(Interest.id.in_(new_ids))
)

# try to update country info if that information is not present
if not self.country:
self.set_country_info()

session.commit()
columns = {
c.key: getattr(self, c.key) for c in inspect(self).mapper.column_attrs
}
columns.pop("geometry", None)
columns.pop("centroid", None)
columns.pop("id", None)

# Update the project in the database
await db.execute(
self.__table__.update().where(Project.id == self.id).values(**columns)
)

async def delete(self, db: Database):
"""Deletes the current model from the DB"""
Expand Down
45 changes: 39 additions & 6 deletions backend/models/postgis/project_info.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
# # from flask import current_app
from sqlalchemy.dialects.postgresql import TSVECTOR
from typing import List
from sqlalchemy import Column, String, Integer, ForeignKey, Index
from sqlalchemy import (
Column,
String,
Integer,
ForeignKey,
Index,
inspect,
insert,
update,
)
from backend.models.dtos.project_dto import ProjectInfoDTO
from backend.db import Base, get_session

Expand Down Expand Up @@ -41,13 +50,27 @@ def create_from_name(cls, name: str):
return new_info

@classmethod
def create_from_dto(cls, dto: ProjectInfoDTO):
async def create_from_dto(cls, dto: ProjectInfoDTO, project_id: int, db: Database):
"""Creates a new ProjectInfo class from dto, used from project edit"""
new_info = cls()
new_info.update_from_dto(dto)
return new_info
self = cls()
self.locale = dto.locale
self.name = dto.name
self.project_id = project_id
self.project_id_str = str(project_id) # Allows project_id to be searched

def update_from_dto(self, dto: ProjectInfoDTO):
# Note project info not bleached on basis that admins are trusted users and shouldn't be doing anything bad
self.short_description = dto.short_description
self.description = dto.description
self.instructions = dto.instructions
self.per_task_instructions = dto.per_task_instructions
columns = {
c.key: getattr(self, c.key) for c in inspect(self).mapper.column_attrs
}
query = insert(ProjectInfo.__table__).values(**columns)
result = await db.execute(query)
return result

async def update_from_dto(self, dto: ProjectInfoDTO, db: Database):
"""Updates existing ProjectInfo from supplied DTO"""
self.locale = dto.locale
self.name = dto.name
Expand All @@ -58,6 +81,16 @@ def update_from_dto(self, dto: ProjectInfoDTO):
self.description = dto.description
self.instructions = dto.instructions
self.per_task_instructions = dto.per_task_instructions
columns = {
c.key: getattr(self, c.key) for c in inspect(self).mapper.column_attrs
}
query = (
update(ProjectInfo.__table__)
.where(ProjectInfo.project_id == self.project_id)
.values(**columns)
)
result = await db.execute(query)
return result

@staticmethod
async def get_dto_for_locale(
Expand Down
2 changes: 2 additions & 0 deletions backend/models/postgis/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from sqlalchemy.orm.exc import MultipleResultsFound
from geoalchemy2 import Geometry
from typing import Any, Dict, List
from shapely.geometry import shape

from sqlalchemy import (
Column,
Expand Down Expand Up @@ -763,6 +764,7 @@ def from_geojson_feature(cls, task_id, task_feature):
task.y = task_feature.properties["y"]
task.zoom = task_feature.properties["zoom"]
task.is_square = task_feature.properties["isSquare"]
task.geometry = shape(task_feature.geometry).wkt
except KeyError as e:
raise InvalidData(
f"PropertyNotFound: Expected property not found: {str(e)}"
Expand Down
Loading
Loading