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 create_async_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
# from fastapi_users.db import SQLAlchemyBaseUserTableUUID
|
||||
# from fastapi_users.db import SQLAlchemyUserDatabase
|
||||
# from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
# from sqlmodel import SQLModel
|
||||
from sqlmodel import Field
|
||||
|
||||
|
||||
# class Base(DeclarativeBase):
|
||||
# pass
|
||||
|
||||
|
||||
# Create a "Base" class for use with fastapi-users-db-sqlmodel and sqlalchemy
|
||||
class Base(SQLModelBaseUserDB):
|
||||
pass
|
||||
|
||||
|
||||
# Create a "User" class for user with fastapi-users-db-sqlmodel
|
||||
class User(Base, table=True):
|
||||
pass
|
||||
|
||||
|
||||
# class User(SQLAlchemyBaseUserTableUUID, Base):
|
||||
# pass
|
||||
username: str = Field(unique=True)
|
||||
|
||||
|
||||
database_url = (
|
||||
|
|
|
@ -1,16 +1,27 @@
|
|||
import uuid
|
||||
from datetime import datetime
|
||||
from datetime import timezone
|
||||
from operator import truth
|
||||
from typing import Annotated
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
from fastapi import Form
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Query
|
||||
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 Text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlmodel import and_
|
||||
from sqlmodel import AutoString
|
||||
from sqlmodel import Column
|
||||
from sqlmodel import Field
|
||||
from sqlmodel import SQLModel
|
||||
|
@ -35,6 +46,7 @@ class Ghost(SQLModel, table=True):
|
|||
TIMESTAMP(timezone=True),
|
||||
)
|
||||
)
|
||||
owner_id: Optional[uuid.UUID] = Field(foreign_key="user.id")
|
||||
|
||||
|
||||
class GhostCreate(Ghost, table=False):
|
||||
|
@ -49,6 +61,49 @@ class GhostRead(Ghost, table=False):
|
|||
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}")
|
||||
@hj.html_or_json("ghost.html")
|
||||
async def read_ghost(
|
||||
|
@ -56,9 +111,19 @@ async def read_ghost(
|
|||
current_user: Annotated[User, Depends(get_current_user())],
|
||||
session: AsyncSession = Depends(get_session),
|
||||
request: Request = None,
|
||||
can_view: bool = Depends(can_view_ghost),
|
||||
):
|
||||
ghost = await session.get(Ghost, ghost_id)
|
||||
data = {"crumbs": [("ghosts", "/ghosts"), (ghost.id, False)]}
|
||||
if not can_view:
|
||||
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", {})
|
||||
return ghost
|
||||
|
||||
|
@ -70,18 +135,21 @@ async def update_ghost(
|
|||
current_user: Annotated[User, Depends(get_current_user())],
|
||||
session: AsyncSession = Depends(get_session),
|
||||
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)
|
||||
if db_ghost is None:
|
||||
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)
|
||||
await session.commit()
|
||||
await session.refresh(db_ghost)
|
||||
return db_ghost
|
||||
|
||||
|
||||
@gf.post("/ghosts")
|
||||
@gf.post("/ghosts/new")
|
||||
async def create_ghost(
|
||||
ghost: GhostCreate,
|
||||
current_user: Annotated[User, Depends(get_current_user())],
|
||||
|
@ -89,17 +157,86 @@ async def create_ghost(
|
|||
request: Request = None,
|
||||
):
|
||||
new_ghost = Ghost.from_orm(ghost)
|
||||
ghost.birthdate = ghost.birthdate.replace(tzinfo=timezone.utc)
|
||||
print(ghost.birthdate)
|
||||
new_ghost.owner_id = current_user.id
|
||||
session.add(new_ghost)
|
||||
await session.commit()
|
||||
await session.refresh(new_ghost)
|
||||
return new_ghost
|
||||
|
||||
|
||||
@gf.get("/ghosts")
|
||||
async def read_users(
|
||||
offset: int = 0, limit: int = Query(default=100, lte=100), session: Session = Depends(get_session)
|
||||
@gf.post("/ghosts")
|
||||
async def get_ghosts(
|
||||
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))
|
||||
return ghosts.scalars().all()
|
||||
offset = start
|
||||
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[:]}
|
||||
|
||||
if hasattr(request.state, "ghostforge"):
|
||||
if "crumbs" in request.state.ghostforge:
|
||||
result["crumbs"] = request.state.ghostforge["crumbs"]
|
||||
for key, value in request.state.ghostforge.items():
|
||||
result[key] = value
|
||||
|
||||
result.update({"request": request})
|
||||
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 {
|
||||
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.
|
||||
|
||||
function saveMenuState() {
|
||||
var leftNavigation = document.getElementById("left-navigation");
|
||||
if (!leftNavigation) {
|
||||
|
@ -77,23 +76,41 @@ function modal_focus(modal_name) {
|
|||
|
||||
// Hotkeys
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Escape" || event.keyCode === 27) {
|
||||
window.location.hash = "#";
|
||||
}
|
||||
// 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 search_box = document.getElementById("search_string");
|
||||
console.log(search_box);
|
||||
if (event.ctrlKey) {
|
||||
event.preventDefault(); // prevent default Ctrl+G behavior (find in page)
|
||||
event.preventDefault();
|
||||
nav_search_button.click();
|
||||
search_box.focus();
|
||||
}
|
||||
// Add support for Apple+G on Macs
|
||||
|
||||
if (event.metaKey) {
|
||||
event.preventDefault(); // prevent default Apple+G behavior (bookmark)
|
||||
event.preventDefault();
|
||||
nav_search_button.click();
|
||||
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
|
||||
|
|
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.
|
||||
# Since 3.7 (we require >= 3.9), dicts are guaranteed ordered as inserted.
|
||||
templates.env.globals["gf_navbar"] = {
|
||||
"Ghosts": {
|
||||
"Dashboard": "/dashboard",
|
||||
"New": "/ghosts/new",
|
||||
"Active": "/ghosts",
|
||||
"Archived": "/ghosts/archive",
|
||||
},
|
||||
"Ghosts": {"Dashboard": "/dashboard", "Browse": "/ghosts"},
|
||||
"Research": {
|
||||
"Guidebook": "/guidebook",
|
||||
"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-icons.min.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>
|
||||
{% block js %}{% endblock js %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="off-canvas off-canvas-sidebar-show d-flex">
|
||||
{% include "navigation/side.html" %}
|
||||
<div class="off-canvas-content">
|
||||
<div class="container" style="padding-right: 5em; padding-top: 1em;">
|
||||
<div class="container ghostforge-container">
|
||||
{% include "navigation/top.html" %}
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include "modals/modals.html" %}
|
||||
</body>
|
||||
{% block bottomjs %}{% endblock bottomjs %}
|
||||
|
||||
</html>
|
||||
|
|
|
@ -3,14 +3,91 @@
|
|||
{% block content %}
|
||||
<div class="columns">
|
||||
<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 class="columns">
|
||||
<div class="column col-3"></div>
|
||||
<div class="column col-6">
|
||||
<h5>More words.</h5>
|
||||
<div class="column col-8">
|
||||
{% include "ghosts/tabs/tabs.html" %}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<button class="btn btn-primary float-right">Save Changes</button>
|
||||
</div>
|
||||
<div class="column col-3"></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>
|
||||
<script src="{{ url_for('static', path='/js/ghost.js') }}"></script>
|
||||
{% 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-container" role="document">
|
||||
<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>
|
||||
<div id="left-navigation" class="off-canvas-sidebar">
|
||||
<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">
|
||||
</a>
|
||||
<div>{% include "modals/search.html" %}</div>
|
||||
{% include "navigation/side_buttons.html" %}
|
||||
<div class="nav-menus">
|
||||
{% for key, value in gf_navbar.items() %}
|
||||
<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">
|
||||
{% if crumbs %}
|
||||
<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 %}
|
||||
<li class="breadcrumb-item">
|
||||
{% if url %}
|
||||
|
@ -31,7 +31,7 @@
|
|||
alt="Avatar">
|
||||
</div>
|
||||
<div class="tile-content">
|
||||
{{ user.email }}
|
||||
{{ user.username }}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
@ -25,15 +25,15 @@ gf = APIRouter()
|
|||
|
||||
|
||||
class UserRead(schemas.BaseUser[uuid.UUID]):
|
||||
pass
|
||||
username: str
|
||||
|
||||
|
||||
class UserCreate(schemas.BaseUserCreate):
|
||||
pass
|
||||
username: str
|
||||
|
||||
|
||||
class UserUpdate(schemas.BaseUserUpdate):
|
||||
pass
|
||||
username: str
|
||||
|
||||
|
||||
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
||||
|
|
Loading…
Reference in a new issue