diff --git a/index.html b/index.html
index f47c3be..a6c9f34 100644
--- a/index.html
+++ b/index.html
@@ -828,11 +828,11 @@
if (idea.start_time && idea.end_time) {
const s = new Date(idea.start_time).toLocaleDateString('nb-NO', {
hour: '2-digit', minute: '2-digit', hour12: false
- });
+ }).replace(/ /g, ' ');
const e = new Date(idea.end_time).toLocaleDateString('nb-NO', {
hour: '2-digit', minute: '2-digit', hour12: false
- });
- timeCell = `
${s} → ${e} | `;
+ }).replace(/ /g, ' ');
+ timeCell = `${s} → ${e} | `;
} else if (idea.start_time) {
const s = new Date(idea.start_time).toLocaleDateString('nb-NO', {
hour: '2-digit', minute: '2-digit', hour12: false
@@ -848,8 +848,8 @@
}
html += ``
- + `${idea.name} ${idea.description} | `
- + timeCell;
+ + `${idea.name} ${linkify(idea.description)} | `
+ + timeCell;
allVoters.forEach(voter => {
html += `${idea.voters.includes(voter) ? '✓' : ''} | `;
});
diff --git a/server.py b/server.py
index e16b8b6..6ef2181 100644
--- a/server.py
+++ b/server.py
@@ -5,6 +5,7 @@ from typing import Optional
from datetime import datetime
from fastapi import FastAPI, HTTPException, Path
from pydantic import BaseModel, Field
+from filelock import FileLock
import argparse
import uvicorn
@@ -49,7 +50,11 @@ class Vote(BaseModel):
DATA_DIR = "" # will be overridden via command line
def get_tour_filepath(tour_id: str) -> str:
- return os.path.join(DATA_DIR, f"{tour_id}.json")
+ fname = f"{tour_id}.json"
+ full = os.path.abspath(os.path.join(DATA_DIR, fname))
+ if not full.startswith(os.path.abspath(DATA_DIR) + os.sep):
+ raise HTTPException(status_code=400, detail="Invalid tour ID")
+ return full
# ─── ENDPOINT: CREATE TOUR ─────────────────────────────────────────────────────
@@ -74,7 +79,7 @@ def create_tour(tour: TourCreate):
filepath = get_tour_filepath(tour_id)
with open(filepath, "w") as f:
# Using default=str so that datetime objects become ISO strings
- json.dump(new_tour.dict(), f, default=str)
+ json.dump(new_tour.model_dump(), f, default=str)
return new_tour
@@ -117,22 +122,24 @@ def add_idea(tour_id: str, idea: IdeaCreate):
if not os.path.exists(filepath):
raise HTTPException(status_code=404, detail="Tour not found")
- with open(filepath) as f:
- data = json.load(f)
+ lock = FileLock(filepath + ".lock")
+ with lock:
+ with open(filepath) as f:
+ data = json.load(f)
- idea_id = str(uuid.uuid4())
- new_idea = Idea(
- id=idea_id,
- name=idea.name,
- description=idea.description,
- voters=[],
- start_time=idea.start_time,
- end_time=idea.end_time,
- )
+ idea_id = str(uuid.uuid4())
+ new_idea = Idea(
+ id=idea_id,
+ name=idea.name,
+ description=idea.description,
+ voters=[],
+ start_time=idea.start_time,
+ end_time=idea.end_time,
+ )
- data["ideas"].append(new_idea.dict())
- with open(filepath, "w") as f:
- json.dump(data, f, default=str)
+ data["ideas"].append(new_idea.model_dump())
+ with open(filepath, "w") as f:
+ json.dump(data, f, default=str)
return new_idea
@@ -147,17 +154,19 @@ def vote_idea(tour_id: str, idea_id: str, vote: Vote):
if not os.path.exists(filepath):
raise HTTPException(status_code=404, detail="Tour not found")
- with open(filepath) as f:
- data = json.load(f)
+ lock = FileLock(filepath + ".lock")
+ with lock:
+ with open(filepath) as f:
+ data = json.load(f)
- for idea in data["ideas"]:
- if idea["id"] == idea_id:
- if vote.voterName not in idea["voters"]:
- idea["voters"].append(vote.voterName)
- # Persist the change
- with open(filepath, "w") as f:
- json.dump(data, f, default=str)
- return Idea(**idea)
+ for idea in data["ideas"]:
+ if idea["id"] == idea_id:
+ if vote.voterName not in idea["voters"]:
+ idea["voters"].append(vote.voterName)
+ # Persist the change
+ with open(filepath, "w") as f:
+ json.dump(data, f, default=str)
+ return Idea(**idea)
raise HTTPException(status_code=404, detail="Idea not found")