Skip to content

Commit

Permalink
Merge branch 'development' of github.com:hotosm/fmtm into feat-submis…
Browse files Browse the repository at this point in the history
…sion-page
  • Loading branch information
sujanadh committed Dec 26, 2023
2 parents c54f506 + a8b2c15 commit 1290c59
Show file tree
Hide file tree
Showing 12 changed files with 169 additions and 28 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ A computer-screen-optimized web app that allows Campaign Managers to:

#### Steps to create a project in FMTM

- Go to [fmtm](https://fmtm.hotosm.org/) .
- Go to [fmtm](https://dev.fmtm.hotosm.org/) .
- If you are new then on the top right cornor click on Sign up and create an account.
Else, Sign in to your existing account .
- Click the '+ CREATE NEW PROJECT' button.
Expand Down
16 changes: 10 additions & 6 deletions src/backend/app/auth/auth_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@
from loguru import logger as log
from sqlalchemy.orm import Session

from ..db import database
from ..db.db_models import DbUser
from ..users import user_crud
from .osm import AuthUser, init_osm_auth, login_required
from app.db import database
from app.db.db_models import DbUser
from app.users import user_crud
from app.auth.osm import AuthUser, init_osm_auth, login_required

router = APIRouter(
prefix="/auth",
Expand Down Expand Up @@ -108,10 +108,14 @@ async def my_data(
"Please contact the administrator."
),
)

# Add user to database
db_user = DbUser(id=user_data["id"], username=user_data["username"])
db_user = DbUser(id=user_data["id"], username=user_data["username"], profile_img = user_data["img_url"])
db.add(db_user)
db.commit()
else:
if user_data.get("img_url"):
user.profile_img = user_data["img_url"]
db.commit()

return JSONResponse(content={"user_data": user_data}, status_code=200)
1 change: 1 addition & 0 deletions src/backend/app/db/db_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class DbUser(Base):

id = Column(BigInteger, primary_key=True, index=True)
username = Column(String, unique=True)
profile_img = Column(String)
role = Column(Enum(UserRole), default=UserRole.MAPPER)

name = Column(String)
Expand Down
27 changes: 27 additions & 0 deletions src/backend/app/projects/project_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@
from app.projects import project_schemas
from app.tasks import tasks_crud
from app.users import user_crud
from app.submission import submission_crud
from app.s3 import get_obj_from_bucket
from app.organization import organization_crud

QR_CODES_DIR = "QR_codes/"
TASK_GEOJSON_DIR = "geojson/"
Expand Down Expand Up @@ -2385,3 +2388,27 @@ async def get_pagination(page: int, count: int, results_per_page: int, total: in
)

return pagination


async def get_dashboard_detail(project_id: int, db: Session):
"""Get project details for project dashboard."""

project = await get_project(db, project_id)
db_organization = await organization_crud.get_organisation_by_id(db, project.organisation_id)

s3_project_path = f"/{project.organisation_id}/{project_id}"
s3_submission_path = f"/{s3_project_path}/submissions.meta.json"

file = get_obj_from_bucket(settings.S3_BUCKET_NAME, s3_submission_path)
project.last_active = (json.loads(file.getvalue()))["last_submission"]

contributors = db.query(db_models.DbTaskHistory).filter(db_models.DbTaskHistory.project_id==project_id).all()
unique_user_ids = {user.user_id for user in contributors if user.user_id is not None}

project.organization = db_organization.name
project.organization_logo = db_organization.logo
project.total_contributors = len(unique_user_ids)
project.total_submission = await submission_crud.get_submission_count_of_a_project(db, project_id)
project.total_tasks = await tasks_crud.get_task_count_in_project(db, project_id)

return project
18 changes: 18 additions & 0 deletions src/backend/app/projects/project_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1216,3 +1216,21 @@ async def get_template_file(
return FileResponse(
file_path, media_type="application/octet-stream", filename=filename
)


@router.get("/project_dashboard/{project_id}", response_model=project_schemas.ProjectDashboard)
async def project_dashboard(
project_id: int, db: Session = Depends(database.get_db)
):
"""
Get the project dashboard details.
Args:
project_id (int): The ID of the project.
db (Session): The database session.
Returns:
ProjectDashboard: The project dashboard details.
"""

return await project_crud.get_dashboard_detail(project_id, db)
41 changes: 40 additions & 1 deletion src/backend/app/projects/project_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
#

import uuid
from datetime import datetime
from dateutil import parser
from typing import List, Optional

from geojson_pydantic import Feature as GeojsonFeature
from pydantic import BaseModel
from pydantic import BaseModel, validator

from app.db import db_models
from app.models.enums import ProjectPriority, ProjectStatus, TaskSplitType
Expand Down Expand Up @@ -143,3 +145,40 @@ class ProjectOut(ProjectBase):
class BackgroundTaskStatus(BaseModel):
status: str
message: Optional[str] = None


class ProjectDashboard(BaseModel):
project_name_prefix: str
organization: str
organization_logo: Optional[str] = None
total_tasks: int
total_submission: int
total_contributors: int
created: datetime
last_active: Optional[str] = None

@validator("created", pre=False, always=True)
def get_created(cls, value, values):
date = value.strftime("%d %b %Y")
return date

@validator("last_active", pre=False, always=True)
def get_last_active(cls, value, values):
if value is None:
return None

last_active = parser.parse(value).replace(tzinfo=None)
current_date = datetime.now()

time_difference = current_date - last_active

days_difference = time_difference.days

if days_difference == 0:
return 'today'
elif days_difference == 1:
return 'yesterday'
elif days_difference < 7:
return f'{days_difference} day{"s" if days_difference > 1 else ""} ago'
else:
return last_active.strftime("%d %b %Y")
8 changes: 6 additions & 2 deletions src/backend/app/s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,16 @@ def get_obj_from_bucket(bucket_name: str, s3_path: str) -> BytesIO:
s3_path = f"/{s3_path}"

client = s3_client()
response = None
try:
response = client.get_object(bucket_name, s3_path)
return BytesIO(response.read())
except Exception as e:
raise ValueError(str(e))
finally:
response.close()
response.release_conn()
if response:
response.close()
response.release_conn()


def copy_obj_bucket_to_bucket(
Expand Down
34 changes: 28 additions & 6 deletions src/backend/app/tasks/tasks_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
# along with FMTM. If not, see <https:#www.gnu.org/licenses/>.
#
import base64
from typing import List

from fastapi import HTTPException
from fastapi import HTTPException, Depends
from geoalchemy2.shape import from_shape
from geojson import dump
from loguru import logger as log
Expand All @@ -26,15 +27,16 @@
from sqlalchemy.orm import Session
from sqlalchemy.sql import text

from ..central import central_crud
from ..db import db_models
from ..models.enums import (
from app.tasks import tasks_schemas
from app.central import central_crud
from app.db import db_models, database
from app.models.enums import (
TaskStatus,
get_action_for_status_change,
verify_valid_status_update,
)
from ..projects import project_crud
from ..users import user_crud
from app.projects import project_crud
from app.users import user_crud


async def get_task_count_in_project(db: Session, project_id: int):
Expand Down Expand Up @@ -317,3 +319,23 @@ async def edit_task_boundary(db: Session, task_id: int, boundary: str):
)

return True


async def update_task_history(tasks: List[tasks_schemas.TaskBase], db: Session = Depends(database.get_db)):
def process_history_entry(history_entry):
status = history_entry.action_text.split()
history_entry.status = status[5]

if history_entry.user_id:
user = db.query(db_models.DbUser).filter_by(id=history_entry.user_id).first()
if user:
history_entry.username = user.username
history_entry.profile_img = user.profile_img

for task in tasks if isinstance(tasks, list) else [tasks]:
task_history = task.task_history
if isinstance(task_history, list):
for history_entry in task_history:
process_history_entry(history_entry)

return tasks
21 changes: 11 additions & 10 deletions src/backend/app/tasks/tasks_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
from sqlalchemy.orm import Session
from sqlalchemy.sql import text

from ..central import central_crud
from ..db import database
from ..models.enums import TaskStatus
from ..projects import project_crud, project_schemas
from ..users import user_schemas
from app.central import central_crud
from app.db import database
from app.models.enums import TaskStatus
from app.projects import project_crud, project_schemas
from app.users import user_schemas
from . import tasks_crud, tasks_schemas

router = APIRouter(
Expand All @@ -37,17 +37,17 @@
responses={404: {"description": "Not found"}},
)


@router.get("/task-list", response_model=List[tasks_schemas.Task])
@router.get("/task-list", response_model=List[tasks_schemas.ReadTask])
async def read_task_list(
project_id: int,
limit: int = 1000,
db: Session = Depends(database.get_db),
):
tasks = await tasks_crud.get_tasks(db, project_id, limit)
updated_tasks = await tasks_crud.update_task_history(tasks, db)
if not tasks:
raise HTTPException(status_code=404, detail="Tasks not found")
return tasks
return updated_tasks


@router.get("/", response_model=List[tasks_schemas.Task])
Expand Down Expand Up @@ -109,7 +109,7 @@ async def read_tasks(task_id: int, db: Session = Depends(database.get_db)):
return task


@router.post("/{task_id}/new_status/{new_status}", response_model=tasks_schemas.Task)
@router.post("/{task_id}/new_status/{new_status}", response_model=tasks_schemas.ReadTask)
async def update_task_status(
user: user_schemas.User,
task_id: int,
Expand All @@ -120,9 +120,10 @@ async def update_task_status(
user_id = user.id

task = await tasks_crud.update_task_status(db, user_id, task_id, new_status)
updated_task = await tasks_crud.update_task_history(task, db)
if not task:
raise HTTPException(status_code=404, detail="Task status could not be updated.")
return task
return updated_task


@router.post("/task-qr-code/{task_id}")
Expand Down
10 changes: 8 additions & 2 deletions src/backend/app/tasks/tasks_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ class TaskHistoryBase(BaseModel):

class TaskHistoryOut(TaskHistoryBase):
"""Task mapping history display."""

pass
status: str
username: str
profile_img: Optional[str]


class TaskBase(BaseModel):
Expand Down Expand Up @@ -144,3 +145,8 @@ def get_qrcode_base64(cls, v: Any, info: ValidationInfo) -> str:
else:
log.warning(f"No QR code found for task ID {info.data.get('id')}")
return ""

class ReadTask(Task):
"""Task details plus updated task history."""

task_history: Optional[List[TaskHistoryOut]] = None
10 changes: 10 additions & 0 deletions src/backend/migrations/002-add-profile-img.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- ## Migration to:
-- * Add field user.profile_img (str).

-- Start a transaction
BEGIN;

ALTER TABLE IF EXISTS public.users
ADD COLUMN IF NOT EXISTS profile_img VARCHAR;
-- Commit the transaction
COMMIT;
9 changes: 9 additions & 0 deletions src/backend/migrations/revert/002-add-profile-img.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- Start a transaction
BEGIN;

-- Remove the profile_img column from the public.users table
ALTER TABLE IF EXISTS public.users
DROP COLUMN IF EXISTS profile_img;

-- Commit the transaction
COMMIT;

0 comments on commit 1290c59

Please sign in to comment.