implemented client side filters, mobile first styling and modals for articles
This commit is contained in:
@@ -22,6 +22,7 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
from fastapi import Depends, FastAPI, HTTPException, Response, status
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from backend.app.config import (
|
||||
CRON_HOURS,
|
||||
@@ -65,51 +66,117 @@ scheduler.start()
|
||||
@app.get("/news", response_model=List[Dict[str, Any]])
|
||||
async def get_news(
|
||||
country: str = "DE",
|
||||
from_: str = "2025-07-01",
|
||||
to_: str = datetime.now(timezone.utc).strftime("%Y-%m-%d"),
|
||||
from_: str = None,
|
||||
to_: str = None,
|
||||
timezone_name: str = "UTC",
|
||||
all_countries: bool = False,
|
||||
all_dates: bool = False,
|
||||
db: sqlite3.Cursor = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get news articles filtered by country and date range.
|
||||
Now optimized for concurrent access while scheduler is running.
|
||||
Now handles client timezone properly and supports multiple countries and all news.
|
||||
|
||||
Args:
|
||||
country: Country code to filter by (default: "DE")
|
||||
from_: Start date in ISO format (default: "2025-07-01")
|
||||
to: End date in ISO format (default: current date)
|
||||
country: Country code(s) to filter by (default: "DE").
|
||||
For multiple countries, use comma-separated values like "DE,US,GB"
|
||||
from_: Start date in ISO format (optional)
|
||||
to_: End date in ISO format (optional)
|
||||
timezone_name: Client timezone for date interpretation (default: "UTC")
|
||||
all_countries: If True, returns news from all countries (overrides country param)
|
||||
all_dates: If True, returns news from all dates (overrides date params)
|
||||
db: Database cursor dependency
|
||||
|
||||
Returns:
|
||||
List of news articles matching the criteria
|
||||
"""
|
||||
try:
|
||||
datetime.fromisoformat(from_)
|
||||
datetime.fromisoformat(to_)
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from_ts = int(datetime.fromisoformat(from_).timestamp())
|
||||
to_ts = int(datetime.fromisoformat(to_).timestamp())
|
||||
# Handle timezone
|
||||
try:
|
||||
client_tz = ZoneInfo(timezone_name)
|
||||
except Exception:
|
||||
logger.warning(f"Invalid timezone '{timezone_name}', using UTC")
|
||||
client_tz = timezone.utc
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
SELECT id, title, description, url, published, country, created_at
|
||||
FROM news
|
||||
WHERE country = ?
|
||||
AND published BETWEEN ? AND ?
|
||||
ORDER BY published DESC LIMIT 1000
|
||||
""",
|
||||
(country, from_ts, to_ts)
|
||||
)
|
||||
where_conditions = []
|
||||
params = []
|
||||
|
||||
return [dict(row) for row in db.fetchall()]
|
||||
if not all_countries:
|
||||
countries = [c.strip().upper() for c in country.split(',') if c.strip()]
|
||||
if len(countries) == 1:
|
||||
where_conditions.append("country = ?")
|
||||
params.append(countries[0])
|
||||
elif len(countries) > 1:
|
||||
placeholders = ','.join(['?' for _ in countries])
|
||||
where_conditions.append(f"country IN ({placeholders})")
|
||||
params.extend(countries)
|
||||
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
400, "Invalid date format. Use ISO format (YYYY-MM-DD)")
|
||||
if not all_dates and (from_ or to_):
|
||||
if not from_:
|
||||
from_ = "2025-01-01" # Default start date
|
||||
if not to_:
|
||||
to_ = datetime.now(timezone.utc).strftime("%Y-%m-%d") # Default to today
|
||||
|
||||
# Parse and convert dates
|
||||
from_date_naive = datetime.fromisoformat(from_)
|
||||
to_date_naive = datetime.fromisoformat(to_)
|
||||
|
||||
from_date = from_date_naive.replace(tzinfo=client_tz)
|
||||
to_date = to_date_naive.replace(tzinfo=client_tz)
|
||||
|
||||
# Include the entire end date
|
||||
to_date = to_date.replace(hour=23, minute=59, second=59)
|
||||
|
||||
from_ts = int(from_date.timestamp())
|
||||
to_ts = int(to_date.timestamp())
|
||||
|
||||
where_conditions.append("published BETWEEN ? AND ?")
|
||||
params.extend([from_ts, to_ts])
|
||||
|
||||
logger.info(f"Date range: {from_date} to {to_date} (UTC timestamps: {from_ts} to {to_ts})")
|
||||
|
||||
# Build the complete SQL query
|
||||
base_sql = """
|
||||
SELECT id, title, summary, url, published, country, created_at
|
||||
FROM news \
|
||||
"""
|
||||
|
||||
if where_conditions:
|
||||
sql = base_sql + " WHERE " + " AND ".join(where_conditions)
|
||||
else:
|
||||
sql = base_sql
|
||||
|
||||
sql += " ORDER BY published DESC LIMIT 1000"
|
||||
|
||||
# Log query info
|
||||
if all_countries and all_dates:
|
||||
logger.info("Querying ALL news articles (no filters)")
|
||||
elif all_countries:
|
||||
logger.info(f"Querying news from ALL countries with date filter")
|
||||
elif all_dates:
|
||||
logger.info(f"Querying ALL dates for countries: {country}")
|
||||
else:
|
||||
logger.info(f"Querying news: countries={country}, timezone={timezone_name}")
|
||||
|
||||
logger.info(f"SQL: {sql}")
|
||||
logger.info(f"Parameters: {params}")
|
||||
|
||||
# Execute the query
|
||||
db.execute(sql, params)
|
||||
rows = db.fetchall()
|
||||
result = [dict(row) for row in rows]
|
||||
|
||||
logger.info(f"Found {len(result)} news articles")
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(f"Date parsing error: {e}")
|
||||
raise HTTPException(400, "Invalid date format. Use ISO format (YYYY-MM-DD)")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error fetching news: {e}")
|
||||
raise HTTPException(
|
||||
500, "Internal server error while fetching news"
|
||||
)
|
||||
raise HTTPException(500, "Internal server error while fetching news")
|
||||
|
||||
|
||||
@app.get("/feeds", response_model=List[Dict[str, Any]])
|
||||
@@ -125,7 +192,7 @@ async def list_feeds(db: sqlite3.Cursor = Depends(get_db)):
|
||||
"""
|
||||
try:
|
||||
db.execute("SELECT * FROM feeds ORDER BY country, url")
|
||||
return [dict(row) for row in db.fetchall()]
|
||||
return JSONResponse(content=[dict(row) for row in db.fetchall()])
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error fetching feeds: {e}")
|
||||
raise HTTPException(
|
||||
@@ -133,23 +200,6 @@ async def list_feeds(db: sqlite3.Cursor = Depends(get_db)):
|
||||
)
|
||||
|
||||
|
||||
@app.get("/meta/last-sync", response_model=TimestampResponse)
|
||||
async def get_last_sync(db: sqlite3.Cursor = Depends(get_db)):
|
||||
"""
|
||||
Get the timestamp of the last successful feed synchronization.
|
||||
|
||||
Args:
|
||||
db: Database cursor dependency
|
||||
|
||||
Returns:
|
||||
Object containing the timestamp as a Unix epoch
|
||||
"""
|
||||
db.execute("SELECT val FROM meta WHERE key='last_sync'")
|
||||
row = db.fetchone()
|
||||
if row is None:
|
||||
import time
|
||||
return {"ts": int(time.time())}
|
||||
return {"ts": int(row["val"])}
|
||||
|
||||
|
||||
@app.post("/feeds", response_model=SuccessResponse)
|
||||
@@ -271,6 +321,25 @@ async def manual_sync(db: sqlite3.Cursor = Depends(get_db)):
|
||||
)
|
||||
|
||||
|
||||
@app.get("/meta/last-sync", response_model=TimestampResponse)
|
||||
async def get_last_sync(db: sqlite3.Cursor = Depends(get_db)):
|
||||
"""
|
||||
Get the timestamp of the last successful feed synchronization.
|
||||
|
||||
Args:
|
||||
db: Database cursor dependency
|
||||
|
||||
Returns:
|
||||
Object containing the timestamp as a Unix epoch
|
||||
"""
|
||||
db.execute("SELECT val FROM meta WHERE key='last_sync'")
|
||||
row = db.fetchone()
|
||||
if row is None:
|
||||
import time
|
||||
return {"ts": int(time.time())}
|
||||
return {"ts": int(row["val"])}
|
||||
|
||||
|
||||
@app.get("/settings/cron", response_model=HoursResponse)
|
||||
async def get_cron_schedule(db: sqlite3.Cursor = Depends(get_db)):
|
||||
"""
|
||||
|
Reference in New Issue
Block a user