Skip to content

Commit

Permalink
feat: add project sharing
Browse files Browse the repository at this point in the history
  • Loading branch information
majkshkurti committed Jan 7, 2025
1 parent 7df13bf commit 59b9c67
Show file tree
Hide file tree
Showing 6 changed files with 307 additions and 2 deletions.
43 changes: 43 additions & 0 deletions alembic/versions/49b9ed64d5a7_add_project_public_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""add project_public table
Revision ID: 49b9ed64d5a7
Revises: 70bf9033bd58
Create Date: 2024-12-23 14:12:25.662809
"""
from alembic import op
import sqlalchemy as sa
import geoalchemy2
import sqlmodel

from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = '49b9ed64d5a7'
down_revision = '70bf9033bd58'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('project_public',
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('to_char(CURRENT_TIMESTAMP AT TIME ZONE \'UTC\', \'YYYY-MM-DD"T"HH24:MI:SSOF\')::timestamptz'), nullable=False),
sa.Column('id', postgresql.UUID(as_uuid=True), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('password', sa.Text(), nullable=True),
sa.Column('config', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('project_id', postgresql.UUID(as_uuid=True), nullable=True),
sa.ForeignKeyConstraint(['project_id'], ['customer.project.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
schema='customer'
)
op.create_index(op.f('ix_customer_project_public_id'), 'project_public', ['id'], unique=False, schema='customer')
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_customer_project_public_id'), table_name='project_public', schema='customer')
op.drop_table('project_public', schema='customer')
# ### end Alembic commands ###
31 changes: 31 additions & 0 deletions alembic/versions/70bf9033bd58_add_builder_config_column.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""add builder_config column
Revision ID: 70bf9033bd58
Revises: f3660efddfc9
Create Date: 2024-12-20 09:38:53.227648
"""
from alembic import op
import sqlalchemy as sa
import geoalchemy2
import sqlmodel

from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = '70bf9033bd58'
down_revision = 'f3660efddfc9'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('project', sa.Column('builder_config', postgresql.JSONB(astext_type=sa.Text()), nullable=True), schema='customer')
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('project', 'builder_config', schema='customer')
# ### end Alembic commands ###
87 changes: 86 additions & 1 deletion src/crud/crud_project.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
from uuid import UUID

from fastapi_pagination import Page
Expand All @@ -7,9 +8,9 @@

from src.core.config import settings
from src.core.content import (
build_shared_with_object,
create_query_shared_content,
update_content_by_id,
build_shared_with_object,
)
from src.crud.base import CRUDBase
from src.crud.crud_layer_project import layer_project as crud_layer_project
Expand All @@ -23,12 +24,16 @@
Team,
)
from src.db.models._link_model import UserProjectLink
from src.db.models.project import ProjectPublic
from src.schemas.common import OrderEnum
from src.schemas.project import (
InitialViewState,
IProjectBaseUpdate,
IProjectCreate,
IProjectRead,
ProjectPublicConfig,
ProjectPublicProjectConfig,
ProjectPublicRead,
)


Expand Down Expand Up @@ -160,5 +165,85 @@ async def update_base(

return IProjectRead(**project.dict())

async def get_public_project(
self, *, async_session: AsyncSession, project_id: str
) -> ProjectPublicRead | None:
project_public = select(ProjectPublic).where(
ProjectPublic.project_id == project_id
)
result = await async_session.execute(project_public)
project = result.scalars().first()
if not project:
return None
project_public_read = ProjectPublicRead(**project.dict())
return project_public_read

async def publish_project(
self, *, async_session: AsyncSession, project_id: str
) -> ProjectPublic:
project = select(Project).where(Project.id == project_id)
project = await async_session.execute(project)
project: Project | None = project.scalars().first()
if not project:
raise Exception("Project not found")
project_public = select(ProjectPublic).where(
ProjectPublic.project_id == project_id
)
project_public = await async_session.execute(project_public)
project_public: ProjectPublic | None = project_public.scalars().first()
user_project = select(UserProjectLink).where(
and_(
UserProjectLink.project_id == project_id,
Project.user_id == UserProjectLink.user_id,
)
)
user_project = await async_session.execute(user_project)
user_project: UserProjectLink = user_project.scalars().first()
if project_public:
await async_session.delete(project_public)

project_layers = await crud_layer_project.get_layers(
async_session=async_session, project_id=project_id
)

new_project_public_project_config = ProjectPublicProjectConfig(
id=project.id,
name=project.name,
description=project.description,
tags=project.tags,
thumbnail_url=project.thumbnail_url,
initial_view_state=user_project.initial_view_state,
layer_order=project.layer_order,
max_extent=project.max_extent,
folder_id=project.folder_id,
builder_config=project.builder_config,
)
new_project_public_config = ProjectPublicConfig(
layers=project_layers,
project=new_project_public_project_config,
)
new_project_public = ProjectPublic(
project_id=project_id, config=json.loads(new_project_public_config.json())
)

async_session.add(new_project_public)
await async_session.commit()
return new_project_public

async def unpublish_project(
self, *, async_session: AsyncSession, project_id: str
) -> None:
public_project = select(ProjectPublic).where(
ProjectPublic.project_id == project_id
)
public_project = await async_session.execute(public_project)
public_project = public_project.scalars().first()
if public_project:
await async_session.delete(public_project)
await async_session.commit()
else:
raise Exception("Project not found")
return None


project = CRUDProject(Project)
47 changes: 47 additions & 0 deletions src/db/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from pydantic import HttpUrl
from sqlalchemy import Text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.dialects.postgresql import UUID as UUID_PG
from sqlalchemy.sql import text
from sqlmodel import (
Expand Down Expand Up @@ -86,6 +87,13 @@ class Project(ContentBaseAttributes, DateTimeBase, table=True):
),
description="Max extent of the project",
)
builder_config: dict | None = Field(
sa_column=Column(
JSONB,
nullable=True,
),
description="Builder config",
)
# Relationships
user_projects: List["UserProjectLink"] = Relationship(
back_populates="project",
Expand All @@ -108,5 +116,44 @@ class Project(ContentBaseAttributes, DateTimeBase, table=True):
sa_relationship_kwargs={"cascade": "all, delete-orphan"},
)

project_public: "ProjectPublic" = Relationship(
back_populates="project",
sa_relationship_kwargs={"cascade": "all, delete-orphan", "uselist": False},
)


class ProjectPublic(DateTimeBase, table=True, extend_existing=True):
"""
A table representing a public project. A public project is a project that is accessible to the public.
Attributes:
password (str): The password required to access the project.
config (dict): The configuration of the project. This is a JSON object which includes project settings, layers etc.
project_id (UUID): The unique identifier for the project.
"""

__tablename__ = "project_public"
__table_args__ = {"schema": settings.CUSTOMER_SCHEMA}
id: UUID | None = Field(
sa_column=Column(
UUID_PG(as_uuid=True),
server_default=text("uuid_generate_v4()"),
nullable=False,
index=True,
primary_key=True,
)
)
password: str | None = Field(sa_column=Column(Text, nullable=True), max_length=255)
config: dict = Field(sa_column=Column(JSONB, nullable=False))
project_id: UUID = Field(
nullable=False,
sa_column=Column(
UUID_PG(as_uuid=True),
ForeignKey(f"{settings.CUSTOMER_SCHEMA}.project.id", ondelete="CASCADE"),
),
)

project: Project = Relationship(back_populates="project_public")


UniqueConstraint(Project.__table__.c.folder_id, Project.__table__.c.name)
61 changes: 61 additions & 0 deletions src/endpoints/v2/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -779,3 +779,64 @@ async def delete_scenario_features(
)

return None


##############################################
### Project public endpoints
##############################################


@router.get(
"/{project_id}/public",
summary="Get public project",
)
async def get_public_project(
project_id: str,
async_session: AsyncSession = Depends(get_db),
):
"""
Get shared project
"""
result = await crud_project.get_public_project(
async_session=async_session, project_id=project_id
)

return result


@router.post(
"/{project_id}/publish",
summary="Publish a project",
# dependencies=[Depends(auth_z)],
)
async def publish_project(
project_id: str,
async_session: AsyncSession = Depends(get_db),
):
"""
Publish a project
"""
result = await crud_project.publish_project(
async_session=async_session, project_id=project_id
)

return result


@router.delete(
"/{project_id}/unpublish",
summary="Unpublish a project",
# dependencies=[Depends(auth_z)],
)
async def unpublish_project(
project_id: str,
async_session: AsyncSession = Depends(get_db),
):
"""
Unpublish a project
"""
result = await crud_project.unpublish_project(
async_session=async_session, project_id=project_id
)

return result
40 changes: 39 additions & 1 deletion src/schemas/project.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from datetime import datetime
from enum import Enum
from typing import Optional
from typing import List, Optional
from uuid import UUID

from pydantic import BaseModel, Field, HttpUrl, validator
Expand Down Expand Up @@ -237,6 +238,43 @@ class IRasterProjectUpdate(BaseModel):
"table": ITableProjectUpdate,
}


class ProjectPublicProjectConfig(BaseModel):
id: UUID = Field(..., description="Project ID")
name: str = Field(..., description="Project name")
description: str | None = Field(..., description="Project description")
tags: List[str] | None = Field(..., description="Project tags")
thumbnail_url: HttpUrl | None = Field(None, description="Project thumbnail URL")
initial_view_state: InitialViewState = Field(
..., description="Initial view state of the project"
)
layer_order: list[int] | None = Field(None, description="Layer order in project")
max_extent: list[float] | None = Field(
None, description="Max extent of the project"
)
folder_id: UUID = Field(..., description="Folder ID")
builder_config: dict | None = Field(None, description="Builder config")


class ProjectPublicConfig(BaseModel):
layers: List[
IFeatureStandardProjectRead
| IFeatureToolProjectRead
| ITableProjectRead
| IRasterProjectRead
] = Field(..., description="Layers of the project")
project: ProjectPublicProjectConfig = Field(
..., description="Project configuration"
)


class ProjectPublicRead(BaseModel):
created_at: datetime = Field(..., description="Created at")
updated_at: datetime = Field(..., description="Updated at")
project_id: UUID
config: ProjectPublicConfig


# TODO: Refactor
request_examples = {
"get": {
Expand Down

0 comments on commit 59b9c67

Please sign in to comment.