This commit is contained in:
wuaho 2021-04-30 18:52:30 +08:00
parent 4c15ded189
commit 3e2edceb1c
27 changed files with 266 additions and 168 deletions

View File

@ -1,3 +1,4 @@
# xbackend
x后端
mysql 层级关系处理 过于复杂了 。下一版本使用mongodb

View File

@ -1,4 +1,3 @@
from datetime import timedelta
from typing import Any, List
from fastapi import APIRouter, Body, Depends, HTTPException, Request
@ -21,48 +20,107 @@ def read_dashboard(project_id: int = Depends(deps.check_project),
# 新建看板
@router.post("/create-dashboard")
def create_dashboard(dashboard_in: schemas.DashboardCreate,
project_id: int = Depends(deps.check_project),
def create_dashboard(dashboard_in: schemas.DashboardIn,
db: Session = Depends(deps.get_db),
current_user: models.User = Depends(deps.get_current_active_user)) -> Any:
dashboard = crud.dashboard.create_with_dashboard(db=db, obj_in=dashboard_in, user_id=current_user.id)
user_id = current_user.id
tree = deps.check_tree(db=db, **dashboard_in.dict(), user_id=user_id)
dashboard = crud.dashboard.create_with_dashboard(db=db, obj_in=schemas.DashboardCreate(**dashboard_in.dict()),
user_id=current_user.id,
)
# 自己创建的拥有权限
crud.authority.create_with_authority(db=db, obj_in=schemas.AuthorityCreate(
dashboard_id=dashboard.id,
project_id=project_id,
project_id=tree.project_id,
authority='rw',
space_id=dashboard.space_id,
folder_id=dashboard.folder_id
), user_id=current_user.id)
space_id=tree.space_id,
folder_id=tree.folder_id
), user_id=user_id)
return {"msg": "新建成功", 'code': 0}
# 删除看板
@router.post("/delete-dashboard")
def create_dashboard(dashboard_del: schemas.DashboardDelete,
db: Session = Depends(deps.get_db),
current_user: models.User = Depends(deps.get_current_active_user)) -> Any:
"""
只能删除自己创建的 会级联删除权限表
:param dashboard_del:
:param db:
:param current_user:
:return:
"""
user_id = current_user.id
dashboard = crud.dashboard.delete_my(db, id=dashboard_del.id, user_id=user_id)
return {"msg": "删除成功", 'code': 0, 'data': dashboard}
@router.post("/create-folder")
def create_folder(folder_in: schemas.FolderCreate, project_id: int = Depends(deps.check_project),
def create_folder(folder_in: schemas.FolderIn,
db: Session = Depends(deps.get_db),
current_user: models.User = Depends(deps.get_current_active_user)) -> Any:
folder = crud.folder.create_with_folder(db=db, obj_in=folder_in, user_id=current_user.id, project_id=project_id)
user_id = current_user.id
tree = deps.check_tree(db, **folder_in.dict(), user_id=user_id)
folder = crud.folder.create_with_folder(db=db, obj_in=schemas.FolderCreate(**folder_in.dict()),
user_id=current_user.id)
# 自己创建的拥有权限
crud.authority.create_with_authority(db=db, obj_in=schemas.AuthorityCreate(
project_id=project_id,
project_id=tree.project_id,
authority='rw',
space_id=folder.space_id,
space_id=tree.space_id,
folder_id=folder.id
), user_id=current_user.id)
), user_id=user_id)
return {"msg": "新建成功", 'code': 0}
# 删除文件夹
@router.post("/delete-folder")
def delete_folder(folder_del: schemas.FolderDelete,
db: Session = Depends(deps.get_db),
current_user: models.User = Depends(deps.get_current_active_user)) -> Any:
"""
只能删除自己创建的 会级联删除权限表
:param dashboard_del:
:param db:
:param current_user:
:return:
"""
user_id = current_user.id
folder = crud.folder.delete_my(db, id=folder_del.id, user_id=user_id)
return {"msg": "删除成功", 'code': 0, 'data': folder}
@router.post("/create-space")
def create_space(space_in: schemas.SpaceCreate, project_id: int = Depends(deps.check_project),
def create_space(space_in: schemas.SpaceIn,
db: Session = Depends(deps.get_db),
current_user: models.User = Depends(deps.get_current_active_user)) -> Any:
space = crud.space.create_with_space(db=db, obj_in=space_in, user_id=current_user.id,project_id=project_id)
user_id = current_user.id
tree = deps.check_tree(db, **space_in.dict(), user_id=user_id)
space = crud.space.create_with_space(db=db, obj_in=schemas.SpaceCreate(**space_in.dict()), user_id=user_id)
# 自己创建的拥有权限
crud.authority.create_with_authority(db=db, obj_in=schemas.AuthorityCreate(
project_id=project_id,
project_id=tree.project_id,
authority='rw',
space_id=space.id,
), user_id=current_user.id)
return {"msg": "新建成功", 'code': 0}
# 删除文件夹
@router.post("/delete-space")
def delete_space(space_del: schemas.SpaceDelete,
db: Session = Depends(deps.get_db),
current_user: models.User = Depends(deps.get_current_active_user)) -> Any:
"""
只能删除自己创建的 会级联删除权限表
"""
user_id = current_user.id
space = crud.space.delete_my(db, id=space_del.id, user_id=user_id)
return {"msg": "删除成功", 'code': 0, 'data': space}

View File

@ -20,8 +20,8 @@ router = APIRouter()
@router.post("/login")
def login(
data: schemas.UserLogin,
# data: OAuth2PasswordRequestForm = Depends(),
# data: schemas.UserLogin,
data: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(deps.get_db),
) -> Any:
"""
@ -35,15 +35,23 @@ def login(
elif not crud.user.is_active(user):
raise HTTPException(status_code=400, detail="Inactive user")
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
return {
'data': {
'name': user.name,
'email': user.email,
'token': security.create_access_token(
'access_token': security.create_access_token(
expires_delta=access_token_expires, id=user.id, email=user.email, is_active=user.is_active,
is_superuser=user.is_superuser, name=user.name
),
"token_type": "bearer",
},
'access_token': security.create_access_token(
expires_delta=access_token_expires, id=user.id, email=user.email, is_active=user.is_active,
is_superuser=user.is_superuser, name=user.name
),
"token_type": "bearer",
'code': 0,
'msg': 'success',

View File

@ -1,5 +1,5 @@
from datetime import timedelta
from typing import Any
from typing import Any, List
from fastapi import APIRouter, Body, Depends, HTTPException, Request
from sqlalchemy.orm import Session
@ -10,26 +10,37 @@ from api import deps
router = APIRouter()
@router.get("/", response_model=List[schemas.Project])
def read_project(db: Session = Depends(deps.get_db),
current_user: models.User = Depends(deps.get_current_active_user)
) -> Any:
my_project = crud.project.get_my_project(db=db, user_id=current_user.id)
return my_project
@router.post("/create-project", response_model=schemas.Project)
def create_project(project_in: schemas.ProjectCreate, db: Session = Depends(deps.get_db),
current_user: models.User = Depends(deps.get_current_active_user)
current_user: models.User = Depends(deps.get_current_active_superuser)
) -> Any:
project = crud.project.create_with_project(db=db, obj_in=project_in, user_id=current_user.id)
# 我的看板 新建 未分组 和 共享给我的 文件夹
kanban = crud.kanban.create(db=db, obj_in=schemas.KanBanCreate(project_id=project.id, user_id=current_user.id))
unknown_folder = schemas.FolderCreate(
project_id=project.id,
user_id=current_user.id,
kanban_id=kanban.id,
name='未分组的'
)
share_folder = schemas.FolderCreate(
project_id=project.id,
kanban_id=kanban.id,
user_id=current_user.id,
name='共享给我的'
)
crud.folder.create_with_folder(db=db, obj_in=unknown_folder, user_id=current_user.id, )
crud.folder.create_with_folder(db=db, obj_in=share_folder, user_id=current_user.id)
folder1 = crud.folder.create_with_folder(db=db, obj_in=unknown_folder, user_id=current_user.id, )
folder2 = crud.folder.create_with_folder(db=db, obj_in=share_folder, user_id=current_user.id)
# 拥有这俩个文件夹权限
authority1 = schemas.AuthorityCreate(project_id=project.id, authority='rw', folder_id=folder1.id)
authority2 = schemas.AuthorityCreate(project_id=project.id, authority='rw', folder_id=folder2.id)
crud.authority.create_with_authority(db=db, obj_in=authority1, user_id=current_user.id)
crud.authority.create_with_authority(db=db, obj_in=authority2, user_id=current_user.id)
return project

View File

@ -11,9 +11,10 @@ import crud, models, schemas
from core import security
from core.config import settings
from db.session import SessionLocal
from models.authority import Authority
reusable_oauth2 = OAuth2PasswordBearer(
tokenUrl=f"{settings.API_V1_STR}/login/"
tokenUrl=f"{settings.API_V1_STR}/user/login/"
)
@ -25,10 +26,10 @@ def get_db() -> Generator:
db.close()
# def get_current_user(token: str = Depends(reusable_oauth2)
# ) -> schemas.UserDBBase:
def get_current_user(token: str
def get_current_user(token: str = Depends(reusable_oauth2)
) -> schemas.UserDBBase:
# def get_current_user(token: str
# ) -> schemas.UserDBBase:
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[security.ALGORITHM]
@ -66,3 +67,37 @@ def check_project(project_id: int, db: Session = Depends(get_db)):
if not crud.project.get(db, id=project_id):
raise HTTPException(status_code=404, detail="没有这个项目")
return project_id
def check_tree(db, *, project_id: int,
user_id: int,
space_id: int = None,
folder_id: int = None,
dashboard_id: int = None,
**kwargs
) -> schemas.Tree:
where = [
Authority.project_id == project_id,
Authority.user_id == user_id
]
if space_id is not None:
where.append(Authority.space_id == space_id)
if folder_id is not None:
where.append(Authority.folder_id == folder_id)
if dashboard_id is not None:
where.append(Authority.dashboard_id == dashboard_id)
if not db.query(Authority).filter(*where).first():
raise HTTPException(status_code=406, detail='请检查参数')
return schemas.Tree(project_id=project_id,
space_id=space_id,
folder_id=folder_id,
dashboard_id=dashboard_id
)

View File

@ -1,6 +1,5 @@
from .crud_user import user
from .curd_project import project
from .curd_kanban import kanban
from .curd_folder import folder
from .curd_dashboard import dashboard
from .curd_space import space

View File

@ -1,5 +1,6 @@
from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union
from fastapi import HTTPException
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
from sqlalchemy.orm import Session
@ -27,7 +28,7 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
return db.query(self.model).filter(self.model.id == id).first()
def get_multi(
self, db: Session, *, skip: int = 0, limit: int = 100
self, db: Session, *, skip: int = 0, limit: int = 100
) -> List[ModelType]:
return db.query(self.model).offset(skip).limit(limit).all()
@ -40,11 +41,11 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
return db_obj
def update(
self,
db: Session,
*,
db_obj: ModelType,
obj_in: Union[UpdateSchemaType, Dict[str, Any]]
self,
db: Session,
*,
db_obj: ModelType,
obj_in: Union[UpdateSchemaType, Dict[str, Any]]
) -> ModelType:
obj_data = jsonable_encoder(db_obj)
if isinstance(obj_in, dict):
@ -64,3 +65,11 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
db.delete(obj)
db.commit()
return obj
def delete_my(self, db: Session, *, id: int, user_id: int) -> ModelType:
obj = db.query(self.model).filter(self.model.id == id, self.model.user_id == user_id).first()
if not obj:
raise HTTPException(status_code=406, detail='找不到你要删除的')
db.delete(obj)
db.commit()
return obj

View File

@ -13,6 +13,7 @@ class CRUDAuthority(CRUDBase[Authority, AuthorityCreate, AuthorityUpdate]):
self, db: Session, *, obj_in: AuthorityCreate, user_id: int
) -> Authority:
obj_in_data = jsonable_encoder(obj_in)
db_obj = self.model(**obj_in_data, user_id=user_id)
db.add(db_obj)
db.commit()
@ -59,12 +60,26 @@ class CRUDAuthority(CRUDBase[Authority, AuthorityCreate, AuthorityUpdate]):
'id': dashboard.id
})
return res
# return {
# 'kanban': list(res['kanban'].values()),
# 'space': list(res['space'].values())
# }
def delete(self, db: Session, user_id: int, project_id: int = None, folder_id: int = None, space_id: int = None,
dashboard_id: int = None):
where = [
Authority.user_id == user_id
]
if project_id is not None:
where.append(Authority.project_id == project_id)
if space_id is not None:
where.append(Authority.space_id == space_id)
if folder_id is not None:
where.append(Authority.folder_id == folder_id)
if dashboard_id is not None:
where.append(Authority.dashboard_id == dashboard_id)
if not db.query(Authority).filter(*where).delete():
return
authority = CRUDAuthority(Authority)

View File

@ -5,6 +5,7 @@ from fastapi.encoders import jsonable_encoder
from sqlalchemy.orm import Session
from crud.base import CRUDBase
from models.authority import Authority
from models.dashboard import Dashboard
from models.folders import Folder
from models.space import Space
@ -24,5 +25,4 @@ class CRUDDashboard(CRUDBase[Dashboard, DashboardCreate, DashboardUpdate]):
dashboard = CRUDDashboard(Dashboard)

View File

@ -6,26 +6,22 @@ from sqlalchemy.orm import Session
from crud.base import CRUDBase
from models.folders import Folder
from models.kanban import KanBan
from models.space import Space
from schemas import FolderUpdate, FolderCreate
class CRUDFolder(CRUDBase[Folder, FolderCreate, FolderUpdate]):
def create_with_folder(
self, db: Session, *, obj_in: FolderCreate, user_id: int, project_id: int
self, db: Session, *, obj_in: FolderCreate, user_id: int
) -> Folder:
obj_in_data = jsonable_encoder(obj_in)
db_obj = self.model(**obj_in_data, user_id=user_id)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
if (not (obj_in.space_id and db.query(Space).get(obj_in.space_id))) ^ \
(not (obj_in.kanban_id and db.query(KanBan).get(obj_in.kanban_id))):
obj_in_data = jsonable_encoder(obj_in)
db_obj = self.model(**obj_in_data, user_id=user_id, project_id=project_id)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
else:
raise HTTPException(status_code=404, detail="找不到上级")
folder = CRUDFolder(Folder)

View File

@ -1,15 +0,0 @@
from typing import Any, Dict, Optional, Union
from fastapi.encoders import jsonable_encoder
from sqlalchemy.orm import Session
from crud.base import CRUDBase
from models.kanban import KanBan
from schemas import KanBanUpdate,KanBanCreate
class CRUDKanBan(CRUDBase[KanBan, KanBanCreate, KanBanUpdate]):
pass
kanban = CRUDKanBan(KanBan)

View File

@ -1,9 +1,11 @@
from typing import Any, Dict, Optional, Union
from fastapi import HTTPException
from fastapi.encoders import jsonable_encoder
from sqlalchemy.orm import Session
from crud.base import CRUDBase
from models.authority import Authority
from models.project import Project
@ -14,14 +16,20 @@ class CRUDProject(CRUDBase[Project, ProjectCreate, ProjectUpdate]):
def create_with_project(
self, db: Session, *, obj_in: ProjectCreate, user_id: int
) -> Project:
if db.query(self.model).filter(self.model.name == obj_in.name).first():
raise HTTPException(status_code=406, detail="项目名已存在")
obj_in_data = jsonable_encoder(obj_in)
db_obj = self.model(**obj_in_data, user_id=user_id)
db_obj = Project(**obj_in_data, user_id=user_id)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def get_my_project(
self, db: Session, *, user_id: int
) -> Project:
return db.query(self.model).join(Authority).filter(Authority.user_id == user_id).all()
project = CRUDProject(Project)

View File

@ -10,10 +10,10 @@ from schemas import SpaceCreate, SpaceUpdate
class CRUDSpace(CRUDBase[Space, SpaceCreate, SpaceUpdate]):
def create_with_space(
self, db: Session, *, obj_in: SpaceCreate, user_id: int, project_id: int
self, db: Session, *, obj_in: SpaceCreate, user_id: int
) -> Space:
obj_in_data = jsonable_encoder(obj_in)
db_obj = self.model(**obj_in_data, user_id=user_id, project_id=project_id)
db_obj = self.model(**obj_in_data, user_id=user_id)
db.add(db_obj)
db.commit()
db.refresh(db_obj)

View File

@ -15,6 +15,7 @@ class Authority(Base):
space_id = Column(Integer, ForeignKey('space.id'))
authority = Column(Enum('rw', 'r'))
create_date = Column(DateTime, default=datetime.datetime.now())
dashboard = relationship('Dashboard', backref="authority")
folder = relationship('Folder', backref="authority")
space = relationship('Space', backref="authority")

View File

@ -4,18 +4,16 @@ from sqlalchemy import Boolean, Column, Integer, String, ForeignKey, DateTime
from sqlalchemy.orm import relationship, backref
from db.base_class import Base
from models.authority import Authority
class Dashboard(Base):
id = Column(Integer, primary_key=True, index=True)
name = Column(String)
user_id = Column(Integer, ForeignKey('user.id'))
folder_id = Column(Integer, ForeignKey('folder.id'))
space_id = Column(Integer, ForeignKey('space.id'))
create_date = Column(DateTime, default=datetime.datetime.now())
folder = relationship('Folder', backref="dashboard")
space = relationship('Space', backref="dashboard")
# authority = relationship('Authority', backref="dashboard",cascade="all,delete-orphan")
# authority = relationship('Authority', backref=backref("dashboard", lazy="joined"))

View File

@ -10,12 +10,6 @@ class Folder(Base):
id = Column(Integer, primary_key=True, index=True)
name = Column(String)
user_id = Column(Integer, ForeignKey('user.id'))
kanban_id = Column(Integer, ForeignKey('kanban.id'))
project_id = Column(Integer, ForeignKey('project.id'))
space_id = Column(Integer, ForeignKey('space.id'))
create_date = Column(DateTime, default=datetime.datetime.now())
kanban = relationship('KanBan', backref='folder')
space = relationship('Space', backref='folder')
# dashboard = relationship('Dashboard', backref=backref("folder", lazy="joined"))
# authority = relationship('Authority', backref="folder", cascade="all, delete")

View File

@ -1,18 +0,0 @@
import datetime
from sqlalchemy import Boolean, Column, Integer, String, ForeignKey, DateTime
from sqlalchemy.orm import relationship, backref
from db.base_class import Base
class KanBan(Base):
id = Column(Integer, primary_key=True, index=True)
project_id = Column(Integer, ForeignKey('project.id'))
user_id = Column(Integer, ForeignKey('user.id'))
create_date = Column(DateTime, default=datetime.datetime.now())
project = relationship('Project', backref='kanban')
# folder = relationship('Folder', backref=backref("kanban", lazy="joined"))

View File

@ -1,4 +1,5 @@
import datetime
import uuid
from sqlalchemy import Boolean, Column, Integer, String, ForeignKey, DateTime
from sqlalchemy.orm import relationship
@ -8,8 +9,10 @@ from db.base_class import Base
class Project(Base):
id = Column(Integer, primary_key=True, index=True)
game = Column(String, unique=True)
app_id = Column(String, unique=True, default=uuid.uuid1().hex)
name = Column(String)
user_id = Column(Integer, ForeignKey('user.id'))
create_date = Column(DateTime, default=datetime.datetime.now())
# authority = relationship('Authority', backref="project", cascade="all, delete")

View File

@ -10,10 +10,11 @@ class Space(Base):
id = Column(Integer, primary_key=True, index=True)
name = Column(String)
user_id = Column(Integer, ForeignKey('user.id'))
project_id = Column(Integer, ForeignKey('project.id'))
create_date = Column(DateTime, default=datetime.datetime.now())
project = relationship('Project', backref='space')
# authority = relationship('Authority', backref="space", cascade="all, delete")
# dashboard = relationship('Dashboard', backref=backref("space", lazy="joined"))
# folder = relationship('Folder', backref=backref("space", lazy="joined"))

View File

@ -3,8 +3,8 @@ from .token import Token, TokenPayload
from .msg import Msg
from .project import ProjectCreate, Project
from .kanban import KanBanCreate, KanBanUpdate
from .folders import FolderCreate, FolderUpdate
from .dashboard import Dashboard,DashboardCreate, DashboardUpdate
from .space import SpaceCreate, SpaceUpdate
from .folders import FolderCreate, FolderUpdate, FolderIn, FolderDelete
from .dashboard import Dashboard, DashboardCreate, DashboardUpdate, DashboardIn, DashboardDelete
from .space import SpaceCreate, SpaceUpdate, SpaceIn, SpaceDelete
from .authority import AuthorityCreate, AuthorityUpdate
from .tree import Tree

View File

@ -7,8 +7,8 @@ class AuthorityBase(BaseModel):
project_id: int = None
dashboard_id: int = None
authority: str = None
space_id: str = None
folder_id: str = None
space_id: int = None
folder_id: int = None
class AuthorityCreate(AuthorityBase):

View File

@ -6,19 +6,27 @@ from pydantic import BaseModel, root_validator, Field
class DashboardBase(BaseModel):
name: str = None
folder_id: int = None
class DashboardIn(DashboardBase):
project_id: int
space_id: int = None
class DashboardCreate(DashboardBase):
name: str
folder_id: int = None
# @root_validator(pre=True)
# def check_parent(cls, values):
# if (values.get('folder_id') is None) ^ (values.get('space_id') is None):
# return values
# else:
# raise ValueError('看板必须有一个上级')
# raise ValueError('必须属于文件夹或者空间')
class DashboardDelete(BaseModel):
id: int
class DashboardCreate(DashboardBase):
name: str
class DashboardUpdate(DashboardBase):
@ -26,10 +34,9 @@ class DashboardUpdate(DashboardBase):
class Dashboard(DashboardBase):
name: str=None
user_id: int=None
folder_id: int=None
space_id: int=None
name: str
user_id: int
create_date: datetime
class Config:
orm_mode = True

View File

@ -4,21 +4,22 @@ from pydantic import BaseModel, root_validator
class FolderBase(BaseModel):
kanban_id: int = None
name: str = None
class FolderIn(FolderBase):
project_id: int
space_id: int = None
name: str
class FolderDelete(BaseModel):
id: int
class FolderCreate(FolderBase):
name: str
@root_validator(pre=True)
def check_parent(cls, values):
if (values.get('kanban_id') is None) ^ (values.get('space_id') is None):
return values
else:
raise ValueError('必须只有一个上级')
class FolderUpdate(FolderBase):
pass
@ -27,9 +28,7 @@ class FolderUpdate(FolderBase):
class Folder(FolderBase):
id: int
name: str
kanban_id: str
user_id: int
project_id: int
create_date: datetime
class Config:

View File

@ -1,28 +0,0 @@
from datetime import datetime
from pydantic import BaseModel
class KanBanBase(BaseModel):
project_id: int = None
user_id: int = None
class KanBanCreate(KanBanBase):
pass
class KanBanUpdate(KanBanBase):
pass
class KanBan(KanBanBase):
id: int
game: str
name: str
user_id: int
create_date: datetime
class Config:
orm_mode = True

View File

@ -5,12 +5,10 @@ from pydantic import BaseModel
# 创建项目请求
class ProjectBase(BaseModel):
game: str = None
name: str = None
class ProjectCreate(ProjectBase):
game: str
name: str
@ -20,7 +18,7 @@ class ProjectUpdate(ProjectBase):
class Project(ProjectBase):
id: int
game: str
app_id: str
name: str
user_id: int
create_date: datetime

View File

@ -7,6 +7,15 @@ class SpaceBase(BaseModel):
name: str = None
class SpaceIn(SpaceBase):
project_id: int
name: str
class SpaceDelete(BaseModel):
id: int
class SpaceCreate(SpaceBase):
name: str

9
schemas/tree.py Normal file
View File

@ -0,0 +1,9 @@
from pydantic import BaseModel
class Tree(BaseModel):
project_id: int
space_id: int = None
folder_id: int = None
dashboard_id: int = None