diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..47183a7 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,155 @@ +import logging +import re +from typing import List + +import psycopg2 +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from psycopg2.extras import RealDictCursor +from pydantic import BaseModel, validator + +log = logging.getLogger(__name__) + +class CountryBands(BaseModel): + uuid: str + country: str + bands: List[str] + + @validator("uuid") + def is_uuid(cls, v): + assert re.match(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", v) + return v + +app = FastAPI() +db = psycopg2.connect(dbname="internationale") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=False, + allow_methods=["*"], + allow_headers=["*"], +) + +@app.post("/update") +def update_country(cb: CountryBands): + try: + with db.cursor(cursor_factory=RealDictCursor) as csr: + csr.execute( + "select * from entries where uuid = %s and country = %s", + (cb.uuid, cb.country)) + bands = [r["band"] for r in csr] + for b in cb.bands: + if b in bands: + bands.remove(b) + else: + csr.execute( + "select * from users where uuid = %s", + (cb.uuid,)) + if not csr.fetchall(): + csr.execute( + "insert into users(uuid, public_id) values(%s, %s)", + (cb.uuid, cb.uuid[24:],)) + csr.execute( + "insert into entries(uuid, country, band) values(%s, %s, %s)", + (cb.uuid, cb.country, b)) + for b in bands: + csr.execute( + """ + delete from entries + where uuid = %s and country = %s and band = %s + """, + (cb.uuid, cb.country, b)) + db.commit() + csr.execute( + "select count(distinct band) from entries where country = %s", + (cb.country,)) + count = csr.fetchone()["count"] + return count + except Exception as e: + log.exception("caught error") + db.rollback() + return HTTPException(500) + +@app.get("/countries/{lang}") +def get_countries(lang: str): + try: + with db.cursor(cursor_factory=RealDictCursor) as csr: + csr.execute( + """ + select * from countries join country_names using (code) + where lang = %s + order by population desc + """, + (lang,)) + return csr.fetchall() + except Exception as e: + log.exception("caught error") + db.rollback() + return HTTPException(500) + +@app.get("/stats/{id}/{lang}") +def get_stats(id: str, lang: str): + try: + with db.cursor(cursor_factory=RealDictCursor) as csr: + csr.execute( + """ + select country, name, + band, + count(*) as total, + count(*) filter(where public_id = %s) as by_user + from entries e + join countries c on c.code = e.country + join country_names cn on e.country = cn.code and cn.lang = %s + join users u using (uuid) + group by country, name, population, band + order by population desc, name, band + """, + (id, lang,)) + country_stats = [] + for r in csr: + if not country_stats or country_stats[-1]["code"] != r["country"]: + country_stats.append({"code": r["country"], + "name": r["name"], + "bands": []}) + country_stats[-1]["bands"].append({"name": r["band"], + "total_count": r["total"], + "by_user": bool(r["by_user"])}) + + stats = { "countries": country_stats } + csr.execute( + """ + with a as ( + select public_id, count(distinct(country)) as c + from entries join users using (uuid) + group by 1) + select public_id, c, rank() over (order by c desc) from a; + """ + ) + for r in csr: + if r["public_id"] == id: + stats["country_rank"] = r["rank"] + stats["country_count"] = r["c"] + stats["user_count"] = csr.rowcount + + csr.execute( + """ + with a as ( + select public_id, count(*) as c + from entries join users using (uuid) + group by 1) + select public_id, c, rank() over (order by c desc) from a; + """ + ) + for r in csr: + if r["public_id"] == id: + stats["country_rank"] = r["rank"] + stats["country_count"] = r["c"] + + return stats + except Exception as e: + log.exception("caught error") + db.rollback() + return HTTPException(500) + +# vim: tw=99 diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..379366c --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,3 @@ +fastapi +psycopg2 +uvicorn diff --git a/.gitignore b/frontend/.gitignore similarity index 100% rename from .gitignore rename to frontend/.gitignore diff --git a/README.md b/frontend/README.md similarity index 100% rename from README.md rename to frontend/README.md diff --git a/package-lock.json b/frontend/package-lock.json similarity index 100% rename from package-lock.json rename to frontend/package-lock.json diff --git a/package.json b/frontend/package.json similarity index 100% rename from package.json rename to frontend/package.json diff --git a/public/favicon.ico b/frontend/public/favicon.ico similarity index 100% rename from public/favicon.ico rename to frontend/public/favicon.ico diff --git a/public/guitar.png b/frontend/public/guitar.png similarity index 100% rename from public/guitar.png rename to frontend/public/guitar.png diff --git a/public/index.html b/frontend/public/index.html similarity index 100% rename from public/index.html rename to frontend/public/index.html diff --git a/public/logo192.png b/frontend/public/logo192.png similarity index 100% rename from public/logo192.png rename to frontend/public/logo192.png diff --git a/public/logo512.png b/frontend/public/logo512.png similarity index 100% rename from public/logo512.png rename to frontend/public/logo512.png diff --git a/public/manifest.json b/frontend/public/manifest.json similarity index 100% rename from public/manifest.json rename to frontend/public/manifest.json diff --git a/public/robots.txt b/frontend/public/robots.txt similarity index 100% rename from public/robots.txt rename to frontend/public/robots.txt diff --git a/src/App.css b/frontend/src/App.css similarity index 100% rename from src/App.css rename to frontend/src/App.css diff --git a/src/App.js b/frontend/src/App.js similarity index 100% rename from src/App.js rename to frontend/src/App.js diff --git a/src/App.test.js b/frontend/src/App.test.js similarity index 100% rename from src/App.test.js rename to frontend/src/App.test.js diff --git a/src/CountryInput.js b/frontend/src/CountryInput.js similarity index 100% rename from src/CountryInput.js rename to frontend/src/CountryInput.js diff --git a/src/Intro.js b/frontend/src/Intro.js similarity index 100% rename from src/Intro.js rename to frontend/src/Intro.js diff --git a/src/MontserratAlternates-Light.ttf b/frontend/src/MontserratAlternates-Light.ttf similarity index 100% rename from src/MontserratAlternates-Light.ttf rename to frontend/src/MontserratAlternates-Light.ttf diff --git a/src/MontserratAlternates-LightItalic.ttf b/frontend/src/MontserratAlternates-LightItalic.ttf similarity index 100% rename from src/MontserratAlternates-LightItalic.ttf rename to frontend/src/MontserratAlternates-LightItalic.ttf diff --git a/src/MontserratAlternates-Medium.ttf b/frontend/src/MontserratAlternates-Medium.ttf similarity index 100% rename from src/MontserratAlternates-Medium.ttf rename to frontend/src/MontserratAlternates-Medium.ttf diff --git a/src/MontserratAlternates-MediumItalic.ttf b/frontend/src/MontserratAlternates-MediumItalic.ttf similarity index 100% rename from src/MontserratAlternates-MediumItalic.ttf rename to frontend/src/MontserratAlternates-MediumItalic.ttf diff --git a/src/MontserratAlternates-Regular.ttf b/frontend/src/MontserratAlternates-Regular.ttf similarity index 100% rename from src/MontserratAlternates-Regular.ttf rename to frontend/src/MontserratAlternates-Regular.ttf diff --git a/src/Philosopher-Bold.ttf b/frontend/src/Philosopher-Bold.ttf similarity index 100% rename from src/Philosopher-Bold.ttf rename to frontend/src/Philosopher-Bold.ttf diff --git a/src/Philosopher-BoldItalic.ttf b/frontend/src/Philosopher-BoldItalic.ttf similarity index 100% rename from src/Philosopher-BoldItalic.ttf rename to frontend/src/Philosopher-BoldItalic.ttf diff --git a/src/Philosopher-Italic.ttf b/frontend/src/Philosopher-Italic.ttf similarity index 100% rename from src/Philosopher-Italic.ttf rename to frontend/src/Philosopher-Italic.ttf diff --git a/src/Philosopher-Regular.ttf b/frontend/src/Philosopher-Regular.ttf similarity index 100% rename from src/Philosopher-Regular.ttf rename to frontend/src/Philosopher-Regular.ttf diff --git a/src/Questionaire.js b/frontend/src/Questionaire.js similarity index 100% rename from src/Questionaire.js rename to frontend/src/Questionaire.js diff --git a/src/Stats.js b/frontend/src/Stats.js similarity index 100% rename from src/Stats.js rename to frontend/src/Stats.js diff --git a/src/guitar.png b/frontend/src/guitar.png similarity index 100% rename from src/guitar.png rename to frontend/src/guitar.png diff --git a/src/index.css b/frontend/src/index.css similarity index 100% rename from src/index.css rename to frontend/src/index.css diff --git a/src/index.js b/frontend/src/index.js similarity index 100% rename from src/index.js rename to frontend/src/index.js diff --git a/src/logo.svg b/frontend/src/logo.svg similarity index 100% rename from src/logo.svg rename to frontend/src/logo.svg diff --git a/src/reportWebVitals.js b/frontend/src/reportWebVitals.js similarity index 100% rename from src/reportWebVitals.js rename to frontend/src/reportWebVitals.js diff --git a/src/setupTests.js b/frontend/src/setupTests.js similarity index 100% rename from src/setupTests.js rename to frontend/src/setupTests.js