mirror of
https://github.com/DarrylNixon/ghostforge
synced 2024-04-22 06:27:20 -07:00
Add data tables and some UI work
This commit is contained in:
parent
47931af84f
commit
9dab48a219
45 changed files with 2200 additions and 62 deletions
|
@ -6,30 +6,15 @@ from fastapi_users_db_sqlmodel import SQLModelUserDatabaseAsync
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine
|
from sqlalchemy.ext.asyncio import create_async_engine
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from sqlmodel import Field
|
||||||
# from fastapi_users.db import SQLAlchemyBaseUserTableUUID
|
|
||||||
# from fastapi_users.db import SQLAlchemyUserDatabase
|
|
||||||
# from sqlalchemy.orm import DeclarativeBase
|
|
||||||
|
|
||||||
# from sqlmodel import SQLModel
|
|
||||||
|
|
||||||
|
|
||||||
# class Base(DeclarativeBase):
|
|
||||||
# pass
|
|
||||||
|
|
||||||
|
|
||||||
# Create a "Base" class for use with fastapi-users-db-sqlmodel and sqlalchemy
|
|
||||||
class Base(SQLModelBaseUserDB):
|
class Base(SQLModelBaseUserDB):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Create a "User" class for user with fastapi-users-db-sqlmodel
|
|
||||||
class User(Base, table=True):
|
class User(Base, table=True):
|
||||||
pass
|
username: str = Field(unique=True)
|
||||||
|
|
||||||
|
|
||||||
# class User(SQLAlchemyBaseUserTableUUID, Base):
|
|
||||||
# pass
|
|
||||||
|
|
||||||
|
|
||||||
database_url = (
|
database_url = (
|
||||||
|
|
|
@ -1,16 +1,27 @@
|
||||||
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from datetime import timezone
|
from operator import truth
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
|
from fastapi import Form
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from fastapi import Query
|
from fastapi import Query
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
|
from sqlalchemy import asc
|
||||||
|
from sqlalchemy import desc
|
||||||
|
from sqlalchemy import exists
|
||||||
|
from sqlalchemy import func
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
from sqlalchemy import or_
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy import Text
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlmodel import and_
|
||||||
|
from sqlmodel import AutoString
|
||||||
from sqlmodel import Column
|
from sqlmodel import Column
|
||||||
from sqlmodel import Field
|
from sqlmodel import Field
|
||||||
from sqlmodel import SQLModel
|
from sqlmodel import SQLModel
|
||||||
|
@ -35,6 +46,7 @@ class Ghost(SQLModel, table=True):
|
||||||
TIMESTAMP(timezone=True),
|
TIMESTAMP(timezone=True),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
owner_id: Optional[uuid.UUID] = Field(foreign_key="user.id")
|
||||||
|
|
||||||
|
|
||||||
class GhostCreate(Ghost, table=False):
|
class GhostCreate(Ghost, table=False):
|
||||||
|
@ -49,6 +61,49 @@ class GhostRead(Ghost, table=False):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class GhostPermissions(SQLModel, table=True):
|
||||||
|
id: int = Field(primary_key=True)
|
||||||
|
user_id: Optional[uuid.UUID] = Field(foreign_key="user.id")
|
||||||
|
ghost_id: Optional[int] = Field(foreign_key="ghost.id")
|
||||||
|
can_edit: Optional[bool] = Field()
|
||||||
|
|
||||||
|
|
||||||
|
async def can_view_ghost(
|
||||||
|
ghost_id: int,
|
||||||
|
current_user: Annotated[User, Depends(get_current_user())],
|
||||||
|
session: AsyncSession = Depends(get_session),
|
||||||
|
):
|
||||||
|
res = await session.get(Ghost, ghost_id)
|
||||||
|
if res.owner_id == current_user.id:
|
||||||
|
return True
|
||||||
|
res = await session.execute(
|
||||||
|
select(GhostPermissions).where(
|
||||||
|
and_(GhostPermissions.ghost_id == ghost_id, GhostPermissions.user_id == current_user.id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return truth(res.scalars().first())
|
||||||
|
|
||||||
|
|
||||||
|
async def can_edit_ghost(
|
||||||
|
ghost_id: int,
|
||||||
|
current_user: Annotated[User, Depends(get_current_user())],
|
||||||
|
session: AsyncSession = Depends(get_session),
|
||||||
|
):
|
||||||
|
res = await session.get(Ghost, ghost_id)
|
||||||
|
if res.owner_id == current_user.id:
|
||||||
|
return True
|
||||||
|
res = await session.execute(
|
||||||
|
select(GhostPermissions).where(
|
||||||
|
and_(
|
||||||
|
GhostPermissions.ghost_id == ghost_id,
|
||||||
|
GhostPermissions.user_id == current_user.id,
|
||||||
|
GhostPermissions.can_edit,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return truth(res.scalars().first())
|
||||||
|
|
||||||
|
|
||||||
@gf.get("/ghosts/{ghost_id}")
|
@gf.get("/ghosts/{ghost_id}")
|
||||||
@hj.html_or_json("ghost.html")
|
@hj.html_or_json("ghost.html")
|
||||||
async def read_ghost(
|
async def read_ghost(
|
||||||
|
@ -56,9 +111,19 @@ async def read_ghost(
|
||||||
current_user: Annotated[User, Depends(get_current_user())],
|
current_user: Annotated[User, Depends(get_current_user())],
|
||||||
session: AsyncSession = Depends(get_session),
|
session: AsyncSession = Depends(get_session),
|
||||||
request: Request = None,
|
request: Request = None,
|
||||||
|
can_view: bool = Depends(can_view_ghost),
|
||||||
):
|
):
|
||||||
ghost = await session.get(Ghost, ghost_id)
|
if not can_view:
|
||||||
data = {"crumbs": [("ghosts", "/ghosts"), (ghost.id, False)]}
|
raise HTTPException(status_code=403, detail="You're not authorized to see this ghost")
|
||||||
|
result = await session.execute(
|
||||||
|
select(Ghost, User.username.label("owner_username"))
|
||||||
|
.join(User, Ghost.owner_id == User.id)
|
||||||
|
.where(Ghost.id == ghost_id)
|
||||||
|
)
|
||||||
|
ghost = result.scalars().first()
|
||||||
|
if not ghost:
|
||||||
|
raise HTTPException(status_code=404, detail="No ghost with that ID")
|
||||||
|
data = {"ghost": ghost, "user": current_user, "crumbs": [("ghosts", "/ghosts"), (ghost.id, False)]}
|
||||||
request.state.ghostforge = data | getattr(request.state, "ghostforge", {})
|
request.state.ghostforge = data | getattr(request.state, "ghostforge", {})
|
||||||
return ghost
|
return ghost
|
||||||
|
|
||||||
|
@ -70,18 +135,21 @@ async def update_ghost(
|
||||||
current_user: Annotated[User, Depends(get_current_user())],
|
current_user: Annotated[User, Depends(get_current_user())],
|
||||||
session: AsyncSession = Depends(get_session),
|
session: AsyncSession = Depends(get_session),
|
||||||
request: Request = None,
|
request: Request = None,
|
||||||
|
can_edit: bool = Depends(can_edit_ghost),
|
||||||
):
|
):
|
||||||
|
if not can_edit:
|
||||||
|
raise HTTPException(status_code=403, detail="You're not authorized to edit this ghost")
|
||||||
db_ghost = await session.get(Ghost, ghost_id)
|
db_ghost = await session.get(Ghost, ghost_id)
|
||||||
if db_ghost is None:
|
if db_ghost is None:
|
||||||
raise HTTPException(status_code=404, detail="No ghost with that ID")
|
raise HTTPException(status_code=404, detail="No ghost with that ID")
|
||||||
for k, v in ghost.dict().items():
|
for k, v in ghost.dict(exclude_unset=True).items():
|
||||||
setattr(db_ghost, k, v)
|
setattr(db_ghost, k, v)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
await session.refresh(db_ghost)
|
await session.refresh(db_ghost)
|
||||||
return db_ghost
|
return db_ghost
|
||||||
|
|
||||||
|
|
||||||
@gf.post("/ghosts")
|
@gf.post("/ghosts/new")
|
||||||
async def create_ghost(
|
async def create_ghost(
|
||||||
ghost: GhostCreate,
|
ghost: GhostCreate,
|
||||||
current_user: Annotated[User, Depends(get_current_user())],
|
current_user: Annotated[User, Depends(get_current_user())],
|
||||||
|
@ -89,17 +157,86 @@ async def create_ghost(
|
||||||
request: Request = None,
|
request: Request = None,
|
||||||
):
|
):
|
||||||
new_ghost = Ghost.from_orm(ghost)
|
new_ghost = Ghost.from_orm(ghost)
|
||||||
ghost.birthdate = ghost.birthdate.replace(tzinfo=timezone.utc)
|
new_ghost.owner_id = current_user.id
|
||||||
print(ghost.birthdate)
|
|
||||||
session.add(new_ghost)
|
session.add(new_ghost)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
await session.refresh(new_ghost)
|
await session.refresh(new_ghost)
|
||||||
return new_ghost
|
return new_ghost
|
||||||
|
|
||||||
|
|
||||||
@gf.get("/ghosts")
|
@gf.post("/ghosts")
|
||||||
async def read_users(
|
async def get_ghosts(
|
||||||
offset: int = 0, limit: int = Query(default=100, lte=100), session: Session = Depends(get_session)
|
current_user: Annotated[User, Depends(get_current_user())],
|
||||||
|
start: int = Form(None, alias="start"),
|
||||||
|
length: int = Form(None, alias="length"),
|
||||||
|
search: Optional[str] = Form(None, alias="search[value]"),
|
||||||
|
order_col: int = Form(0, alias="order[0][column]"),
|
||||||
|
order_dir: str = Form("asc", alias="order[0][dir]"),
|
||||||
|
session: AsyncSession = Depends(get_session),
|
||||||
|
request: Request = None,
|
||||||
):
|
):
|
||||||
ghosts = await session.execute(select(Ghost).offset(offset).limit(limit))
|
offset = start
|
||||||
return ghosts.scalars().all()
|
limit = offset + length
|
||||||
|
|
||||||
|
# Get the column name based on the order_col integer value
|
||||||
|
ghost_columns = [c.key for c in inspect(Ghost).c]
|
||||||
|
order_col_name = ghost_columns[order_col]
|
||||||
|
|
||||||
|
# Retrieve filtered ghosts from database
|
||||||
|
query = (
|
||||||
|
select(Ghost, User.email.label("owner_email")).offset(offset).limit(limit).join(User, Ghost.owner_id == User.id)
|
||||||
|
)
|
||||||
|
permission_filter = or_(
|
||||||
|
Ghost.owner_id == current_user.id,
|
||||||
|
exists(
|
||||||
|
select(GhostPermissions).where(
|
||||||
|
and_(GhostPermissions.ghost_id == Ghost.id, GhostPermissions.user_id == current_user.id)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
count = select(func.count(Ghost.id)).join(User, Ghost.owner_id == User.id).filter(permission_filter)
|
||||||
|
total_ghosts = (await session.execute(count)).scalar()
|
||||||
|
|
||||||
|
query = query.filter(permission_filter)
|
||||||
|
|
||||||
|
if search:
|
||||||
|
conditions = []
|
||||||
|
for col in ghost_columns:
|
||||||
|
column_attr = getattr(Ghost, col)
|
||||||
|
if isinstance(column_attr.type, (AutoString, Text)):
|
||||||
|
conditions.append(column_attr.ilike(f"%{search}%"))
|
||||||
|
query = query.where(or_(*conditions))
|
||||||
|
if order_dir == "asc":
|
||||||
|
query = query.order_by(asc(getattr(Ghost, order_col_name)))
|
||||||
|
else:
|
||||||
|
query = query.order_by(desc(getattr(Ghost, order_col_name)))
|
||||||
|
ghosts = (await session.execute(query)).all()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"recordsTotal": total_ghosts,
|
||||||
|
"recordsFiltered": total_ghosts,
|
||||||
|
"data": ghosts,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@gf.get("/ghosts")
|
||||||
|
@hj.html_or_json("ghosts/ghosts.html")
|
||||||
|
async def read_users(
|
||||||
|
current_user: Annotated[User, Depends(get_current_user())],
|
||||||
|
offset: int = 0,
|
||||||
|
limit: int = Query(default=100, lte=100),
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
request: Request = None,
|
||||||
|
):
|
||||||
|
subquery = (
|
||||||
|
select(GhostPermissions.ghost_id)
|
||||||
|
.where(GhostPermissions.user_id == current_user.id)
|
||||||
|
.union(select(Ghost.id).where(Ghost.owner_id == current_user.id))
|
||||||
|
.subquery()
|
||||||
|
)
|
||||||
|
query = await session.execute(select(Ghost).where(Ghost.id.in_(subquery)).offset(offset).limit(limit))
|
||||||
|
ghosts = query.scalars().all()
|
||||||
|
data = {"ghosts": ghosts, "user": current_user, "crumbs": [("ghosts", "/ghosts")]}
|
||||||
|
request.state.ghostforge = data | getattr(request.state, "ghostforge", {})
|
||||||
|
return ghosts
|
||||||
|
|
|
@ -52,8 +52,8 @@ class HtmlJson:
|
||||||
result = {"data": result[:]}
|
result = {"data": result[:]}
|
||||||
|
|
||||||
if hasattr(request.state, "ghostforge"):
|
if hasattr(request.state, "ghostforge"):
|
||||||
if "crumbs" in request.state.ghostforge:
|
for key, value in request.state.ghostforge.items():
|
||||||
result["crumbs"] = request.state.ghostforge["crumbs"]
|
result[key] = value
|
||||||
|
|
||||||
result.update({"request": request})
|
result.update({"request": request})
|
||||||
return templates.TemplateResponse(template, result)
|
return templates.TemplateResponse(template, result)
|
||||||
|
|
1
ghostforge/static/css/colReorder.dataTables.min.css
vendored
Normal file
1
ghostforge/static/css/colReorder.dataTables.min.css
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
table.DTCR_clonedTable.dataTable{position:absolute !important;background-color:rgba(255, 255, 255, 0.7);z-index:202}div.DTCR_pointer{width:1px;background-color:#0259c4;z-index:201}
|
0
ghostforge/static/css/dataTables.dataTables.min.css
vendored
Normal file
0
ghostforge/static/css/dataTables.dataTables.min.css
vendored
Normal file
1600
ghostforge/static/css/datatables.css
Normal file
1600
ghostforge/static/css/datatables.css
Normal file
File diff suppressed because it is too large
Load diff
1
ghostforge/static/css/fixedHeader.dataTables.min.css
vendored
Normal file
1
ghostforge/static/css/fixedHeader.dataTables.min.css
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
table.fixedHeader-floating{background-color:white}table.fixedHeader-floating.no-footer{border-bottom-width:0}table.fixedHeader-locked{position:absolute !important;background-color:white}@media print{table.fixedHeader-floating{display:none}}
|
|
@ -51,3 +51,13 @@ summary.left-nav::before {
|
||||||
details[open] summary.left-nav::before {
|
details[open] summary.left-nav::before {
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ghost_view_title {
|
||||||
|
padding: 0 .2rem;
|
||||||
|
padding-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghostforge-container {
|
||||||
|
padding-right: 5em;
|
||||||
|
padding-top: 1em;
|
||||||
|
}
|
||||||
|
|
1
ghostforge/static/css/jquery.dataTables.min.css
vendored
Normal file
1
ghostforge/static/css/jquery.dataTables.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
ghostforge/static/css/responsive.dataTables.min.css
vendored
Normal file
1
ghostforge/static/css/responsive.dataTables.min.css
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
table.dataTable.dtr-inline.collapsed>tbody>tr>td.child,table.dataTable.dtr-inline.collapsed>tbody>tr>th.child,table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty{cursor:default !important}table.dataTable.dtr-inline.collapsed>tbody>tr>td.child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th.child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty:before{display:none !important}table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control,table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control{position:relative;padding-left:30px;cursor:pointer}table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before{top:50%;left:5px;height:1em;width:1em;margin-top:-9px;display:block;position:absolute;color:white;border:.15em solid white;border-radius:1em;box-shadow:0 0 .2em #444;box-sizing:content-box;text-align:center;text-indent:0 !important;font-family:"Courier New",Courier,monospace;line-height:1em;content:"+";background-color:#31b131}table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td.dtr-control:before,table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th.dtr-control:before{content:"-";background-color:#d33333}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td.dtr-control,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th.dtr-control{padding-left:27px}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td.dtr-control:before,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th.dtr-control:before{left:4px;height:14px;width:14px;border-radius:14px;line-height:14px;text-indent:3px}table.dataTable.dtr-column>tbody>tr>td.dtr-control,table.dataTable.dtr-column>tbody>tr>th.dtr-control,table.dataTable.dtr-column>tbody>tr>td.control,table.dataTable.dtr-column>tbody>tr>th.control{position:relative;cursor:pointer}table.dataTable.dtr-column>tbody>tr>td.dtr-control:before,table.dataTable.dtr-column>tbody>tr>th.dtr-control:before,table.dataTable.dtr-column>tbody>tr>td.control:before,table.dataTable.dtr-column>tbody>tr>th.control:before{top:50%;left:50%;height:.8em;width:.8em;margin-top:-0.5em;margin-left:-0.5em;display:block;position:absolute;color:white;border:.15em solid white;border-radius:1em;box-shadow:0 0 .2em #444;box-sizing:content-box;text-align:center;text-indent:0 !important;font-family:"Courier New",Courier,monospace;line-height:1em;content:"+";background-color:#31b131}table.dataTable.dtr-column>tbody>tr.parent td.dtr-control:before,table.dataTable.dtr-column>tbody>tr.parent th.dtr-control:before,table.dataTable.dtr-column>tbody>tr.parent td.control:before,table.dataTable.dtr-column>tbody>tr.parent th.control:before{content:"-";background-color:#d33333}table.dataTable>tbody>tr.child{padding:.5em 1em}table.dataTable>tbody>tr.child:hover{background:transparent !important}table.dataTable>tbody>tr.child ul.dtr-details{display:inline-block;list-style-type:none;margin:0;padding:0}table.dataTable>tbody>tr.child ul.dtr-details>li{border-bottom:1px solid #efefef;padding:.5em 0}table.dataTable>tbody>tr.child ul.dtr-details>li:first-child{padding-top:0}table.dataTable>tbody>tr.child ul.dtr-details>li:last-child{border-bottom:none}table.dataTable>tbody>tr.child span.dtr-title{display:inline-block;min-width:75px;font-weight:bold}div.dtr-modal{position:fixed;box-sizing:border-box;top:0;left:0;height:100%;width:100%;z-index:100;padding:10em 1em}div.dtr-modal div.dtr-modal-display{position:absolute;top:0;left:0;bottom:0;right:0;width:50%;height:50%;overflow:auto;margin:auto;z-index:102;overflow:auto;background-color:#f5f5f7;border:1px solid black;border-radius:.5em;box-shadow:0 12px 30px rgba(0, 0, 0, 0.6)}div.dtr-modal div.dtr-modal-content{position:relative;padding:1em}div.dtr-modal div.dtr-modal-close{position:absolute;top:6px;right:6px;width:22px;height:22px;border:1px solid #eaeaea;background-color:#f9f9f9;text-align:center;border-radius:3px;cursor:pointer;z-index:12}div.dtr-modal div.dtr-modal-close:hover{background-color:#eaeaea}div.dtr-modal div.dtr-modal-background{position:fixed;top:0;left:0;right:0;bottom:0;z-index:101;background:rgba(0, 0, 0, 0.6)}@media screen and (max-width: 767px){div.dtr-modal div.dtr-modal-display{width:95%}}
|
1
ghostforge/static/css/searchBuilder.dataTables.min.css
vendored
Normal file
1
ghostforge/static/css/searchBuilder.dataTables.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
ghostforge/static/css/searchPanes.dataTables.min.css
vendored
Normal file
1
ghostforge/static/css/searchPanes.dataTables.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
ghostforge/static/css/stateRestore.dataTables.min.css
vendored
Normal file
1
ghostforge/static/css/stateRestore.dataTables.min.css
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
div.dtsr-confirmation,div.dtsr-creation{position:fixed;top:20%;left:50%;width:500px;background-color:white;margin-left:-250px;border-radius:6px;box-shadow:0 0 5px #555;border:2px solid #444;z-index:2003;box-sizing:border-box;padding:1em}div.dtsr-confirmation div.dtsr-confirmation-title-row,div.dtsr-creation div.dtsr-confirmation-title-row{text-align:left}div.dtsr-confirmation div.dtsr-confirmation-title-row h2,div.dtsr-creation div.dtsr-confirmation-title-row h2{border-bottom:0px;margin-top:0px;padding-top:0px}div.dtsr-confirmation div.dtsr-confirmation-text,div.dtsr-creation div.dtsr-confirmation-text{text-align:center}div.dtsr-confirmation div.dtsr-confirmation-buttons,div.dtsr-creation div.dtsr-confirmation-buttons{text-align:right;margin-top:1em}div.dtsr-confirmation div.dtsr-confirmation-buttons button.dtsr-confirmation-button,div.dtsr-creation div.dtsr-confirmation-buttons button.dtsr-confirmation-button{margin:0px}div.dtsr-confirmation div.dtsr-creation-text,div.dtsr-creation div.dtsr-creation-text{text-align:left;padding:0px;border:none}div.dtsr-confirmation div.dtsr-creation-text span,div.dtsr-creation div.dtsr-creation-text span{font-size:20px}div.dtsr-confirmation div.dtsr-creation-form div.dtsr-left,div.dtsr-confirmation div.dtsr-creation-form div.dtsr-right,div.dtsr-creation div.dtsr-creation-form div.dtsr-left,div.dtsr-creation div.dtsr-creation-form div.dtsr-right{display:inline-block;width:50%}div.dtsr-confirmation div.dtsr-creation-form div.dtsr-left,div.dtsr-creation div.dtsr-creation-form div.dtsr-left{text-align:right}div.dtsr-confirmation div.dtsr-creation-form div.dtsr-right,div.dtsr-confirmation div.dtsr-creation-form div.dtsr-name-row,div.dtsr-creation div.dtsr-creation-form div.dtsr-right,div.dtsr-creation div.dtsr-creation-form div.dtsr-name-row{text-align:left}div.dtsr-confirmation div.dtsr-creation-form div.dtsr-form-row label.dtsr-name-label,div.dtsr-creation div.dtsr-creation-form div.dtsr-form-row label.dtsr-name-label{width:33.3%;display:inline-block;text-align:right;padding-right:15px;padding-left:15px}div.dtsr-confirmation div.dtsr-creation-form div.dtsr-form-row input.dtsr-name-input,div.dtsr-creation div.dtsr-creation-form div.dtsr-form-row input.dtsr-name-input{width:66.6%;display:inline-block}div.dtsr-confirmation div.dtsr-creation-form div.dtsr-form-row input.dtsr-check-box,div.dtsr-creation div.dtsr-creation-form div.dtsr-form-row input.dtsr-check-box{margin-left:33.3%;margin-right:14px;display:inline-block}div.dtsr-confirmation div.dtsr-creation-form div.dtsr-form-row label.dtsr-toggle-title,div.dtsr-creation div.dtsr-creation-form div.dtsr-form-row label.dtsr-toggle-title{margin-right:-33.3%}div.dtsr-confirmation div.dtsr-confirmation-text,div.dtsr-creation div.dtsr-confirmation-text{text-align:left}div.dtsr-confirmation div.dtsr-confirmation-text label.dtsr-name-label,div.dtsr-creation div.dtsr-confirmation-text label.dtsr-name-label{width:auto;display:inline-block;text-align:right;padding-right:15px}div.dtsr-confirmation div.dtsr-confirmation-text input.dtsr-name-input,div.dtsr-creation div.dtsr-confirmation-text input.dtsr-name-input{width:66.6%;display:inline-block}div.dtsr-confirmation div.dtsr-confirmation-text input.dtsr-check-box,div.dtsr-creation div.dtsr-confirmation-text input.dtsr-check-box{margin-left:33.3%;margin-right:14px;display:inline-block}div.dtsr-confirmation div.dtsr-modal-foot,div.dtsr-creation div.dtsr-modal-foot{text-align:right;padding-top:10px}div.dtsr-confirmation span.dtsr-modal-error,div.dtsr-creation span.dtsr-modal-error{color:red;font-size:.9em}div.dtsr-creation{top:10%}div.dtsr-form-row{padding:10px}div.dtsr-check-row{padding-top:0px}div.dtsr-creation-text{padding:10px}div.dtsr-popover-close{position:absolute;top:10px;right:10px;width:22px;height:22px;border:1px solid #eaeaea;background-color:#f9f9f9;text-align:center;border-radius:3px;cursor:pointer;z-index:12}div.dtsr-background{z-index:2002;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0, 0, 0, 0.7);background:radial-gradient(ellipse farthest-corner at center, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.7) 100%)}div.dt-button-collection h3{text-align:center;margin-top:4px;margin-bottom:8px;font-size:1.5em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}div.dt-button-collection span.dtsr-emptyStates{border-radius:5px;display:inline-block;line-height:1.6em;white-space:nowrap;text-align:center;vertical-align:middle;width:100%;padding-bottom:7px;padding-top:3px}div.dt-button-collection h3{font-size:1.1em}div.dtsr-creation-form div.dtsr-form-row input.dtsr-name-input{width:57% !important;padding:5px 4px;border:1px solid #aaa;border-radius:3px}div.dtsr-creation-form div.dtsr-form-row input.dtsr-check-box{margin-left:calc(33.3% + 30px) !important}div.dtsr-creation-form div.dtsr-form-row label.dtsr-toggle-title{margin-right:calc(-33.3% - 30px) !important}
|
4
ghostforge/static/js/colReorder.dataTables.min.js
vendored
Normal file
4
ghostforge/static/js/colReorder.dataTables.min.js
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/*! DataTables styling wrapper for ColReorder
|
||||||
|
* © SpryMedia Ltd - datatables.net/license
|
||||||
|
*/
|
||||||
|
!function(n){var o,d;"function"==typeof define&&define.amd?define(["jquery","datatables.net-dt","datatables.net-colreorder"],function(e){return n(e,window,document)}):"object"==typeof exports?(o=require("jquery"),d=function(e,t){t.fn.dataTable||require("datatables.net-dt")(e,t),t.fn.dataTable.ColReorder||require("datatables.net-colreorder")(e,t)},"undefined"!=typeof window?module.exports=function(e,t){return e=e||window,t=t||o(e),d(e,t),n(t,0,e.document)}:(d(window,o),module.exports=n(o,window,window.document))):n(jQuery,window,document)}(function(e,t,n,o){"use strict";return e.fn.dataTable});
|
4
ghostforge/static/js/dataTables.colReorder.min.js
vendored
Normal file
4
ghostforge/static/js/dataTables.colReorder.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
ghostforge/static/js/dataTables.dataTables.min.js
vendored
Normal file
4
ghostforge/static/js/dataTables.dataTables.min.js
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/*! DataTables styling integration
|
||||||
|
* ©2018 SpryMedia Ltd - datatables.net/license
|
||||||
|
*/
|
||||||
|
!function(t){var o,d;"function"==typeof define&&define.amd?define(["jquery","datatables.net"],function(e){return t(e,window,document)}):"object"==typeof exports?(o=require("jquery"),d=function(e,n){n.fn.dataTable||require("datatables.net")(e,n)},"undefined"!=typeof window?module.exports=function(e,n){return e=e||window,n=n||o(e),d(e,n),t(n,0,e.document)}:(d(window,o),module.exports=t(o,window,window.document))):t(jQuery,window,document)}(function(e,n,t,o){"use strict";return e.fn.dataTable});
|
4
ghostforge/static/js/dataTables.fixedHeader.min.js
vendored
Normal file
4
ghostforge/static/js/dataTables.fixedHeader.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
ghostforge/static/js/dataTables.responsive.min.js
vendored
Normal file
4
ghostforge/static/js/dataTables.responsive.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
ghostforge/static/js/dataTables.searchBuilder.min.js
vendored
Normal file
4
ghostforge/static/js/dataTables.searchBuilder.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
ghostforge/static/js/dataTables.searchPanes.min.js
vendored
Normal file
4
ghostforge/static/js/dataTables.searchPanes.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
ghostforge/static/js/dataTables.stateRestore.min.js
vendored
Normal file
4
ghostforge/static/js/dataTables.stateRestore.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
ghostforge/static/js/fixedHeader.dataTables.min.js
vendored
Normal file
4
ghostforge/static/js/fixedHeader.dataTables.min.js
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/*! DataTables styling wrapper for FixedHeader
|
||||||
|
* © SpryMedia Ltd - datatables.net/license
|
||||||
|
*/
|
||||||
|
!function(n){var d,a;"function"==typeof define&&define.amd?define(["jquery","datatables.net-dt","datatables.net-fixedheader"],function(e){return n(e,window,document)}):"object"==typeof exports?(d=require("jquery"),a=function(e,t){t.fn.dataTable||require("datatables.net-dt")(e,t),t.fn.dataTable.FixedHeader||require("datatables.net-fixedheader")(e,t)},"undefined"!=typeof window?module.exports=function(e,t){return e=e||window,t=t||d(e),a(e,t),n(t,0,e.document)}:(a(window,d),module.exports=n(d,window,window.document))):n(jQuery,window,document)}(function(e,t,n,d){"use strict";return e.fn.dataTable});
|
16
ghostforge/static/js/ghost.js
Normal file
16
ghostforge/static/js/ghost.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
function switchTab(tab) {
|
||||||
|
var i;
|
||||||
|
var x = document.getElementsByClassName("profile_tab");
|
||||||
|
var a = document.getElementsByClassName("profile_tab_link");
|
||||||
|
for (i = 0; i < x.length; i++) {
|
||||||
|
x[i].style.display = "none";
|
||||||
|
}
|
||||||
|
for (i = 0; i < a.length; i++) {
|
||||||
|
if (a[i].id.endsWith(tab)) {
|
||||||
|
a[i].classList.add("active");
|
||||||
|
} else {
|
||||||
|
a[i].classList.remove("active");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.getElementById(tab).style.display = "block";
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
// Save and restore navigation menu state from local storage.
|
// Save and restore navigation menu state from local storage.
|
||||||
|
|
||||||
function saveMenuState() {
|
function saveMenuState() {
|
||||||
var leftNavigation = document.getElementById("left-navigation");
|
var leftNavigation = document.getElementById("left-navigation");
|
||||||
if (!leftNavigation) {
|
if (!leftNavigation) {
|
||||||
|
@ -77,23 +76,41 @@ function modal_focus(modal_name) {
|
||||||
|
|
||||||
// Hotkeys
|
// Hotkeys
|
||||||
document.addEventListener("keydown", function (event) {
|
document.addEventListener("keydown", function (event) {
|
||||||
|
if (event.key === "Escape" || event.keyCode === 27) {
|
||||||
|
window.location.hash = "#";
|
||||||
|
}
|
||||||
// Ctrl/Cmd+G for search modal
|
// Ctrl/Cmd+G for search modal
|
||||||
if (event.key == "g") {
|
else if (event.key == "g") {
|
||||||
const nav_search_button = document.getElementById("nav_search_button");
|
const nav_search_button = document.getElementById("nav_search_button");
|
||||||
const search_box = document.getElementById("search_string");
|
const search_box = document.getElementById("search_string");
|
||||||
console.log(search_box);
|
|
||||||
if (event.ctrlKey) {
|
if (event.ctrlKey) {
|
||||||
event.preventDefault(); // prevent default Ctrl+G behavior (find in page)
|
event.preventDefault();
|
||||||
nav_search_button.click();
|
nav_search_button.click();
|
||||||
search_box.focus();
|
search_box.focus();
|
||||||
}
|
}
|
||||||
// Add support for Apple+G on Macs
|
|
||||||
if (event.metaKey) {
|
if (event.metaKey) {
|
||||||
event.preventDefault(); // prevent default Apple+G behavior (bookmark)
|
event.preventDefault();
|
||||||
nav_search_button.click();
|
nav_search_button.click();
|
||||||
search_box.focus();
|
search_box.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Ctrl/Cmd+N for new ghost modal
|
||||||
|
else if (event.key == "n") {
|
||||||
|
const nav_new_button = document.getElementById("nav_new_button");
|
||||||
|
const new_ghost_box = document.getElementById("new_firstname");
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
nav_new_button.click();
|
||||||
|
new_ghost_box.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.metaKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
nav_new_button.click();
|
||||||
|
new_ghost_box.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Run various onload/unload functions since script is loaded before elements are
|
// Run various onload/unload functions since script is loaded before elements are
|
||||||
|
|
2
ghostforge/static/js/jquery-3.7.0.min.js
vendored
Normal file
2
ghostforge/static/js/jquery-3.7.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
ghostforge/static/js/jquery.dataTables.min.js
vendored
Normal file
4
ghostforge/static/js/jquery.dataTables.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
ghostforge/static/js/responsive.dataTables.min.js
vendored
Normal file
4
ghostforge/static/js/responsive.dataTables.min.js
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/*! DataTables styling wrapper for Responsive
|
||||||
|
* © SpryMedia Ltd - datatables.net/license
|
||||||
|
*/
|
||||||
|
!function(t){var o,d;"function"==typeof define&&define.amd?define(["jquery","datatables.net-dt","datatables.net-responsive"],function(e){return t(e,window,document)}):"object"==typeof exports?(o=require("jquery"),d=function(e,n){n.fn.dataTable||require("datatables.net-dt")(e,n),n.fn.dataTable.Responsive||require("datatables.net-responsive")(e,n)},"undefined"!=typeof window?module.exports=function(e,n){return e=e||window,n=n||o(e),d(e,n),t(n,0,e.document)}:(d(window,o),module.exports=t(o,window,window.document))):t(jQuery,window,document)}(function(e,n,t,o){"use strict";return e.fn.dataTable});
|
4
ghostforge/static/js/searchBuilder.dataTables.min.js
vendored
Normal file
4
ghostforge/static/js/searchBuilder.dataTables.min.js
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/*! DataTables integration for DataTables' SearchBuilder
|
||||||
|
* © SpryMedia Ltd - datatables.net/license
|
||||||
|
*/
|
||||||
|
!function(n){var d,a;"function"==typeof define&&define.amd?define(["jquery","datatables.net-dt","datatables.net-searchbuilder"],function(e){return n(e,window,document)}):"object"==typeof exports?(d=require("jquery"),a=function(e,t){t.fn.dataTable||require("datatables.net-dt")(e,t),t.fn.dataTable.SearchBuilder||require("datatables.net-searchbuilder")(e,t)},"undefined"!=typeof window?module.exports=function(e,t){return e=e||window,t=t||d(e),a(e,t),n(t,0,e.document)}:(a(window,d),module.exports=n(d,window,window.document))):n(jQuery,window,document)}(function(e,t,n,d){"use strict";return e.fn.dataTable});
|
4
ghostforge/static/js/searchPanes.dataTables.min.js
vendored
Normal file
4
ghostforge/static/js/searchPanes.dataTables.min.js
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/*! Bootstrap integration for DataTables' SearchPanes
|
||||||
|
* © SpryMedia Ltd - datatables.net/license
|
||||||
|
*/
|
||||||
|
!function(t){var a,d;"function"==typeof define&&define.amd?define(["jquery","datatables.net-dt","datatables.net-searchpanes"],function(e){return t(e,window,document)}):"object"==typeof exports?(a=require("jquery"),d=function(e,n){n.fn.dataTable||require("datatables.net-dt")(e,n),n.fn.dataTable.SearchPanes||require("datatables.net-searchpanes")(e,n)},"undefined"!=typeof window?module.exports=function(e,n){return e=e||window,n=n||a(e),d(e,n),t(n,0,e.document)}:(d(window,a),module.exports=t(a,window,window.document))):t(jQuery,window,document)}(function(e,n,t,a){"use strict";return e.fn.dataTable});
|
4
ghostforge/static/js/stateRestore.dataTables.min.js
vendored
Normal file
4
ghostforge/static/js/stateRestore.dataTables.min.js
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/*! Bootstrap integration for DataTables' StateRestore
|
||||||
|
* © SpryMedia Ltd - datatables.net/license
|
||||||
|
*/
|
||||||
|
!function(n){var o,d;"function"==typeof define&&define.amd?define(["jquery","datatables.net-dt","datatables.net-staterestore"],function(e){return n(e,window,document)}):"object"==typeof exports?(o=require("jquery"),d=function(e,t){t.fn.dataTable||require("datatables.net-dt")(e,t),t.fn.dataTable.StateRestore||require("datatables.net-staterestore")(e,t)},"undefined"!=typeof window?module.exports=function(e,t){return e=e||window,t=t||o(e),d(e,t),n(t,0,e.document)}:(d(window,o),module.exports=n(o,window,window.document))):n(jQuery,window,document)}(function(e,t,n,o){"use strict";return e.fn.dataTable});
|
|
@ -15,12 +15,7 @@ templates.env.globals["gf_repository_url"] = "https://github.com/DarrylNixon/gho
|
||||||
# Same, but build the navbar from an ordered dictionary for centralization.
|
# Same, but build the navbar from an ordered dictionary for centralization.
|
||||||
# Since 3.7 (we require >= 3.9), dicts are guaranteed ordered as inserted.
|
# Since 3.7 (we require >= 3.9), dicts are guaranteed ordered as inserted.
|
||||||
templates.env.globals["gf_navbar"] = {
|
templates.env.globals["gf_navbar"] = {
|
||||||
"Ghosts": {
|
"Ghosts": {"Dashboard": "/dashboard", "Browse": "/ghosts"},
|
||||||
"Dashboard": "/dashboard",
|
|
||||||
"New": "/ghosts/new",
|
|
||||||
"Active": "/ghosts",
|
|
||||||
"Archived": "/ghosts/archive",
|
|
||||||
},
|
|
||||||
"Research": {
|
"Research": {
|
||||||
"Guidebook": "/guidebook",
|
"Guidebook": "/guidebook",
|
||||||
"Cheat Sheet": "/cheatsheet",
|
"Cheat Sheet": "/cheatsheet",
|
||||||
|
|
|
@ -10,20 +10,24 @@
|
||||||
<link href="{{ url_for('static', path='/css/spectre-exp.min.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', path='/css/spectre-exp.min.css') }}" rel="stylesheet">
|
||||||
<link href="{{ url_for('static', path='/css/spectre-icons.min.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', path='/css/spectre-icons.min.css') }}" rel="stylesheet">
|
||||||
<link href="{{ url_for('static', path='/css/ghostforge.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', path='/css/ghostforge.css') }}" rel="stylesheet">
|
||||||
|
{% block css %}{% endblock css %}
|
||||||
<script src="{{ url_for('static', path='/js/ghostforge.js') }}"></script>
|
<script src="{{ url_for('static', path='/js/ghostforge.js') }}"></script>
|
||||||
|
{% block js %}{% endblock js %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="off-canvas off-canvas-sidebar-show d-flex">
|
<div class="off-canvas off-canvas-sidebar-show d-flex">
|
||||||
{% include "navigation/side.html" %}
|
{% include "navigation/side.html" %}
|
||||||
<div class="off-canvas-content">
|
<div class="off-canvas-content">
|
||||||
<div class="container" style="padding-right: 5em; padding-top: 1em;">
|
<div class="container ghostforge-container">
|
||||||
{% include "navigation/top.html" %}
|
{% include "navigation/top.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% include "modals/modals.html" %}
|
||||||
</body>
|
</body>
|
||||||
|
{% block bottomjs %}{% endblock bottomjs %}
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -3,14 +3,91 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column col-12">
|
<div class="column col-12">
|
||||||
<h1>Words!</h1>
|
<h1><span class="text-light bg-dark ghost_view_title">{% block title %}
|
||||||
|
{{
|
||||||
|
ghost.first_name }} {{
|
||||||
|
ghost.middle_name
|
||||||
|
}} {{
|
||||||
|
ghost.last_name }}{% endblock title %}</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<h5>[age] year old American Male</h5>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column col-3"></div>
|
<div class="column col-8">
|
||||||
<div class="column col-6">
|
{% include "ghosts/tabs/tabs.html" %}
|
||||||
<h5>More words.</h5>
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<button class="btn btn-primary float-right">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column col-4">
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<div class="panel-title h6">Notes</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="tile">
|
||||||
|
<div class="tile-icon">
|
||||||
|
<figure class="avatar"><img src="../img/avatar-1.png" alt="Avatar"></figure>
|
||||||
|
</div>
|
||||||
|
<div class="tile-content">
|
||||||
|
<p class="tile-title text-bold">Thor Odinson</p>
|
||||||
|
<p class="tile-subtitle">Earth's Mightiest Heroes joined forces to take on threats that were too
|
||||||
|
big for any one hero to tackle...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tile">
|
||||||
|
<div class="tile-icon">
|
||||||
|
<figure class="avatar"><img src="../img/avatar-2.png" alt="Avatar"></figure>
|
||||||
|
</div>
|
||||||
|
<div class="tile-content">
|
||||||
|
<p class="tile-title text-bold">Bruce Banner</p>
|
||||||
|
<p class="tile-subtitle">The Strategic Homeland Intervention, Enforcement, and Logistics
|
||||||
|
Division...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tile">
|
||||||
|
<div class="tile-icon">
|
||||||
|
<figure class="avatar" data-initial="TS"></figure>
|
||||||
|
</div>
|
||||||
|
<div class="tile-content">
|
||||||
|
<p class="tile-title text-bold">Tony Stark</p>
|
||||||
|
<p class="tile-subtitle">Earth's Mightiest Heroes joined forces to take on threats that were too
|
||||||
|
big for any one hero to tackle...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tile">
|
||||||
|
<div class="tile-icon">
|
||||||
|
<figure class="avatar"><img src="../img/avatar-4.png" alt="Avatar"></figure>
|
||||||
|
</div>
|
||||||
|
<div class="tile-content">
|
||||||
|
<p class="tile-title text-bold">Steve Rogers</p>
|
||||||
|
<p class="tile-subtitle">The Strategic Homeland Intervention, Enforcement, and Logistics
|
||||||
|
Division...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tile">
|
||||||
|
<div class="tile-icon">
|
||||||
|
<figure class="avatar"><img src="../img/avatar-3.png" alt="Avatar"></figure>
|
||||||
|
</div>
|
||||||
|
<div class="tile-content">
|
||||||
|
<p class="tile-title text-bold">Natasha Romanoff</p>
|
||||||
|
<p class="tile-subtitle">Earth's Mightiest Heroes joined forces to take on threats that were too
|
||||||
|
big for any one hero to tackle...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-footer">
|
||||||
|
<div class="input-group">
|
||||||
|
<input class="form-input" type="text" placeholder="Hello">
|
||||||
|
<button class="btn btn-primary input-group-btn">Send</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column col-3"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<script src="{{ url_for('static', path='/js/ghost.js') }}"></script>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
94
ghostforge/templates/ghosts/ghosts.html
Normal file
94
ghostforge/templates/ghosts/ghosts.html
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
<script src="{{ url_for('static', path='/js/jquery-3.7.0.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/colReorder.dataTables.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/dataTables.searchPanes.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/dataTables.colReorder.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/dataTables.stateRestore.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/responsive.dataTables.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/dataTables.dataTables.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/fixedHeader.dataTables.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/searchBuilder.dataTables.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/dataTables.fixedHeader.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/searchPanes.dataTables.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/dataTables.responsive.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/stateRestore.dataTables.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/dataTables.searchBuilder.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/jquery.dataTables.min.js') }}"></script>
|
||||||
|
{% endblock js %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
<link href="{{ url_for('static', path='/css/datatables.css') }}" rel="stylesheet">
|
||||||
|
<link href="{{ url_for('static', path='/css/colReorder.datatables.min.css') }}" rel="stylesheet">
|
||||||
|
<link href="{{ url_for('static', path='/css/dataTables.datatables.min.css') }}" rel="stylesheet">
|
||||||
|
<link href="{{ url_for('static', path='/css/fixedHeader.datatables.min.css') }}" rel="stylesheet">
|
||||||
|
<link href="{{ url_for('static', path='/css/jquery.datatables.min.css') }}" rel="stylesheet">
|
||||||
|
<link href="{{ url_for('static', path='/css/responsive.datatables.min.css') }}" rel="stylesheet">
|
||||||
|
<link href="{{ url_for('static', path='/css/colReorder.datatables.min.css') }}" rel="stylesheet">
|
||||||
|
<link href="{{ url_for('static', path='/css/stateRestore.datatables.min.css') }}" rel="stylesheet">
|
||||||
|
{% endblock css %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column col-12">
|
||||||
|
<h1>Browse Ghosts</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column col-12">
|
||||||
|
<table id="ghosts-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>First</th>
|
||||||
|
<th>Middle</th>
|
||||||
|
<th>Last</th>
|
||||||
|
<th>Birthdate</th>
|
||||||
|
<th>Owner</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
||||||
|
|
||||||
|
{% block bottomjs %}
|
||||||
|
<script>
|
||||||
|
$(document).ready(function () {
|
||||||
|
$('#ghosts-table').DataTable({
|
||||||
|
"processing": true,
|
||||||
|
"serverSide": true,
|
||||||
|
"ajax": {
|
||||||
|
"url": "/ghosts",
|
||||||
|
"type": "POST",
|
||||||
|
"dataType": "json"
|
||||||
|
},
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"data": "Ghost.id",
|
||||||
|
"render": function (data, type, row, meta) {
|
||||||
|
return '<a href="/ghosts/' + data + '">' + data + '</a>';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ "data": "Ghost.first_name" },
|
||||||
|
{ "data": "Ghost.middle_name" },
|
||||||
|
{ "data": "Ghost.last_name" },
|
||||||
|
{
|
||||||
|
"data": "Ghost.birthdate",
|
||||||
|
"render": function (data, type, row, meta) {
|
||||||
|
var date = new Date(data);
|
||||||
|
var day = date.getDate().toString().padStart(2, '0');
|
||||||
|
var month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||||
|
var year = date.getFullYear().toString();
|
||||||
|
return month + '/' + day + '/' + year;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ "data": "owner_email" },
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock bottomjs %}
|
27
ghostforge/templates/ghosts/tabs/persona/identity.html
Normal file
27
ghostforge/templates/ghosts/tabs/persona/identity.html
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<h5>Identity</h5>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label form-inline">First Name
|
||||||
|
<input class="form-input" type="text" id="firstname" placeholder="First">
|
||||||
|
</label>
|
||||||
|
<label class="form-label form-inline">Middle Name
|
||||||
|
<input class="form-input" type="text" id="middlename" placeholder="Middle">
|
||||||
|
</label>
|
||||||
|
<label class="form-label form-inline">Last Name
|
||||||
|
<input class="form-input" type="text" id="lastname" placeholder="Last">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Ethnicities
|
||||||
|
<div class="form-group">
|
||||||
|
<select class="form-select form-inline" multiple>
|
||||||
|
<option>German</option>
|
||||||
|
<option>Irish</option>
|
||||||
|
<option>Mexican</option>
|
||||||
|
<option>Canadian</option>
|
||||||
|
<option>American</option>
|
||||||
|
<option>British</option>
|
||||||
|
<option>Chinese</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
10
ghostforge/templates/ghosts/tabs/persona/persona.html
Normal file
10
ghostforge/templates/ghosts/tabs/persona/persona.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<div id="Persona" class="profile_tab">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title h5">Persona</div>
|
||||||
|
<div class="card-subtitle text-gray">"i don't even know who you are"</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% include "ghosts/tabs/persona/top.html" %}
|
||||||
|
{% include "ghosts/tabs/persona/identity.html" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
20
ghostforge/templates/ghosts/tabs/persona/top.html
Normal file
20
ghostforge/templates/ghosts/tabs/persona/top.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">
|
||||||
|
Owner
|
||||||
|
<input class="form-input" type="text" id="owner" placeholder="Owner GUID" value="{{ owner_username }}">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="popover popover-top">
|
||||||
|
<h6>Completion</h6>
|
||||||
|
<div class="popover-container">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<small><strong>Completion</strong> measures how many "important" fields are filled
|
||||||
|
to backstop this
|
||||||
|
persona. It doesn't check values for efficacy, just that they have
|
||||||
|
values.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<meter class="meter" value="60" min="0" max="100" low="30" high="80" style="margin-bottom: 1em;"></meter>
|
43
ghostforge/templates/ghosts/tabs/tabs.html
Normal file
43
ghostforge/templates/ghosts/tabs/tabs.html
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<ul class="tab tab-block">
|
||||||
|
<li class="tab-item">
|
||||||
|
<a href="#" onclick="switchTab('Persona')" class="profile_tab_link active" id="TabPersona">Persona</a>
|
||||||
|
</li>
|
||||||
|
<li class="tab-item">
|
||||||
|
<a href="#" onclick="switchTab('Credentials')" class="profile_tab_link" id="TabCredentials">Credentials</a>
|
||||||
|
</li>
|
||||||
|
<li class="tab-item">
|
||||||
|
<a href="#" onclick="switchTab('Acquisitions')" class="profile_tab_link" id="TabAcquisitions">Acquisitions</a>
|
||||||
|
</li>
|
||||||
|
<li class="tab-item">
|
||||||
|
<a href="#" onclick="switchTab('Meta')" class="profile_tab_link" id="TabMeta">Meta</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<form>
|
||||||
|
<div class="card">
|
||||||
|
{% include "ghosts/tabs/persona/persona.html" %}
|
||||||
|
|
||||||
|
<div id="Credentials" class="profile_tab" style="display:none">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title h5">Credentials</div>
|
||||||
|
<div class="card-subtitle text-gray">"my voice is my passport, verify me"</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">Credentials are very important</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="Acquisitions" class="profile_tab" style="display:none">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title h5">Acquisitions</div>
|
||||||
|
<div class="card-subtitle text-gray">"if you can't hack it, buy it"</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">Acquisitions are very important</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="History" class="profile_tab" style="display:none">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title h5">Meta</div>
|
||||||
|
<div class="card-subtitle text-gray">"now where was i..."</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">History is very important</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
2
ghostforge/templates/modals/modals.html
Normal file
2
ghostforge/templates/modals/modals.html
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
{% include "modals/search.html" %}
|
||||||
|
{% include "modals/new_ghost.html" %}
|
28
ghostforge/templates/modals/new_ghost.html
Normal file
28
ghostforge/templates/modals/new_ghost.html
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<div class="modal modal-sm" id="new-ghost-modal"><a class="modal-overlay" href="#" aria-label="Close"></a>
|
||||||
|
<div class="modal-container" role="document">
|
||||||
|
<div class="modal-header"><a class="btn btn-clear float-right" href="#" aria-label="Close"></a>
|
||||||
|
<div class="modal-title h5">New Ghost</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="content">
|
||||||
|
<form>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">
|
||||||
|
<h6>First Name</h6>
|
||||||
|
<input class="form-input" id="new_firstname" type="text" placeholder="First">
|
||||||
|
</label>
|
||||||
|
<label class="form-label">
|
||||||
|
<h6>Last Name</h6>
|
||||||
|
<input class="form-input" id="new_lastname" type="text" placeholder="Last">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn">Randomize</button>
|
||||||
|
<button class="btn btn-primary">Create Ghost</button>
|
||||||
|
<a class="btn btn-link" href="#modals-sizes" aria-label="Close">Close</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -1,9 +1,3 @@
|
||||||
<div align="center">
|
|
||||||
<a class="btn btn-primary btn-sm" href="#search-modal" id="nav_search_button">
|
|
||||||
<i class="icon icon-search"></i>
|
|
||||||
⌘-g
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="modal modal-sm" id="search-modal"><a class="modal-overlay" href="#modals-sizes" aria-label="Close"></a>
|
<div class="modal modal-sm" id="search-modal"><a class="modal-overlay" href="#modals-sizes" aria-label="Close"></a>
|
||||||
<div class="modal-container" role="document">
|
<div class="modal-container" role="document">
|
||||||
<div class="modal-header"><a class="btn btn-clear float-right" href="#modals-sizes" aria-label="Close"></a>
|
<div class="modal-header"><a class="btn btn-clear float-right" href="#modals-sizes" aria-label="Close"></a>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<a class="off-canvas-toggle btn btn-primary btn-action" href="#left-navigation"><i class="icon icon-menu"></i></a>
|
<a class="off-canvas-toggle btn btn-primary btn-action" href="#left-navigation"><i class="icon icon-menu"></i></a>
|
||||||
<div id="left-navigation" class="off-canvas-sidebar">
|
<div id="left-navigation" class="off-canvas-sidebar">
|
||||||
<div class="navigation-menu">
|
<div class="navigation-menu">
|
||||||
<a href="/" class="logo">
|
<a href="/" class="logo" style="display: block;">
|
||||||
<img src="{{ url_for('static', path='/img/ghostforge-sidebar.png') }}" class="brand">
|
<img src="{{ url_for('static', path='/img/ghostforge-sidebar.png') }}" class="brand">
|
||||||
</a>
|
</a>
|
||||||
<div>{% include "modals/search.html" %}</div>
|
{% include "navigation/side_buttons.html" %}
|
||||||
<div class="nav-menus">
|
<div class="nav-menus">
|
||||||
{% for key, value in gf_navbar.items() %}
|
{% for key, value in gf_navbar.items() %}
|
||||||
<details id="{{ key }}" , class="accordion">
|
<details id="{{ key }}" , class="accordion">
|
||||||
|
|
14
ghostforge/templates/navigation/side_buttons.html
Normal file
14
ghostforge/templates/navigation/side_buttons.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<div class="text-center">
|
||||||
|
<div style="display: inline-block;">
|
||||||
|
<a class="btn btn-primary btn-sm" href="#search-modal" id="nav_search_button">
|
||||||
|
<i class="icon icon-search"></i>
|
||||||
|
⌘-g
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div style="display: inline-block;">
|
||||||
|
<a class="btn btn-primary btn-sm" href="#new-ghost-modal" id="nav_new_button">
|
||||||
|
<i class="icon icon-plus"></i>
|
||||||
|
⌘-n
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -2,7 +2,7 @@
|
||||||
<section class="navbar-section">
|
<section class="navbar-section">
|
||||||
{% if crumbs %}
|
{% if crumbs %}
|
||||||
<ul class="breadcrumb">
|
<ul class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><kbd>{{ user.email.split('@')[0] }}</kbd></li>
|
<li class="breadcrumb-item"><kbd>{{ user.username }}</kbd></li>
|
||||||
{% for name, url in crumbs %}
|
{% for name, url in crumbs %}
|
||||||
<li class="breadcrumb-item">
|
<li class="breadcrumb-item">
|
||||||
{% if url %}
|
{% if url %}
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
alt="Avatar">
|
alt="Avatar">
|
||||||
</div>
|
</div>
|
||||||
<div class="tile-content">
|
<div class="tile-content">
|
||||||
{{ user.email }}
|
{{ user.username }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -25,15 +25,15 @@ gf = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
class UserRead(schemas.BaseUser[uuid.UUID]):
|
class UserRead(schemas.BaseUser[uuid.UUID]):
|
||||||
pass
|
username: str
|
||||||
|
|
||||||
|
|
||||||
class UserCreate(schemas.BaseUserCreate):
|
class UserCreate(schemas.BaseUserCreate):
|
||||||
pass
|
username: str
|
||||||
|
|
||||||
|
|
||||||
class UserUpdate(schemas.BaseUserUpdate):
|
class UserUpdate(schemas.BaseUserUpdate):
|
||||||
pass
|
username: str
|
||||||
|
|
||||||
|
|
||||||
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
||||||
|
|
Loading…
Reference in a new issue