mirror of
https://github.com/DarrylNixon/ghostforge
synced 2024-04-22 06:27:20 -07:00
Improve data tables, start adding notes
This commit is contained in:
parent
11679795ab
commit
425780f8a4
53 changed files with 552 additions and 224 deletions
|
@ -6,6 +6,7 @@ repos:
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
|
exclude: 'ghostforge/static/js/es6/faker-8.0.2.mjs'
|
||||||
- repo: https://github.com/asottile/reorder_python_imports
|
- repo: https://github.com/asottile/reorder_python_imports
|
||||||
rev: v3.9.0
|
rev: v3.9.0
|
||||||
hooks:
|
hooks:
|
||||||
|
|
|
@ -28,6 +28,7 @@ from sqlmodel import TIMESTAMP
|
||||||
|
|
||||||
from ghostforge.db import get_session
|
from ghostforge.db import get_session
|
||||||
from ghostforge.db import User
|
from ghostforge.db import User
|
||||||
|
from ghostforge.helpers.stringies import age_in_human
|
||||||
from ghostforge.htmljson import HtmlJson
|
from ghostforge.htmljson import HtmlJson
|
||||||
from ghostforge.users import get_current_user
|
from ghostforge.users import get_current_user
|
||||||
|
|
||||||
|
@ -104,7 +105,7 @@ async def can_edit_ghost(
|
||||||
|
|
||||||
|
|
||||||
@gf.get("/ghosts/{ghost_id}")
|
@gf.get("/ghosts/{ghost_id}")
|
||||||
@hj.html_or_json("ghost.html")
|
@hj.html_or_json("ghosts/ghost.html")
|
||||||
async def read_ghost(
|
async def read_ghost(
|
||||||
ghost_id: int,
|
ghost_id: int,
|
||||||
current_user: Annotated[User, Depends(get_current_user())],
|
current_user: Annotated[User, Depends(get_current_user())],
|
||||||
|
@ -114,22 +115,25 @@ async def read_ghost(
|
||||||
):
|
):
|
||||||
if not can_view:
|
if not can_view:
|
||||||
raise HTTPException(status_code=403, detail="You're not authorized to see this ghost")
|
raise HTTPException(status_code=403, detail="You're not authorized to see this ghost")
|
||||||
result = await session.execute(
|
result = (
|
||||||
select(Ghost, User.username.label("owner_username"), User.id.label("owner_guid"))
|
await session.execute(
|
||||||
.join(User, Ghost.owner_id == User.id)
|
select(Ghost, User.username.label("owner_username"), User.id.label("owner_guid"))
|
||||||
.where(Ghost.id == ghost_id)
|
.join(User, Ghost.owner_id == User.id)
|
||||||
)
|
.where(Ghost.id == ghost_id)
|
||||||
ghost = result.first()
|
)
|
||||||
if not ghost:
|
).first()
|
||||||
|
if not result:
|
||||||
raise HTTPException(status_code=404, detail="No ghost with that ID")
|
raise HTTPException(status_code=404, detail="No ghost with that ID")
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"ghost": ghost.Ghost,
|
"ghost": result.Ghost,
|
||||||
"owner": ghost.owner_username,
|
"owner": result.owner_username,
|
||||||
"user": current_user,
|
"user": current_user,
|
||||||
"crumbs": [("ghosts", "/ghosts"), (ghost.Ghost.id, False)],
|
"computed": {"age": age_in_human(result.Ghost.birthdate)},
|
||||||
|
"crumbs": [("ghosts", "/ghosts"), (result.Ghost.id, False)],
|
||||||
}
|
}
|
||||||
request.state.ghostforge = data | getattr(request.state, "ghostforge", {})
|
request.state.ghostforge = data | getattr(request.state, "ghostforge", {})
|
||||||
return ghost
|
return result
|
||||||
|
|
||||||
|
|
||||||
@gf.put("/ghosts/{ghost_id}")
|
@gf.put("/ghosts/{ghost_id}")
|
||||||
|
@ -179,19 +183,13 @@ async def get_ghosts(
|
||||||
session: AsyncSession = Depends(get_session),
|
session: AsyncSession = Depends(get_session),
|
||||||
request: Request = None,
|
request: Request = None,
|
||||||
):
|
):
|
||||||
offset = start
|
|
||||||
limit = offset + length
|
|
||||||
|
|
||||||
# Get the column name based on the order_col integer value
|
# Get the column name based on the order_col integer value
|
||||||
ghost_columns = [c.key for c in inspect(Ghost).c]
|
ghost_columns = [c.key for c in inspect(Ghost).c]
|
||||||
order_col_name = ghost_columns[order_col]
|
order_col_name = ghost_columns[order_col]
|
||||||
|
|
||||||
# Retrieve filtered ghosts from database
|
# Retrieve filtered ghosts from database
|
||||||
query = (
|
query = select(Ghost, User.username.label("owner_username"), User.id.label("owner_guid")).join(
|
||||||
select(Ghost, User.username.label("owner_username"), User.id.label("owner_guid"))
|
User, Ghost.owner_id == User.id
|
||||||
.offset(offset)
|
|
||||||
.limit(limit)
|
|
||||||
.join(User, Ghost.owner_id == User.id)
|
|
||||||
)
|
)
|
||||||
permission_filter = or_(
|
permission_filter = or_(
|
||||||
Ghost.owner_id == current_user.id,
|
Ghost.owner_id == current_user.id,
|
||||||
|
@ -214,11 +212,15 @@ async def get_ghosts(
|
||||||
if isinstance(column_attr.type, (AutoString, Text)):
|
if isinstance(column_attr.type, (AutoString, Text)):
|
||||||
conditions.append(column_attr.ilike(f"%{search}%"))
|
conditions.append(column_attr.ilike(f"%{search}%"))
|
||||||
query = query.where(or_(*conditions))
|
query = query.where(or_(*conditions))
|
||||||
|
|
||||||
if order_dir == "asc":
|
if order_dir == "asc":
|
||||||
query = query.order_by(asc(getattr(Ghost, order_col_name)))
|
query = query.order_by(asc(getattr(Ghost, order_col_name)))
|
||||||
else:
|
else:
|
||||||
query = query.order_by(desc(getattr(Ghost, order_col_name)))
|
query = query.order_by(desc(getattr(Ghost, order_col_name)))
|
||||||
|
|
||||||
|
query = query.offset(start).limit(length)
|
||||||
ghosts = (await session.execute(query)).all()
|
ghosts = (await session.execute(query)).all()
|
||||||
|
print(len(ghosts))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"recordsTotal": total_ghosts,
|
"recordsTotal": total_ghosts,
|
||||||
|
|
21
ghostforge/helpers/stringies.py
Normal file
21
ghostforge/helpers/stringies.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
def age_in_human(birthdate: datetime.datetime) -> str:
|
||||||
|
today = datetime.datetime.now(birthdate.tzinfo)
|
||||||
|
delta = abs(today - birthdate)
|
||||||
|
|
||||||
|
if (years := delta.days // 365) > 0:
|
||||||
|
res = f"{years} year{'s' if years > 1 else ''}"
|
||||||
|
elif (months := (delta.days % 365) // 30) > 0:
|
||||||
|
res = f"{months} month{'s' if months > 1 else ''}"
|
||||||
|
elif (weeks := ((delta.days % 365) % 30) // 7) > 0:
|
||||||
|
res = f"{weeks} week{'s' if weeks > 1 else ''}"
|
||||||
|
elif (days := ((delta.days % 365) % 30) % 7) > 0:
|
||||||
|
res = f"{days} day{'s' if days > 1 else ''}"
|
||||||
|
else:
|
||||||
|
res = "0 days"
|
||||||
|
|
||||||
|
res += " early" if birthdate > today else " old"
|
||||||
|
|
||||||
|
return res
|
166
ghostforge/notes.py
Normal file
166
ghostforge/notes.py
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from fastapi import Depends
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
from sqlmodel import Field
|
||||||
|
from sqlmodel import select
|
||||||
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
|
from ghostforge.db import get_session
|
||||||
|
from ghostforge.db import User
|
||||||
|
from ghostforge.ghosts import can_view_ghost
|
||||||
|
from ghostforge.ghosts import Ghost
|
||||||
|
from ghostforge.users import get_current_user
|
||||||
|
|
||||||
|
|
||||||
|
class GhostNote(SQLModel, table=True):
|
||||||
|
id: Optional[int] = Field(primary_key=True)
|
||||||
|
ghost_id: int = Field(foreign_key=Ghost.id)
|
||||||
|
user_id: Optional[int] = Field(foreign_key=User.id)
|
||||||
|
event_time: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
content: str
|
||||||
|
version: int = Field(default=1)
|
||||||
|
is_deleted: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class GhostNoteEdit(SQLModel, table=True):
|
||||||
|
id: Optional[int] = Field(primary_key=True)
|
||||||
|
note_id: int = Field(foreign_key=GhostNote.id)
|
||||||
|
user_id: Optional[int] = Field(foreign_key=User.id)
|
||||||
|
edit_time: datetime = Field(default_factory=datetime.utcnow)
|
||||||
|
content: str
|
||||||
|
version: int = Field()
|
||||||
|
|
||||||
|
|
||||||
|
class GhostNoteCreate(GhostNote, table=False):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class GhostNoteUpdate(GhostNote, table=False):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class GhostNoteRead(GhostNote, table=False):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
gf = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
# Route to read all latest notes for the specified ghost
|
||||||
|
@gf.get("/ghosts/{ghost_id}/notes/", response_model=list[GhostNoteRead])
|
||||||
|
async def read_ghost_notes(
|
||||||
|
ghost_id: int, session: AsyncSession = Depends(get_session), can_view: bool = Depends(can_view_ghost)
|
||||||
|
):
|
||||||
|
if not can_view:
|
||||||
|
raise HTTPException(status_code=403, detail="You're not authorized to see this ghost")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
select(GhostNote)
|
||||||
|
.join(GhostNoteEdit, isouter=True)
|
||||||
|
.filter(GhostNote.ghost_id == ghost_id, GhostNote.is_deleted is False)
|
||||||
|
.order_by(GhostNote.event_time.desc(), GhostNoteEdit.version.desc())
|
||||||
|
.group_by(GhostNote.id, GhostNoteEdit.note_id)
|
||||||
|
.options(selectinload(GhostNote.user))
|
||||||
|
)
|
||||||
|
|
||||||
|
notes = (await session.execute(query)).all()
|
||||||
|
return notes
|
||||||
|
|
||||||
|
|
||||||
|
# Route to create a new note for the specified ghost for the current user
|
||||||
|
@gf.post("/ghosts/{ghost_id}/notes/new", response_model=GhostNoteRead)
|
||||||
|
async def create_ghost_note(
|
||||||
|
ghost_id: int,
|
||||||
|
note: GhostNoteCreate,
|
||||||
|
current_user: User = Depends(get_current_user()),
|
||||||
|
session: AsyncSession = Depends(get_session),
|
||||||
|
can_view: bool = Depends(can_view_ghost),
|
||||||
|
):
|
||||||
|
if not can_view:
|
||||||
|
raise HTTPException(status_code=403, detail="You're not authorized to see this ghost")
|
||||||
|
db_note = GhostNote.from_orm(note)
|
||||||
|
db_note.ghost_id = ghost_id
|
||||||
|
db_note.user_id = current_user.id
|
||||||
|
session.add(db_note)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(db_note)
|
||||||
|
return db_note
|
||||||
|
|
||||||
|
|
||||||
|
# Route to read the latest version of a note
|
||||||
|
@gf.get("/ghosts/{ghost_id}/notes/{note_id}", response_model=GhostNoteRead)
|
||||||
|
async def read_ghost_note(
|
||||||
|
ghost_id: int, note_id: int, session: AsyncSession = Depends(get_session), can_view: bool = Depends(can_view_ghost)
|
||||||
|
):
|
||||||
|
if not can_view:
|
||||||
|
raise HTTPException(status_code=403, detail="You're not authorized to see this ghost")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
select(GhostNote)
|
||||||
|
.join(GhostNoteEdit, isouter=True)
|
||||||
|
.filter(
|
||||||
|
GhostNote.id == note_id,
|
||||||
|
GhostNote.ghost_id == ghost_id,
|
||||||
|
GhostNote.is_deleted is False,
|
||||||
|
)
|
||||||
|
.order_by(GhostNote.event_time.desc(), GhostNoteEdit.version.desc())
|
||||||
|
.group_by(GhostNote.id, GhostNoteEdit.note_id)
|
||||||
|
.options(selectinload(GhostNote.user))
|
||||||
|
)
|
||||||
|
|
||||||
|
note = (await session.execute(query)).first()
|
||||||
|
if not note:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Note not found",
|
||||||
|
)
|
||||||
|
return note
|
||||||
|
|
||||||
|
|
||||||
|
# Route to edit a note
|
||||||
|
@gf.put("/ghosts/{ghost_id}/notes/{note_id}", response_model=GhostNoteRead)
|
||||||
|
async def update_ghost_note(
|
||||||
|
ghost_id: int,
|
||||||
|
note_id: int,
|
||||||
|
note: GhostNoteUpdate,
|
||||||
|
current_user: User = Depends(get_current_user()),
|
||||||
|
session: AsyncSession = Depends(get_session),
|
||||||
|
can_view: bool = Depends(can_view_ghost),
|
||||||
|
):
|
||||||
|
if not can_view:
|
||||||
|
raise HTTPException(status_code=403, detail="You're not authorized to see this ghost")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
select(GhostNote)
|
||||||
|
.filter(
|
||||||
|
GhostNote.id == note_id,
|
||||||
|
GhostNote.ghost_id == ghost_id,
|
||||||
|
GhostNote.is_deleted is False,
|
||||||
|
GhostNote.user_id == current_user.id,
|
||||||
|
)
|
||||||
|
.options(selectinload(GhostNote.user))
|
||||||
|
)
|
||||||
|
check_note = (await session.execute(query)).first()
|
||||||
|
if not check_note:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Note not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
new_edit = GhostNoteEdit(
|
||||||
|
note_id=check_note.note_id,
|
||||||
|
user_id=current_user.id,
|
||||||
|
content=note.content,
|
||||||
|
version=check_note.version + 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
session.add(new_edit)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(new_edit)
|
||||||
|
|
||||||
|
return new_edit
|
|
@ -4,6 +4,7 @@ from fastapi import Depends
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from fastapi import Response
|
from fastapi import Response
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
@ -60,6 +61,13 @@ async def on_startup():
|
||||||
await create_db_and_tables()
|
await create_db_and_tables()
|
||||||
|
|
||||||
|
|
||||||
|
@gf.get("/faker.mjs")
|
||||||
|
async def get_faker():
|
||||||
|
response = FileResponse("ghostforge/static/js/es6/faker-8.0.2.mjs", media_type="application/javascript")
|
||||||
|
response.headers["Cache-Control"] = "public, max-age=31536000, immutable"
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@gf.get("/", response_class=HTMLResponse)
|
@gf.get("/", response_class=HTMLResponse)
|
||||||
async def home(request: Request, current_user=Depends(get_current_user())):
|
async def home(request: Request, current_user=Depends(get_current_user())):
|
||||||
if current_user:
|
if current_user:
|
||||||
|
|
38
ghostforge/static/js/edit.js
Normal file
38
ghostforge/static/js/edit.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
const button = document.getElementById("edit-field-button");
|
||||||
|
const modal = document.getElementById("edit-field");
|
||||||
|
const modal_title = document.getElementById("edit_modal_title");
|
||||||
|
const modal_key = document.getElementById("edit_modal_key");
|
||||||
|
const modal_input = document.getElementById("edit_modal_input");
|
||||||
|
const modal_button = document.getElementById("edit_modal_submit")
|
||||||
|
|
||||||
|
function editFieldModal(field_id) {
|
||||||
|
const field = document.getElementById(field_id);
|
||||||
|
const label = document.getElementById(field_id + "_label");
|
||||||
|
|
||||||
|
modal_title.textContent = "Edit " + label.textContent;
|
||||||
|
modal_key.textContent = "New " + label.textContent;
|
||||||
|
modal_input.value = field.textContent;
|
||||||
|
modal_button.onclick = function () {
|
||||||
|
editField(field, document.getElementById("edit_modal_input").value);
|
||||||
|
}
|
||||||
|
button.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function editField(field, newvalue) {
|
||||||
|
if (field.textContent == newvalue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
field.textContent = newvalue;
|
||||||
|
window.location.hash = "#";
|
||||||
|
}
|
||||||
|
|
||||||
|
[].forEach.call(document.querySelectorAll('input[type=checkbox]'), function (checkbox) {
|
||||||
|
checkbox.checked = checkbox.defaultChecked;
|
||||||
|
});
|
||||||
|
const editButtons = document.querySelectorAll("[id^='button_edit_']");
|
||||||
|
editButtons.forEach(function (button) {
|
||||||
|
button.addEventListener("click", function () {
|
||||||
|
const fieldName = button.id.slice("button_edit_".length);
|
||||||
|
editFieldModal(fieldName);
|
||||||
|
});
|
||||||
|
});
|
17
ghostforge/static/js/es6/faker-8.0.2.mjs
Normal file
17
ghostforge/static/js/es6/faker-8.0.2.mjs
Normal file
File diff suppressed because one or more lines are too long
|
@ -79,36 +79,22 @@ document.addEventListener("keydown", function (event) {
|
||||||
if (event.key === "Escape" || event.keyCode === 27) {
|
if (event.key === "Escape" || event.keyCode === 27) {
|
||||||
window.location.hash = "#";
|
window.location.hash = "#";
|
||||||
}
|
}
|
||||||
// Ctrl/Cmd+G for search modal
|
// Ctrl/Cmd+g for search modal
|
||||||
else 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");
|
if (event.ctrlKey || event.metaKey) {
|
||||||
if (event.ctrlKey) {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
nav_search_button.click();
|
nav_search_button.click();
|
||||||
search_box.focus();
|
modal_focus("#search_modal");
|
||||||
}
|
|
||||||
|
|
||||||
if (event.metaKey) {
|
|
||||||
event.preventDefault();
|
|
||||||
nav_search_button.click();
|
|
||||||
search_box.focus();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Ctrl/Cmd+N for new ghost modal
|
// Ctrl/Cmd+n for new ghost modal
|
||||||
else if (event.key == "n") {
|
else if (event.key == "n") {
|
||||||
const nav_new_button = document.getElementById("nav_new_button");
|
const nav_new_button = document.getElementById("nav_new_button");
|
||||||
const new_ghost_box = document.getElementById("new_firstname");
|
if (event.ctrlKey || event.metaKey) {
|
||||||
if (event.ctrlKey) {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
nav_new_button.click();
|
nav_new_button.click();
|
||||||
new_ghost_box.focus();
|
modal_focus("#new-ghost-modal");
|
||||||
}
|
|
||||||
|
|
||||||
if (event.metaKey) {
|
|
||||||
event.preventDefault();
|
|
||||||
nav_new_button.click();
|
|
||||||
new_ghost_box.focus();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -116,6 +102,5 @@ document.addEventListener("keydown", function (event) {
|
||||||
// Run various onload/unload functions since script is loaded before elements are
|
// Run various onload/unload functions since script is loaded before elements are
|
||||||
window.onload = function () {
|
window.onload = function () {
|
||||||
restoreMenuState();
|
restoreMenuState();
|
||||||
modal_focus("#search-modal");
|
|
||||||
};
|
};
|
||||||
window.onbeforeunload = saveMenuState;
|
window.onbeforeunload = saveMenuState;
|
||||||
|
|
50
ghostforge/static/js/user.js
Normal file
50
ghostforge/static/js/user.js
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
const loginError = document.getElementById('auth-error');
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
$('#save_changes').click(function () {
|
||||||
|
let username = $('#username').text();
|
||||||
|
let email = $('#email').text();
|
||||||
|
let password = $('#password').text();
|
||||||
|
let is_active = $('#edit_active').is(':checked');
|
||||||
|
let is_verified = $('#edit_verified').is(':checked');
|
||||||
|
let is_superuser = $('#edit_superuser').is(':checked');
|
||||||
|
let user_guid = $('#user_guid')[0].value;
|
||||||
|
|
||||||
|
let request_body = {
|
||||||
|
"username": username,
|
||||||
|
"email": email,
|
||||||
|
"password": password,
|
||||||
|
"is_active": is_active,
|
||||||
|
"is_verified": is_verified,
|
||||||
|
"is_superuser": is_superuser
|
||||||
|
};
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/manage/' + user_guid,
|
||||||
|
type: 'PUT',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify(request_body),
|
||||||
|
success: function (result) {
|
||||||
|
let toast = document.getElementById("error-msg");
|
||||||
|
if (!toast) {
|
||||||
|
toast = document.createElement('div');
|
||||||
|
toast.id = "success-msg";
|
||||||
|
toast.classList.add("toast", "toast-success");
|
||||||
|
loginError.appendChild(toast);
|
||||||
|
}
|
||||||
|
toast.innerText = "User updated successfully.";
|
||||||
|
},
|
||||||
|
error: function (xhr, textStatus, errorThrown) {
|
||||||
|
let errorMessage = xhr.responseJSON.detail;
|
||||||
|
let toast = document.getElementById("error-msg");
|
||||||
|
if (!toast) {
|
||||||
|
toast = document.createElement('div');
|
||||||
|
toast.id = "error-msg";
|
||||||
|
toast.classList.add("toast", "toast-error");
|
||||||
|
loginError.appendChild(toast);
|
||||||
|
}
|
||||||
|
toast.innerText = errorMessage;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -18,16 +18,16 @@ templates.env.globals["gf_navbar"] = {
|
||||||
"Ghosts": {"Dashboard": "/dashboard", "Browse": "/ghosts"},
|
"Ghosts": {"Dashboard": "/dashboard", "Browse": "/ghosts"},
|
||||||
"Research": {
|
"Research": {
|
||||||
"Guidebook": "/guidebook",
|
"Guidebook": "/guidebook",
|
||||||
"Cheat Sheet": "/cheatsheet",
|
"Cheat Sheet*": "/cheatsheet",
|
||||||
},
|
},
|
||||||
"Settings": {
|
"Settings": {
|
||||||
"Your Profile": "/profile",
|
"Your Profile": "/profile",
|
||||||
"Configuration": "/configuration",
|
"Configuration*": "/configuration",
|
||||||
"Integrations": "/integrations",
|
"Integrations*": "/integrations",
|
||||||
"Manage Users": "/manage",
|
"Manage Users": "/manage",
|
||||||
},
|
},
|
||||||
"Meta": {
|
"Meta": {
|
||||||
"System Logs": "/logs",
|
"System Logs*": "/logs",
|
||||||
"Logout": "/logout",
|
"Logout": "/logout",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,9 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="icon" type="image/x-icon" href="{{ url_for('static', path='/img/favicon.ico') }}">
|
<link rel="icon" type="image/x-icon" href="{{ url_for('static', path='/img/favicon.ico') }}">
|
||||||
<title>{% block title %}{{ title }}{% endblock title %}</title>
|
<title>{% block title %}{{ title }}{% endblock title %}</title>
|
||||||
<link href="{{ url_for('static', path='/css/spectre.min.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', path='/css/3p/spectre/spectre.min.css') }}" rel="stylesheet">
|
||||||
<link href="{{ url_for('static', path='/css/spectre-exp.min.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', path='/css/3p/spectre/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/3p/spectre/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 %}
|
{% block css %}{% endblock css %}
|
||||||
<script src="{{ url_for('static', path='/js/ghostforge.js') }}"></script>
|
<script src="{{ url_for('static', path='/js/ghostforge.js') }}"></script>
|
||||||
|
@ -26,8 +26,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include "modals/modals.html" %}
|
{% block bottom_modals %}{% endblock bottom_modals %}
|
||||||
|
{% block bottomjs %}{% endblock bottomjs %}
|
||||||
</body>
|
</body>
|
||||||
{% block bottomjs %}{% endblock bottomjs %}
|
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
30
ghostforge/templates/datatables.html
Normal file
30
ghostforge/templates/datatables.html
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{% block js %}
|
||||||
|
<script src="{{ url_for('static', path='/js/3p/jquery-3.7.0.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/3p/datatables/dataTables.dataTables.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/3p/datatables/jquery.dataTables.min.js') }}"></script>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', path='/js/3p/datatables/colReorder.dataTables.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/3p/datatables/fixedHeader.dataTables.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/3p/datatables/responsive.dataTables.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/3p/datatables/searchPanes.dataTables.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/3p/datatables/searchBuilder.dataTables.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/3p/datatables/stateRestore.dataTables.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/3p/datatables/dataTables.searchPanes.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/3p/datatables/dataTables.colReorder.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/3p/datatables/dataTables.stateRestore.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/3p/datatables/dataTables.fixedHeader.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/3p/datatables/dataTables.responsive.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/3p/datatables/dataTables.searchBuilder.min.js') }}"></script>
|
||||||
|
{% endblock js %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
<link href="{{ url_for('static', path='/css/3p/datatables/datatables.css') }}" rel="stylesheet">
|
||||||
|
<link href="{{ url_for('static', path='/css/3p/datatables/dataTables.datatables.min.css') }}" rel="stylesheet">
|
||||||
|
<link href="{{ url_for('static', path='/css/3p/datatables/jquery.datatables.min.css') }}" rel="stylesheet">
|
||||||
|
<link href="{{ url_for('static', path='/css/3p/datatables/colReorder.datatables.min.css') }}" rel="stylesheet">
|
||||||
|
<link href="{{ url_for('static', path='/css/3p/datatables/fixedHeader.datatables.min.css') }}" rel="stylesheet">
|
||||||
|
<link href="{{ url_for('static', path='/css/3p/datatables/responsive.datatables.min.css') }}" rel="stylesheet">
|
||||||
|
<link href="{{ url_for('static', path='/css/3p/datatables/searchPanes.datatables.min.css') }}" rel="stylesheet">
|
||||||
|
<link href="{{ url_for('static', path='/css/3p/datatables/searchBuilder.datatables.min.css') }}" rel="stylesheet">
|
||||||
|
<link href="{{ url_for('static', path='/css/3p/datatables/stateRestore.datatables.min.css') }}" rel="stylesheet">
|
||||||
|
{% endblock css %}
|
|
@ -12,7 +12,7 @@
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<h5>[age] year old American Male</h5>
|
<h5>{{ computed["age"] }} American Male</h5>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
|
@ -90,5 +90,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="{{ url_for('static', path='/js/ghost.js') }}"></script>
|
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
|
{% block bottomjs %}
|
||||||
|
<script src="{{ url_for('static', path='/js/ghost.js') }}"></script>
|
||||||
|
{% endblock bottomjs %}
|
|
@ -2,34 +2,10 @@
|
||||||
|
|
||||||
{% block title %}Browse Ghosts{% endblock title %}
|
{% block title %}Browse Ghosts{% endblock title %}
|
||||||
|
|
||||||
{% 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 %}
|
{% block js %}
|
||||||
<link href="{{ url_for('static', path='/css/datatables.css') }}" rel="stylesheet">
|
{% include "datatables.html" %}
|
||||||
<link href="{{ url_for('static', path='/css/colReorder.datatables.min.css') }}" rel="stylesheet">
|
{% endblock js %}
|
||||||
<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 %}
|
{% block content %}
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
|
@ -97,5 +73,13 @@
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
$(document).ready(function () {
|
||||||
|
var urlParams = new URLSearchParams(window.location.search);
|
||||||
|
var searchTerm = urlParams.get('initial');
|
||||||
|
if (searchTerm) {
|
||||||
|
$('#ghosts-table').DataTable().search(searchTerm).draw();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock bottomjs %}
|
{% endblock bottomjs %}
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="icon" type="image/x-icon" href="{{ url_for('static', path='/img/favicon.ico') }}">
|
<link rel="icon" type="image/x-icon" href="{{ url_for('static', path='/img/favicon.ico') }}">
|
||||||
<title>{% block title %}{{ title }}{% endblock title %}</title>
|
<title>{% block title %}{{ title }}{% endblock title %}</title>
|
||||||
<link href="{{ url_for('static', path='/css/spectre.min.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', path='/css/3p/spectre/spectre.min.css') }}" rel="stylesheet">
|
||||||
<link href="{{ url_for('static', path='/css/spectre-icons.min.css') }}" rel="stylesheet">
|
<link href="{{ url_for('static', path='/css/3p/spectre/spectre-icons.min.css') }}" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<html>
|
<html>
|
||||||
|
|
29
ghostforge/templates/modals/edit_field.html
Normal file
29
ghostforge/templates/modals/edit_field.html
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<div class="modal modal-sm" id="edit-field"><a class="modal-overlay" href="#close" aria-label="Close"></a>
|
||||||
|
<div class="modal-container" role="document">
|
||||||
|
<div class="modal-header"><a class="btn btn-clear float-right" href="#close" aria-label="Close"></a>
|
||||||
|
<div class="modal-title h5" id="edit_modal_title"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="content">
|
||||||
|
<form id="edit_modal_form" , method="get" , action="#">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="edit_modal_input">
|
||||||
|
<h6 id="edit_modal_key"></h6>
|
||||||
|
</label>
|
||||||
|
<div id="edit_modal_input_div">
|
||||||
|
<input class="form-input" id="edit_modal_input" name="edit_modal_new_value" type="text">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<a class="btn btn-primary" id="edit_modal_submit">Change Value</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: none;" hidden>
|
||||||
|
<a class="btn btn-primary btn-sm" href="#edit-field" id="edit-field-button">
|
||||||
|
</a>
|
||||||
|
</div>
|
|
@ -1,6 +1,6 @@
|
||||||
<div class="modal modal-sm" id="new-ghost-modal"><a class="modal-overlay" href="#" aria-label="Close"></a>
|
<div class="modal modal-sm" id="new-ghost-modal"><a class="modal-overlay" href="#close" 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="#" aria-label="Close"></a>
|
<div class="modal-header"><a class="btn btn-clear float-right" href="#close" aria-label="Close"></a>
|
||||||
<div class="modal-title h5">New Ghost</div>
|
<div class="modal-title h5">New Ghost</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
@ -20,9 +20,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn">Randomize</button>
|
<button class="btn">Random Ghost</button>
|
||||||
<button class="btn btn-primary">Create Ghost</button>
|
<button class="btn btn-primary">Create</button>
|
||||||
<a class="btn btn-link" href="#modals-sizes" aria-label="Close">Close</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,34 +1,23 @@
|
||||||
<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="#close" 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="#close" aria-label="Close"></a>
|
||||||
<div class="modal-title h5">Search Ghosts</div>
|
<div class="modal-title h5">Search Ghosts</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<form>
|
<form method="get" , action="/ghosts">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="search_string">
|
<label class="form-label" for="search_string">
|
||||||
<h6>Search Term</h6>
|
<h6>Search Term</h6>
|
||||||
</label>
|
</label>
|
||||||
<input class="form-input" id="search_string" type="text" placeholder="Type a string here">
|
<input class="form-input" id="search_string" name="initial" type="text"
|
||||||
</div>
|
placeholder="Type a string here">
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">
|
|
||||||
<h6>Scope</h6>
|
|
||||||
</label>
|
|
||||||
<label class="form-radio">
|
|
||||||
<input type="radio" name="scope"><i class="form-icon"></i> All text fields
|
|
||||||
</label>
|
|
||||||
<label class="form-radio">
|
|
||||||
<input type="radio" name="scope" checked=""><i class="form-icon"></i> Name fields only
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-primary">Search</button>
|
<button class="btn btn-primary">Search</button>
|
||||||
<a class="btn btn-link" href="#modals-sizes" aria-label="Close">Close</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -24,7 +24,6 @@
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<!-- ETC -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div align="right">
|
<div align="right">
|
||||||
|
@ -38,3 +37,13 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a class="off-canvas-overlay" href="#close"></a>
|
<a class="off-canvas-overlay" href="#close"></a>
|
||||||
|
{% block bottom_modals %}
|
||||||
|
{% include "modals/side_modals.html" %}
|
||||||
|
{% endblock bottom_modals %}
|
||||||
|
{% block bottomjs %}
|
||||||
|
<script type="module">
|
||||||
|
import { faker } from "/faker.mjs";
|
||||||
|
|
||||||
|
console.log(faker.person.firstName());
|
||||||
|
</script>
|
||||||
|
{% endblock bottomjs %}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
<div class="text-center">
|
<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;">
|
<div style="display: inline-block;">
|
||||||
<a class="btn btn-primary btn-sm" href="#new-ghost-modal" id="nav_new_button">
|
<a class="btn btn-primary btn-sm" href="#new-ghost-modal" id="nav_new_button">
|
||||||
<i class="icon icon-plus"></i>
|
<i class="icon icon-plus"></i>
|
||||||
⌘-n
|
⌘-n
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<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>
|
</div>
|
||||||
|
|
|
@ -1,74 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}{{ target_user.username }}{% endblock title %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column col-12">
|
|
||||||
<h1>Manage User</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column col-4"></div>
|
|
||||||
<div class="column col-4">
|
|
||||||
<div class="panel">
|
|
||||||
<div class="panel-header text-center">
|
|
||||||
<figure class="avatar avatar-lg"><img src="{{ url_for('static', path='/img/default-avatar.png') }}"
|
|
||||||
alt="Avatar"></figure>
|
|
||||||
<div class="panel-title h5 mt-10">{{ target_user.username }}</div>
|
|
||||||
<div class="panel-subtitle">
|
|
||||||
{% if target_user.is_superuser %}
|
|
||||||
<kbd>Administrator</kbd>
|
|
||||||
{% else %}
|
|
||||||
Normal User
|
|
||||||
{% endif %}
|
|
||||||
{% if not target_user.is_active %}
|
|
||||||
<s>Deactivated</s>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<nav class="panel-nav">
|
|
||||||
<ul class="tab tab-block">
|
|
||||||
<li class="tab-item active">Profile</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
<div class="panel-body">
|
|
||||||
<div class="tile tile-centered">
|
|
||||||
<div class="tile-content">
|
|
||||||
<div class="tile-title text-bold">Username</div>
|
|
||||||
<div class="tile-subtitle">{{ target_user.username }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="tile-action">
|
|
||||||
<button class="btn btn-link btn-action btn-lg tooltip tooltip-left"
|
|
||||||
data-tooltip="Change Username" id="edit_username"><i class="icon icon-edit"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="tile tile-centered">
|
|
||||||
<div class="tile-content">
|
|
||||||
<div class="tile-title text-bold">E-mail</div>
|
|
||||||
<div class="tile-subtitle">{{ target_user.email }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="tile-action">
|
|
||||||
<button class="btn btn-link btn-action btn-lg tooltip tooltip-left" data-tooltip="Change E-mail"
|
|
||||||
id="edit_email"><i class="icon icon-edit"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="tile tile-centered">
|
|
||||||
<div class="tile-content">
|
|
||||||
<div class="tile-title text-bold">New Password</div>
|
|
||||||
<div class="tile-subtitle">blocked</div>
|
|
||||||
</div>
|
|
||||||
<div class="tile-action">
|
|
||||||
<button class="btn btn-link btn-action btn-lg tooltip tooltip-left"
|
|
||||||
data-tooltip="Change Password" id="edit_password"><i class="icon icon-edit"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="panel-footer">
|
|
||||||
<button class="btn btn-primary btn-block">Save</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column col-4"></div>
|
|
||||||
</div>
|
|
||||||
{% endblock content %}
|
|
102
ghostforge/templates/users/user.html
Normal file
102
ghostforge/templates/users/user.html
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ target_user.username }}{% endblock title %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
<script src="{{ url_for('static', path='/js/3p/jquery-3.7.0.min.js') }}"></script>
|
||||||
|
{% endblock js %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column col-12">
|
||||||
|
<h1>Manage User</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column col-3"></div>
|
||||||
|
<div class="column col-6">
|
||||||
|
<div id="auth-error"></div>
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-header text-center">
|
||||||
|
<figure class="avatar avatar-lg"><img src="{{ url_for('static', path='/img/default-avatar.png') }}"
|
||||||
|
alt="Avatar"></figure>
|
||||||
|
<div class="panel-title h5 mt-10">{{ target_user.username }}</div>
|
||||||
|
<div class="panel-subtitle">
|
||||||
|
{% if target_user.is_superuser %}
|
||||||
|
<kbd>Administrator</kbd>
|
||||||
|
{% else %}
|
||||||
|
Normal User
|
||||||
|
{% endif %}
|
||||||
|
{% if not target_user.is_active %}
|
||||||
|
<s>Deactivated</s>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav class="panel-nav">
|
||||||
|
<ul class="tab tab-block">
|
||||||
|
<li class="tab-item active">Profile</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="tile tile-centered">
|
||||||
|
<div class="tile-content">
|
||||||
|
<div class="tile-title text-bold" id="username_label">Username</div>
|
||||||
|
<div class="tile-subtitle" id="username">{{ target_user.username }}</div>
|
||||||
|
<input type="hidden" id="user_guid" value="{{ target_user.id }}" />
|
||||||
|
</div>
|
||||||
|
<div class="tile-action">
|
||||||
|
<a class="btn btn-link btn-action btn-lg tooltip tooltip-left" data-tooltip="Change Username"
|
||||||
|
id="button_edit_username"><i class="icon icon-edit"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tile tile-centered">
|
||||||
|
<div class="tile-content">
|
||||||
|
<div class="tile-title text-bold" id="email_label">E-mail</div>
|
||||||
|
<div class="tile-subtitle" id="email">{{ target_user.email }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="tile-action">
|
||||||
|
<button class="btn btn-link btn-action btn-lg tooltip tooltip-left" data-tooltip="Change E-mail"
|
||||||
|
id="button_edit_email"><i class="icon icon-edit"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tile tile-centered">
|
||||||
|
<div class="tile-content">
|
||||||
|
<div class="tile-title text-bold" id="password_label">Password</div>
|
||||||
|
<div class="tile-subtitle" id="password">***</div>
|
||||||
|
</div>
|
||||||
|
<div class="tile-action">
|
||||||
|
<button class="btn btn-link btn-action btn-lg tooltip tooltip-left"
|
||||||
|
data-tooltip="Change Password" id="button_edit_password"><i
|
||||||
|
class="icon icon-edit"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-checkbox form-inline">
|
||||||
|
<input type="checkbox" id="edit_active" {% if target_user.is_active %}checked{% endif %}><i
|
||||||
|
class="form-icon"></i> Active
|
||||||
|
</label>
|
||||||
|
<label class="form-checkbox form-inline">
|
||||||
|
<input type="checkbox" id="edit_superuser" {% if target_user.is_verified %}checked{% endif %}><i
|
||||||
|
class="form-icon"></i> Verified
|
||||||
|
</label>
|
||||||
|
<label class="form-checkbox form-inline">
|
||||||
|
<input type="checkbox" id="edit_superuser" {% if target_user.is_superuser %}checked{% endif
|
||||||
|
%}><i class="form-icon"></i> Administrator
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-footer">
|
||||||
|
<button class="btn btn-primary btn-block" id="save_changes">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column col-3"></div>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
||||||
|
{% block bottom_modals %}
|
||||||
|
{% include "modals/edit_field.html" %}
|
||||||
|
{% endblock bottom_modals %}
|
||||||
|
{% block bottomjs %}
|
||||||
|
<script src="{{ url_for('static', path='/js/edit.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', path='/js/user.js') }}"></script>
|
||||||
|
{% endblock bottomjs %}
|
|
@ -3,34 +3,9 @@
|
||||||
{% block title %}Browse Ghosts{% endblock title %}
|
{% block title %}Browse Ghosts{% endblock title %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
<script src="{{ url_for('static', path='/js/jquery-3.7.0.min.js') }}"></script>
|
{% include "datatables.html" %}
|
||||||
<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 %}
|
{% 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 %}
|
{% block content %}
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column col-12">
|
<div class="column col-12">
|
|
@ -64,7 +64,8 @@ class UserCreate(schemas.BaseUserCreate):
|
||||||
|
|
||||||
|
|
||||||
class UserUpdate(schemas.BaseUserUpdate):
|
class UserUpdate(schemas.BaseUserUpdate):
|
||||||
pass
|
username: str
|
||||||
|
email: str
|
||||||
|
|
||||||
|
|
||||||
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
||||||
|
@ -105,7 +106,7 @@ def get_current_user(active: bool = True, optional: bool = False) -> User:
|
||||||
|
|
||||||
|
|
||||||
@gf.get("/manage", response_model=List[UserRead])
|
@gf.get("/manage", response_model=List[UserRead])
|
||||||
@hj.html_or_json("users.html")
|
@hj.html_or_json("users/users.html")
|
||||||
async def read_users(
|
async def read_users(
|
||||||
current_user: Annotated[User, Depends(get_current_user())],
|
current_user: Annotated[User, Depends(get_current_user())],
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
|
@ -146,7 +147,7 @@ async def create_user(
|
||||||
|
|
||||||
|
|
||||||
@gf.get("/manage/{user_guid}", response_model=UserRead)
|
@gf.get("/manage/{user_guid}", response_model=UserRead)
|
||||||
@hj.html_or_json("user.html")
|
@hj.html_or_json("users/user.html")
|
||||||
async def get_user(
|
async def get_user(
|
||||||
user_guid: str,
|
user_guid: str,
|
||||||
current_user: Annotated[User, Depends(get_current_user())],
|
current_user: Annotated[User, Depends(get_current_user())],
|
||||||
|
@ -170,7 +171,7 @@ async def get_user(
|
||||||
|
|
||||||
@gf.put("/manage/{user_guid}", response_model=UserRead)
|
@gf.put("/manage/{user_guid}", response_model=UserRead)
|
||||||
async def update_user(
|
async def update_user(
|
||||||
user_guid: str,
|
user_guid: uuid.UUID,
|
||||||
user_update: UserUpdate,
|
user_update: UserUpdate,
|
||||||
current_user: Annotated[User, Depends(get_current_user())],
|
current_user: Annotated[User, Depends(get_current_user())],
|
||||||
user_manager: UserManager = Depends(get_user_manager),
|
user_manager: UserManager = Depends(get_user_manager),
|
||||||
|
@ -178,20 +179,12 @@ async def update_user(
|
||||||
):
|
):
|
||||||
if not current_user.is_superuser and current_user.id != user_guid:
|
if not current_user.is_superuser and current_user.id != user_guid:
|
||||||
raise HTTPException(status_code=403, detail="No permission to edit this user")
|
raise HTTPException(status_code=403, detail="No permission to edit this user")
|
||||||
db_user = await user_manager.get(user_guid, session)
|
db_user = await user_manager.get(user_guid)
|
||||||
if not db_user:
|
if not db_user:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
|
||||||
update_data = user_update.dict(exclude_unset=True)
|
user_update = UserUpdate(**user_update.dict(exclude_unset=True))
|
||||||
if "is_superuser" in update_data and not current_user.is_superuser:
|
return await user_manager.update(user_update, db_user)
|
||||||
del update_data["is_superuser"]
|
|
||||||
if "password" in update_data:
|
|
||||||
update_data["hashed_password"] = await user_manager.hash_password(update_data["password"])
|
|
||||||
del update_data["password"]
|
|
||||||
await user_manager.update(db_user, session, **update_data)
|
|
||||||
|
|
||||||
await session.refresh(db_user)
|
|
||||||
return db_user
|
|
||||||
|
|
||||||
|
|
||||||
@gf.get("/profile", response_class=HTMLResponse)
|
@gf.get("/profile", response_class=HTMLResponse)
|
||||||
|
|
|
@ -24,6 +24,7 @@ dependencies = [
|
||||||
"fastapi-users-db-sqlmodel==0.3.0",
|
"fastapi-users-db-sqlmodel==0.3.0",
|
||||||
"sqlmodel==0.0.8",
|
"sqlmodel==0.0.8",
|
||||||
"markdown==3.4.3",
|
"markdown==3.4.3",
|
||||||
|
"faker==18.9.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|
Loading…
Reference in a new issue