Compare commits

...

12 commits

11 changed files with 116 additions and 25 deletions

View file

@ -1,6 +1,6 @@
/.dockerignore .*
/.gitignore
/inventory.sqlite3 /inventory.sqlite3
/README.md /README.md
*.pyc *.pyc
Dockerfile Dockerfile
/contrib/

View file

@ -1,4 +1,4 @@
FROM python:3.10-alpine FROM python:3.12-alpine3.19
WORKDIR /app WORKDIR /app

47
contrib/bulk_create.py Executable file
View file

@ -0,0 +1,47 @@
#!/usr/bin/env python3
import sys
import requests
BASE = "https://in.ccc.ac"
COUNT = 0
START_ID = 1
session = requests.Session()
# session.auth = (":)", ":)")
res = session.get(f"{BASE}/api/items")
res.raise_for_status()
ids = res.json().keys()
kiste_ids = {int(i.lstrip("K")) for i in ids if i.startswith("K") and i != "K"}
print("existing ids:", repr(kiste_ids), file=sys.stderr)
current_id = START_ID
new_ids = []
while len(new_ids) < COUNT:
if current_id not in kiste_ids:
new_ids.append(current_id)
current_id += 1
print("created:", file=sys.stderr)
if len(new_ids) == 0:
print("(none)", file=sys.stderr)
for i in new_ids:
j = {
"id": f"K{i}",
"is_in": None,
"coords_bl": None,
"coords_tr": None,
"type": "Kiste",
"name": None,
"content": None,
"note": "Bitte Name, Ort, Inhalt eintragen",
"hidden": False,
}
res = session.put(f"{BASE}/api/items/{j['id']}", json=j)
res.raise_for_status()
print(f"{j['id']},{BASE}/form.html?id={j['id']}")

24
crud.py
View file

@ -1,18 +1,32 @@
from sqlalchemy.orm import Session from enum import Enum
from sqlalchemy import DECIMAL, cast
from sqlalchemy import DECIMAL, cast
from sqlalchemy.orm import Session
import models
import schemas
import models, schemas
def get_items(db: Session) -> list[models.Item]: def get_items(db: Session) -> list[models.Item]:
return db.query(models.Item).all() return db.query(models.Item).all()
def put_item(db: Session, id: str, item: schemas.Item): def put_item(db: Session, id: str, item: schemas.Item):
updated = bool(db.query(models.Item).filter(models.Item.id == id).update(item.dict())) updated = bool(
db.query(models.Item).filter(models.Item.id == id).update(item.dict())
)
if not updated: if not updated:
db.add(models.Item(**item.dict())) db.add(models.Item(**item.dict()))
db.commit() db.commit()
return updated if updated:
return PutItemResult.UPDATED
else:
return PutItemResult.ADDED
class PutItemResult(Enum):
ADDED = 1
UPDATED = 2
def delete_item(db: Session, id: str): def delete_item(db: Session, id: str):

View file

@ -5,9 +5,7 @@ from sqlalchemy.orm import sessionmaker
SQLALCHEMY_DATABASE_URL = "sqlite:///./inventory.sqlite3" SQLALCHEMY_DATABASE_URL = "sqlite:///./inventory.sqlite3"
engine = create_engine( engine = create_engine(
SQLALCHEMY_DATABASE_URL, SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}, echo=True
connect_args = {"check_same_thread": False},
echo=True
) )
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

35
main.py
View file

@ -1,9 +1,12 @@
import re
from calendar import timegm
from datetime import date
from fastapi import Depends, FastAPI, Response from fastapi import Depends, FastAPI, Response
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from sqlalchemy import event from sqlalchemy import event
from sqlalchemy.orm import Session
from sqlalchemy.engine import Engine from sqlalchemy.engine import Engine
import re from sqlalchemy.orm import Session
import crud import crud
from database import SessionLocal, engine from database import SessionLocal, engine
@ -14,6 +17,7 @@ Base.metadata.create_all(bind=engine)
app = FastAPI() app = FastAPI()
def get_db(): def get_db():
db = SessionLocal() db = SessionLocal()
try: try:
@ -21,6 +25,7 @@ def get_db():
finally: finally:
db.close() db.close()
@event.listens_for(Engine, "connect") @event.listens_for(Engine, "connect")
def _set_sqlite_pragma(conn, _): def _set_sqlite_pragma(conn, _):
cursor = conn.cursor() cursor = conn.cursor()
@ -31,21 +36,35 @@ def _set_sqlite_pragma(conn, _):
@app.get("/api/items", response_model=dict[str, Item]) @app.get("/api/items", response_model=dict[str, Item])
async def list_items(db: Session = Depends(get_db)): async def list_items(db: Session = Depends(get_db)):
# natural sort by id # natural sort by id
natsort = lambda item: [int(t) if t.isdigit() else t.lower() for t in re.split('(\d+)', item.id)] natsort = lambda item: [
int(t) if t.isdigit() else t.lower() for t in re.split(r"(\d+)", item.id)
]
items = crud.get_items(db) items = crud.get_items(db)
items = sorted(items, key=natsort) items = sorted(items, key=natsort)
return {i.id:i for i in items} return {i.id: i for i in items}
@app.put("/api/items/{id}") @app.put(
"/api/items/{id}",
status_code=201,
responses={201: {"description": "created"}, 204: {"description": "updated"}},
)
async def put_item(id: str, item: Item, db: Session = Depends(get_db)): async def put_item(id: str, item: Item, db: Session = Depends(get_db)):
if crud.put_item(db, id, item): if item.last_updated is None:
return Response(b'', status_code=204) item.last_updated = month_timestamp()
return Response(b'', status_code=201) if crud.put_item(db, id, item) == crud.PutItemResult.UPDATED:
return Response(b"", status_code=204)
return Response(b"", status_code=201)
@app.delete("/api/items/{id}", status_code=204) @app.delete("/api/items/{id}", status_code=204)
async def delete_item(id: str, db: Session = Depends(get_db)): async def delete_item(id: str, db: Session = Depends(get_db)):
crud.delete_item(db, id) crud.delete_item(db, id)
def month_timestamp() -> int:
"""Provides the timestamp of the current month's beginning (for improved privacy)"""
return timegm(date.today().replace(day=1).timetuple())
app.mount("/", StaticFiles(directory="static", html=True), name="static") app.mount("/", StaticFiles(directory="static", html=True), name="static")

View file

@ -1,7 +1,8 @@
from sqlalchemy import Column, ForeignKey, String, Text, Boolean from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, Text
from database import Base from database import Base
class Item(Base): class Item(Base):
__tablename__ = "items" __tablename__ = "items"
@ -14,3 +15,4 @@ class Item(Base):
content = Column(Text) content = Column(Text)
note = Column(Text) note = Column(Text)
hidden = Column(Boolean, nullable=False) hidden = Column(Boolean, nullable=False)
last_updated = Column(Integer)

View file

@ -1,3 +1,3 @@
fastapi>=0.79.0 fastapi>=0.109.0
sqlalchemy>=1.4.39 sqlalchemy>=2.0.25
uvicorn>=0.17.6 uvicorn>=0.27.0.post1

View file

@ -1,4 +1,4 @@
from pydantic import BaseModel from pydantic import BaseModel, Field
class Item(BaseModel): class Item(BaseModel):
@ -11,6 +11,7 @@ class Item(BaseModel):
content: str | None content: str | None
note: str | None note: str | None
hidden: bool hidden: bool
last_updated: int | None = Field(None)
class Config: class Config:
orm_mode = True from_attributes = True

View file

@ -43,6 +43,9 @@
<label for="hidden">Hide by default</label> <label for="hidden">Hide by default</label>
<input type="checkbox" id="hidden"> <input type="checkbox" id="hidden">
<label for="last_updated">Last updated</label>
<input id="last_updated" type="text" disabled>
</form> </form>
<button id="delete" class="btn red" autocomplete="off">Delete</button> <button id="delete" class="btn red" autocomplete="off">Delete</button>

View file

@ -39,6 +39,7 @@ function fillForm() {
document.getElementById('content').value = item.content; document.getElementById('content').value = item.content;
document.getElementById('note').value = item.note; document.getElementById('note').value = item.note;
document.getElementById('hidden').checked = item.hidden; document.getElementById('hidden').checked = item.hidden;
document.getElementById('last_updated').value = formatTimestamp(item.last_updated);
formCoordsToMap(); formCoordsToMap();
} }
@ -172,3 +173,9 @@ function formCoordsToMap() {
coordsToMap(coords_bl, coords_tr); coordsToMap(coords_bl, coords_tr);
} }
} }
function formatTimestamp(ts) {
const date = new Date(ts * 1000);
// using Swedish format as a hack to get an iso formatted date
return date.toLocaleDateString("sv", {timeZone: "UTC"}).replace(/\-\d+$/,'')
}