Initial commit
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
**/**.pyc
|
||||||
0
.projectile
Normal file
26
README.org
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#+title: Readme
|
||||||
|
|
||||||
|
* Tasques
|
||||||
|
** TODO Refactor organitzatiu
|
||||||
|
* Idees
|
||||||
|
** Jams
|
||||||
|
*** Properes jams
|
||||||
|
**** Data i lloc de les properes jams
|
||||||
|
**** Info dels temes que es tocaràn (a la slow jam)
|
||||||
|
*** Live jam
|
||||||
|
**** Quin tema està sonant ara
|
||||||
|
*** Historial de jams
|
||||||
|
**** Veure a cada jam passada quins temes s'han tocat
|
||||||
|
** Temes
|
||||||
|
*** Navegació
|
||||||
|
**** Cerca de temes
|
||||||
|
***** Per nom, tonalitat, compàs, tipus, (shazam!?!?!)
|
||||||
|
***** Per tocats recentment
|
||||||
|
**** Informació
|
||||||
|
***** Nom
|
||||||
|
***** Tonalitat i compàs
|
||||||
|
***** Vegades que s'ha tocat
|
||||||
|
* Referències
|
||||||
|
Fonts d'inspiració i altres materials de referència per fer la web de folkugat
|
||||||
|
** Simple Site
|
||||||
|
https://github.com/tataraba/simplesite
|
||||||
35
custom-python.nix
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{ pkgs }:
|
||||||
|
rec {
|
||||||
|
# Custom python package
|
||||||
|
jinja2-fragments = pkgs.python311Packages.buildPythonPackage rec {
|
||||||
|
pname = "jinja2_fragments";
|
||||||
|
version = "1.2.1";
|
||||||
|
|
||||||
|
format = "wheel";
|
||||||
|
src = pkgs.python311Packages.fetchPypi rec {
|
||||||
|
inherit pname version format;
|
||||||
|
sha256 = "c9255e5aadd2cb4e3fbd27530370af0b728a522f139455008febc5145d819f84";
|
||||||
|
dist = python;
|
||||||
|
python = "py3";
|
||||||
|
};
|
||||||
|
|
||||||
|
propagatedBuildInputs = [
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
pytailwindcss = pkgs.python311Packages.buildPythonPackage rec {
|
||||||
|
pname = "pytailwindcss";
|
||||||
|
version = "0.2.0";
|
||||||
|
|
||||||
|
format = "wheel";
|
||||||
|
src = pkgs.python311Packages.fetchPypi rec {
|
||||||
|
inherit pname version format;
|
||||||
|
sha256 = "30e7bd3b78de19a39a7e66329db02d393ee540f010924397b4d780e85041fae4";
|
||||||
|
dist = python;
|
||||||
|
python = "py3";
|
||||||
|
};
|
||||||
|
|
||||||
|
propagatedBuildInputs = [
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
53
flake.nix
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
description = "Development flake for the HTMX folkugat web";
|
||||||
|
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||||
|
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, flake-utils }:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system: let
|
||||||
|
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
|
||||||
|
python = pkgs.python311;
|
||||||
|
pythonPackages = pkgs.python311Packages;
|
||||||
|
|
||||||
|
# custom-python = import ./custom-python.nix { inherit pkgs; };
|
||||||
|
|
||||||
|
projectDependencies = with pythonPackages; [
|
||||||
|
# API
|
||||||
|
fastapi
|
||||||
|
jinja2
|
||||||
|
uvicorn
|
||||||
|
# Auth
|
||||||
|
pyjwt
|
||||||
|
# Search
|
||||||
|
levenshtein
|
||||||
|
# Test
|
||||||
|
pytest
|
||||||
|
];
|
||||||
|
|
||||||
|
in {
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
nativeBuildInputs = [ pkgs.bashInteractive ];
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
# Core python dependencies
|
||||||
|
python
|
||||||
|
pythonPackages.pip
|
||||||
|
pythonPackages.virtualenv
|
||||||
|
# Other dependencies
|
||||||
|
nodePackages_latest.tailwindcss
|
||||||
|
# IDE tools
|
||||||
|
pythonPackages.pylsp-mypy
|
||||||
|
pythonPackages.isort
|
||||||
|
pythonPackages.autopep8
|
||||||
|
nodePackages.pyright
|
||||||
|
# Development tools
|
||||||
|
black
|
||||||
|
pythonPackages.ipython
|
||||||
|
pythonPackages.pytest
|
||||||
|
pythonPackages.setuptools
|
||||||
|
] ++ projectDependencies;
|
||||||
|
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
0
folkugat_web/__init__.py
Normal file
6
folkugat_web/api/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from ._router import router
|
||||||
|
from .auth import *
|
||||||
|
from .index import *
|
||||||
|
from .sessions import *
|
||||||
|
from .tema import *
|
||||||
|
from .temes import *
|
||||||
4
folkugat_web/api/_router.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
|
||||||
|
router = APIRouter(default_response_class=HTMLResponse)
|
||||||
34
folkugat_web/api/auth.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from typing import Annotated, Optional
|
||||||
|
|
||||||
|
from fastapi import Cookie, Form, Request
|
||||||
|
from folkugat_web.api import router
|
||||||
|
from folkugat_web.config import auth as config
|
||||||
|
from folkugat_web.fragments import nota
|
||||||
|
from folkugat_web.services import auth as service
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/nota")
|
||||||
|
def nota_input(request: Request, value: Optional[str] = None):
|
||||||
|
return nota.input(request, value)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/nota")
|
||||||
|
def login(request: Request, value: Annotated[Optional[str], Form()] = None,
|
||||||
|
nota_folkugat: Annotated[Optional[str], Cookie()] = None):
|
||||||
|
logged_in = service.logged_in(nota_folkugat)
|
||||||
|
new_login = service.login(value)
|
||||||
|
if new_login and not logged_in:
|
||||||
|
response = nota.nota(request)
|
||||||
|
response.set_cookie(key=config.COOKIE_NAME,
|
||||||
|
value=service.build_token(),
|
||||||
|
max_age=config.COOKIE_MAX_AGE)
|
||||||
|
else:
|
||||||
|
response = nota.footer(request, value, logged_in)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/logout")
|
||||||
|
def logout(request: Request):
|
||||||
|
response = nota.nota(request)
|
||||||
|
response.delete_cookie(key=config.COOKIE_NAME)
|
||||||
|
return response
|
||||||
18
folkugat_web/api/index.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from fastapi import Request
|
||||||
|
from folkugat_web.api import router
|
||||||
|
from folkugat_web.services import auth
|
||||||
|
from folkugat_web.templates import templates
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/")
|
||||||
|
def index(request: Request, logged_in: auth.LoggedIn):
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"index.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"page_title": "Folkugat",
|
||||||
|
"content": "/api/content/sessions",
|
||||||
|
"logged_in": logged_in,
|
||||||
|
"animate": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
1
folkugat_web/api/sessions/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import editor, index, sessio
|
||||||
42
folkugat_web/api/sessions/editor.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import datetime
|
||||||
|
from typing import Annotated, Optional
|
||||||
|
|
||||||
|
from fastapi import Form, Request
|
||||||
|
from folkugat_web.api import router
|
||||||
|
from folkugat_web.fragments import sessions
|
||||||
|
from folkugat_web.services import auth
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/sessions/editor/")
|
||||||
|
def insert_row(request: Request, _: auth.RequireLogin):
|
||||||
|
return sessions.sessions_editor_insert_row(request)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/sessions/editor/{session_id}/")
|
||||||
|
def editor_row(request: Request, session_id: int, _: auth.RequireLogin):
|
||||||
|
return sessions.sessions_editor_row(request, session_id=session_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/api/sessions/editor/{session_id}/")
|
||||||
|
def modify_session(
|
||||||
|
request: Request, session_id: int,
|
||||||
|
_: auth.RequireLogin,
|
||||||
|
date: Annotated[datetime.date, Form()],
|
||||||
|
start_time: Annotated[datetime.time, Form()],
|
||||||
|
end_time: Annotated[datetime.time, Form()],
|
||||||
|
venue_name: Annotated[Optional[str], Form()] = None,
|
||||||
|
venue_url: Annotated[Optional[str], Form()] = None,
|
||||||
|
):
|
||||||
|
session_date = sessions.model.Session(id=session_id, date=date, start_time=start_time, end_time=end_time,
|
||||||
|
venue=sessions.model.SessionVenue(name=venue_name, url=venue_url))
|
||||||
|
return sessions.sessions_editor_post_row(request, session_date)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/api/sessions/editor/{session_id}/")
|
||||||
|
def delete_date(session_id: int, _: auth.RequireLogin):
|
||||||
|
return sessions.sessions_editor_delete_row(session_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/sessions/editor/{session_id}/edita")
|
||||||
|
def editor_row_editing(request: Request, session_id: int, _: auth.RequireLogin):
|
||||||
|
return sessions.sessions_editor_row_editing(request, session_id)
|
||||||
48
folkugat_web/api/sessions/index.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
from fastapi import Request
|
||||||
|
from folkugat_web.api import router
|
||||||
|
from folkugat_web.config import calendari as calendari_conf
|
||||||
|
from folkugat_web.fragments import live, sessions
|
||||||
|
from folkugat_web.services import auth
|
||||||
|
from folkugat_web.templates import templates
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sessions")
|
||||||
|
def page(request: Request, logged_in: auth.LoggedIn):
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"index.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"page_title": "Folkugat",
|
||||||
|
"content": "/api/content/sessions",
|
||||||
|
"logged_in": logged_in,
|
||||||
|
"animate": False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/content/sessions")
|
||||||
|
def content(request: Request, logged_in: auth.LoggedIn):
|
||||||
|
return sessions.sessions_pagina(request, logged_in)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/sessions/upcoming")
|
||||||
|
def calendari(
|
||||||
|
request: Request,
|
||||||
|
logged_in: auth.LoggedIn,
|
||||||
|
limit: int = calendari_conf.CALENDARI_PAGING_CONFIG.initial_items,
|
||||||
|
):
|
||||||
|
return sessions.sessions_calendari(request=request, limit=limit, logged_in=logged_in)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/sessions/history")
|
||||||
|
def history(
|
||||||
|
request: Request,
|
||||||
|
logged_in: auth.LoggedIn,
|
||||||
|
limit: int = calendari_conf.HISTORY_PAGING_CONFIG.initial_items,
|
||||||
|
):
|
||||||
|
return sessions.sessions_historial(request=request, limit=limit, logged_in=logged_in)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/sessions/live")
|
||||||
|
def get_live(request: Request):
|
||||||
|
return live.sessio_en_directe(request)
|
||||||
41
folkugat_web/api/sessions/sessio.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
from fastapi import Request
|
||||||
|
from folkugat_web.api import router
|
||||||
|
from folkugat_web.fragments import live, sessions
|
||||||
|
from folkugat_web.services import auth
|
||||||
|
from folkugat_web.templates import templates
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sessio/{session_id}")
|
||||||
|
def page(
|
||||||
|
request: Request,
|
||||||
|
session_id: int,
|
||||||
|
logged_in: auth.LoggedIn,
|
||||||
|
):
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"index.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"page_title": "Folkugat",
|
||||||
|
"content": f"/api/content/sessio/{session_id}",
|
||||||
|
"logged_in": logged_in,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/content/sessio/{session_id}")
|
||||||
|
def contingut(
|
||||||
|
request: Request,
|
||||||
|
session_id: int,
|
||||||
|
logged_in: auth.LoggedIn,
|
||||||
|
):
|
||||||
|
return sessions.sessio(request, session_id, logged_in)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/api/sessio/{session_id}/live")
|
||||||
|
def set_live(request: Request, session_id: int, _: auth.RequireLogin):
|
||||||
|
return live.start_live_session(request=request, session_id=session_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/api/sessio/{session_id}/live")
|
||||||
|
def stop_live(request: Request, session_id: int, _: auth.RequireLogin):
|
||||||
|
return live.stop_live_session(request=request, session_id=session_id)
|
||||||
1
folkugat_web/api/tema/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import editor, index
|
||||||
23
folkugat_web/api/tema/editor.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from fastapi import Request
|
||||||
|
from folkugat_web.api import router
|
||||||
|
from folkugat_web.fragments import tema
|
||||||
|
from folkugat_web.services import auth
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/tema/{tema_id}/editor/title")
|
||||||
|
def title_editor(
|
||||||
|
request: Request,
|
||||||
|
logged_in: auth.RequireLogin,
|
||||||
|
tema_id: int,
|
||||||
|
):
|
||||||
|
return tema.title_editor(request=request, logged_in=logged_in, tema_id=tema_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/tema/{tema_id}/editor/lyric/{lyric_idx}")
|
||||||
|
def lyric_editor(
|
||||||
|
request: Request,
|
||||||
|
logged_in: auth.RequireLogin,
|
||||||
|
tema_id: int,
|
||||||
|
lyric_idx: int,
|
||||||
|
):
|
||||||
|
return tema.lyric_editor(request=request, logged_in=logged_in, tema_id=tema_id, lyric_idx=lyric_idx)
|
||||||
84
folkugat_web/api/tema/index.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import Request
|
||||||
|
from fastapi.params import Form
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
from folkugat_web.api import router
|
||||||
|
from folkugat_web.fragments import tema, temes
|
||||||
|
from folkugat_web.model import temes as model
|
||||||
|
from folkugat_web.services import auth
|
||||||
|
from folkugat_web.services.temes import write as temes_w
|
||||||
|
from folkugat_web.templates import templates
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/tema/{tema_id}")
|
||||||
|
def page(request: Request, logged_in: auth.LoggedIn, tema_id: int):
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"index.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"page_title": "Folkugat",
|
||||||
|
"content": f"/api/tema/{tema_id}",
|
||||||
|
"logged_in": logged_in,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/tema/{tema_id}")
|
||||||
|
def contingut(request: Request, logged_in: auth.LoggedIn, tema_id: int):
|
||||||
|
return temes.tema(request, tema_id, logged_in)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/tema/{tema_id}/title")
|
||||||
|
def title(request: Request, logged_in: auth.LoggedIn, tema_id: int):
|
||||||
|
return tema.title(request=request, tema_id=tema_id, logged_in=logged_in)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/api/tema/{tema_id}/title")
|
||||||
|
def set_title(
|
||||||
|
request: Request,
|
||||||
|
logged_in: auth.RequireLogin,
|
||||||
|
tema_id: int,
|
||||||
|
title: Annotated[str, Form()],
|
||||||
|
):
|
||||||
|
new_tema = temes_w.update_title(tema_id=tema_id, title=title)
|
||||||
|
return tema.title(request=request, tema=new_tema, logged_in=logged_in)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/tema/{tema_id}/lyric/{lyric_idx}")
|
||||||
|
def lyric(request: Request, logged_in: auth.LoggedIn, tema_id: int, lyric_idx: int):
|
||||||
|
return tema.lyric(request=request, logged_in=logged_in, tema_id=tema_id, lyric_idx=lyric_idx)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/api/tema/{tema_id}/lyric/{lyric_idx}")
|
||||||
|
def set_lyric(
|
||||||
|
request: Request,
|
||||||
|
logged_in: auth.RequireLogin,
|
||||||
|
tema_id: int,
|
||||||
|
lyric_idx: int,
|
||||||
|
title: Annotated[str, Form()],
|
||||||
|
lyric: Annotated[str, Form()],
|
||||||
|
):
|
||||||
|
new_lyric = model.Lyrics(title=title, content=lyric.strip())
|
||||||
|
temes_w.update_lyric(tema_id=tema_id, lyric_idx=lyric_idx, lyric=new_lyric)
|
||||||
|
return tema.lyric(request=request, logged_in=logged_in, tema_id=tema_id, lyric_idx=lyric_idx)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/tema/{tema_id}/lyric")
|
||||||
|
def add_lyric(
|
||||||
|
request: Request,
|
||||||
|
logged_in: auth.RequireLogin,
|
||||||
|
tema_id: int,
|
||||||
|
):
|
||||||
|
new_tema = temes_w.add_lyric(tema_id=tema_id)
|
||||||
|
lyric_idx = len(new_tema.lyrics) - 1
|
||||||
|
return tema.lyric_editor(request=request, logged_in=logged_in, tema_id=tema_id, lyric_idx=lyric_idx)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/api/tema/{tema_id}/lyric/{lyric_idx}")
|
||||||
|
def delete_lyric(
|
||||||
|
tema_id: int,
|
||||||
|
lyric_idx: int,
|
||||||
|
):
|
||||||
|
temes_w.delete_lyric(tema_id=tema_id, lyric_idx=lyric_idx)
|
||||||
|
return HTMLResponse()
|
||||||
1
folkugat_web/api/temes/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import index
|
||||||
29
folkugat_web/api/temes/index.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from fastapi import Request
|
||||||
|
from folkugat_web.api import router
|
||||||
|
from folkugat_web.fragments import temes
|
||||||
|
from folkugat_web.services import auth
|
||||||
|
from folkugat_web.templates import templates
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/temes")
|
||||||
|
def page(request: Request, logged_in: auth.LoggedIn):
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"index.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"page_title": "Folkugat",
|
||||||
|
"content": "/api/content/temes",
|
||||||
|
"logged_in": logged_in,
|
||||||
|
"animate": False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/content/temes")
|
||||||
|
def content(request: Request, logged_in: auth.LoggedIn):
|
||||||
|
return temes.temes_pagina(request, logged_in)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/temes/busca")
|
||||||
|
def busca(request: Request, query: str, logged_in: auth.LoggedIn):
|
||||||
|
return temes.temes_busca(request, query, logged_in)
|
||||||
1028
folkugat_web/assets/static/css/main.css
Normal file
BIN
folkugat_web/assets/static/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
folkugat_web/assets/static/favicon/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
folkugat_web/assets/static/favicon/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
folkugat_web/assets/static/favicon/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
folkugat_web/assets/static/favicon/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 768 B |
BIN
folkugat_web/assets/static/favicon/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
folkugat_web/assets/static/favicon/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
folkugat_web/assets/static/favicon/favicon.png
Normal file
|
After Width: | Height: | Size: 105 KiB |
47
folkugat_web/assets/static/img/folkugat.svg
Normal file
|
After Width: | Height: | Size: 56 KiB |
1
folkugat_web/assets/static/js/htmx.min.js
vendored
Normal file
16883
folkugat_web/assets/static/js/pdf/pdf.mjs
Normal file
1
folkugat_web/assets/static/js/pdf/pdf.mjs.map
Normal file
241
folkugat_web/assets/static/js/pdf/pdf.sandbox.mjs
Normal file
1
folkugat_web/assets/static/js/pdf/pdf.sandbox.mjs.map
Normal file
56973
folkugat_web/assets/static/js/pdf/pdf.worker.mjs
Normal file
1
folkugat_web/assets/static/js/pdf/pdf.worker.mjs.map
Normal file
106
folkugat_web/assets/static/js/pdf_viewer.js
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
const pdfjsLib = await import('./pdf/pdf.mjs');
|
||||||
|
const pdfjsWorker = await import('./pdf/pdf.worker.mjs');
|
||||||
|
|
||||||
|
// The workerSrc property shall be specified.
|
||||||
|
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker;
|
||||||
|
|
||||||
|
|
||||||
|
export function displayPdf(options) {
|
||||||
|
var pdfDoc = null,
|
||||||
|
pageNum = 1,
|
||||||
|
pageRendering = false,
|
||||||
|
pageNumPending = null,
|
||||||
|
ctx = options.canvas.getContext('2d');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get page info from document, resize canvas accordingly, and render page.
|
||||||
|
* @param num Page number.
|
||||||
|
*/
|
||||||
|
function renderPage(num) {
|
||||||
|
pageRendering = true;
|
||||||
|
// Using promise to fetch the page
|
||||||
|
pdfDoc.getPage(num).then(function(page) {
|
||||||
|
var canvasWidth = options.canvas.getBoundingClientRect().width,
|
||||||
|
pageWidth = page.getViewport({scale: 1.0}).width,
|
||||||
|
scale = canvasWidth/pageWidth,
|
||||||
|
viewport = page.getViewport({scale: scale});
|
||||||
|
options.canvas.height = viewport.height;
|
||||||
|
options.canvas.width = viewport.width;
|
||||||
|
|
||||||
|
// Render PDF page into canvas context
|
||||||
|
var renderContext = {
|
||||||
|
canvasContext: ctx,
|
||||||
|
viewport: viewport
|
||||||
|
};
|
||||||
|
var renderTask = page.render(renderContext);
|
||||||
|
|
||||||
|
// Wait for rendering to finish
|
||||||
|
renderTask.promise.then(function() {
|
||||||
|
pageRendering = false;
|
||||||
|
if (pageNumPending !== null) {
|
||||||
|
// New page rendering is pending
|
||||||
|
renderPage(pageNumPending);
|
||||||
|
pageNumPending = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update page counters
|
||||||
|
options.pageNum.textContent = num;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If another page rendering in progress, waits until the rendering is
|
||||||
|
* finised. Otherwise, executes rendering immediately.
|
||||||
|
*/
|
||||||
|
function queueRenderPage(num) {
|
||||||
|
if (pageRendering) {
|
||||||
|
pageNumPending = num;
|
||||||
|
} else {
|
||||||
|
renderPage(num);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-render page (e.g. on resize)
|
||||||
|
*/
|
||||||
|
function reRender() {
|
||||||
|
queueRenderPage(pageNum);
|
||||||
|
}
|
||||||
|
document.defaultView.addEventListener('resize', reRender)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays previous page.
|
||||||
|
*/
|
||||||
|
function onPrevPage() {
|
||||||
|
if (pageNum <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pageNum--;
|
||||||
|
queueRenderPage(pageNum);
|
||||||
|
}
|
||||||
|
options.prevPage.addEventListener('click', onPrevPage);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays next page.
|
||||||
|
*/
|
||||||
|
function onNextPage() {
|
||||||
|
if (pageNum >= pdfDoc.numPages) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pageNum++;
|
||||||
|
queueRenderPage(pageNum);
|
||||||
|
}
|
||||||
|
options.nextPage.addEventListener('click', onNextPage);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously downloads PDF.
|
||||||
|
*/
|
||||||
|
pdfjsLib.getDocument(options.url).promise.then(function(pdfDoc_) {
|
||||||
|
pdfDoc = pdfDoc_;
|
||||||
|
options.pageCount.textContent = pdfDoc.numPages;
|
||||||
|
|
||||||
|
// Initial/first page rendering
|
||||||
|
renderPage(pageNum);
|
||||||
|
});
|
||||||
|
}
|
||||||
1
folkugat_web/assets/static/site.webmanifest
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||||
3
folkugat_web/assets/static/src/tw.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
7
folkugat_web/assets/templates/fragments/content.html
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<div id="content">
|
||||||
|
<div hx-get="{{ content }}"
|
||||||
|
hx-trigger="load"
|
||||||
|
hx-target="#content"
|
||||||
|
hx-swap="innerHTML">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
10
folkugat_web/assets/templates/fragments/footer.html
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<div id="footer">
|
||||||
|
<hr class="h-px my-3 bg-beige border-0 ">
|
||||||
|
<div id="nota" class="flex items-center justify-center">
|
||||||
|
<div hx-post="/api/nota"
|
||||||
|
hx-trigger="load"
|
||||||
|
hx-target="#nota"
|
||||||
|
hx-swap="innerHTML">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
5
folkugat_web/assets/templates/fragments/header.html
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<div class="h-4/5 min-h-[400px] flex flex-col items-center justify-center">
|
||||||
|
<img src = "{{ url_for('static', path='img/folkugat.svg') }}" class="{% if animate %} opacity-0 animate-fade-in-one {% endif %} m-3" width="100" alt="Folkugat"/>
|
||||||
|
<h1 class="text-3xl sm:text-8xl text-beige m-3 {% if animate %} opacity-0 animate-fade-in-one {% endif %}">{{ page_title }}</h1>
|
||||||
|
<h2 class="text-center sm:text-3xl m-6 {% if animate %} opacity-0 animate-fade-in-two {% endif %}">Sessions de folk a Sant Cugat</h2>
|
||||||
|
</div>
|
||||||
22
folkugat_web/assets/templates/fragments/marquee.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{% if session %}
|
||||||
|
<div class="bg-beige text-white mt-2 py-2 text-xl overflow-hidden">
|
||||||
|
<a href="/sessio/{{ session.id }}">
|
||||||
|
<div class="animate-marquee whitespace-nowrap">
|
||||||
|
{% for _ in (1, 2, 3) %}
|
||||||
|
<span class="inline-block mx-4">
|
||||||
|
Sessió en directe
|
||||||
|
</span>
|
||||||
|
<span class="inline-block mx-4">
|
||||||
|
♫
|
||||||
|
</span>
|
||||||
|
<span class="inline-block mx-4">
|
||||||
|
Està sonant <i>Pasdoble de Muntanya</i>
|
||||||
|
</span>
|
||||||
|
<span class="inline-block mx-4">
|
||||||
|
♫
|
||||||
|
</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
35
folkugat_web/assets/templates/fragments/menu.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<hr class="h-px bg-beige border-0">
|
||||||
|
<ul class="flex flex-col min-[450px]:flex-row items-center justify-center">
|
||||||
|
|
||||||
|
{% if menu_selected_id == Pages.Sessions %}
|
||||||
|
<li class="p-2 text-beige">
|
||||||
|
<button>
|
||||||
|
{% else %}
|
||||||
|
<li class="p-2">
|
||||||
|
<button hx-get="/api/content/sessions"
|
||||||
|
hx-replace-url="/sessions"
|
||||||
|
hx-target="#content">
|
||||||
|
{% endif %}
|
||||||
|
Sessions
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{% if menu_selected_id == Pages.Temes %}
|
||||||
|
<li class="p-2 text-beige">
|
||||||
|
<button>
|
||||||
|
{% else %}
|
||||||
|
<li class="p-2">
|
||||||
|
<button hx-get="/api/content/temes"
|
||||||
|
hx-replace-url="/temes"
|
||||||
|
hx-target="#content">
|
||||||
|
{% endif %}
|
||||||
|
Temes
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
<hr class="h-px bg-beige border-0">
|
||||||
|
<div hx-get="/api/sessions/live"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-trigger="load, every 20s, reload-marquee from:body">
|
||||||
|
</div>
|
||||||
32
folkugat_web/assets/templates/fragments/nota/footer.html
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{% if logged_in %}
|
||||||
|
|
||||||
|
<button class="p-12"
|
||||||
|
hx-post="/api/logout"
|
||||||
|
hx-target="#nota">
|
||||||
|
😎
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
<button class="p-12"
|
||||||
|
hx-get="/api/nota?value={{value or ''}}"
|
||||||
|
hx-target="#nota"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-include="#nota-input">
|
||||||
|
♫
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{% if value %}
|
||||||
|
<div class="p-12">
|
||||||
|
{{ value }}
|
||||||
|
</div>
|
||||||
|
<button class="p-12"
|
||||||
|
hx-get="/api/nota?value={{value or ''}}"
|
||||||
|
hx-target="#nota"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-include="#nota-input">
|
||||||
|
♫
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
20
folkugat_web/assets/templates/fragments/nota/input.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<button class="p-12"
|
||||||
|
hx-post="/api/nota"
|
||||||
|
hx-target="#nota"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-include="#nota-input"
|
||||||
|
hx-trigger="clicked, keyup[keyCode==13] from:#nota-input">
|
||||||
|
♫
|
||||||
|
</button>
|
||||||
|
<input id="nota-input" type="text" name="value" value="{{value or ''}}"
|
||||||
|
class="rounded border border-yellow-50 focus:outline-none
|
||||||
|
text-yellow-50 text-center
|
||||||
|
animate-grow max-w-xs
|
||||||
|
bg-brown p-1 m-1">
|
||||||
|
<button class="p-12"
|
||||||
|
hx-post="/api/nota"
|
||||||
|
hx-target="#nota"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-include="#nota-input">
|
||||||
|
♫
|
||||||
|
</button>
|
||||||
5
folkugat_web/assets/templates/fragments/nota/nota.html
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<div hx-post="/api/nota"
|
||||||
|
hx-trigger="load"
|
||||||
|
hx-target="#nota"
|
||||||
|
hx-swap="innerHTML">
|
||||||
|
</div>
|
||||||
40
folkugat_web/assets/templates/fragments/pdf_viewer.html
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<!-- PDF Viewer -->
|
||||||
|
<script type="module">
|
||||||
|
const pdfViewer = await import("{{ url_for('static', path='js/pdf_viewer.js') }}");
|
||||||
|
const options = {
|
||||||
|
url: '{{ pdf_url }}',
|
||||||
|
// PDF Canvas (where the pdf will be displayed)
|
||||||
|
canvas: document.getElementById('the-canvas'),
|
||||||
|
// Navigation elements
|
||||||
|
prevPage: document.getElementById('prev'),
|
||||||
|
nextPage: document.getElementById('next'),
|
||||||
|
// Page elements
|
||||||
|
pageCount: document.getElementById('page_count'),
|
||||||
|
pageNum: document.getElementById('page_num'),
|
||||||
|
}
|
||||||
|
pdfViewer.displayPdf(options);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center justify-center">
|
||||||
|
<!-- <div class="text-beige"> -->
|
||||||
|
<!-- <a href="{{ pdf_url }}" -->
|
||||||
|
<!-- target="_blank"> -->
|
||||||
|
<!-- <i class="fa fa-file-pdf" aria-hidden="true"></i> -->
|
||||||
|
<!-- obrir -->
|
||||||
|
<!-- </a> -->
|
||||||
|
<!-- </div> -->
|
||||||
|
<div class="flex flex-row flex-nowrap items-center justify-center w-full">
|
||||||
|
<button id="prev" class="flex-none text-xl text-beige mx-3">
|
||||||
|
<i class="fa fa-chevron-left" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<canvas class="flex-auto m-2 w-1/2 max-w-3xl"
|
||||||
|
id="the-canvas">
|
||||||
|
</canvas>
|
||||||
|
<button id="next" class="flex-none text-xl text-beige mx-3">
|
||||||
|
<i class="fa fa-chevron-right" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span><span id="page_num">?</span> / <span id="page_count">?</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<button title="Inicia una sessió"
|
||||||
|
class="text-beige mx-1"
|
||||||
|
hx-put="/api/sessio/{{ session.id }}/live"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fa fa-play" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<button title="Atura una sessió"
|
||||||
|
class="text-beige mx-1"
|
||||||
|
hx-delete="/api/sessio/{{ session.id }}/live"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fa fa-stop" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
25
folkugat_web/assets/templates/fragments/sessions/pagina.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{% include "fragments/menu.html" %}
|
||||||
|
<div class="p-12 text-center">
|
||||||
|
<h3 class="text-3xl text-beige p-4">Calendari</h3>
|
||||||
|
{% if logged_in %}
|
||||||
|
<button title="Afegeix una sessió"
|
||||||
|
class="text-beige m-2"
|
||||||
|
hx-post="/api/sessions/editor/"
|
||||||
|
hx-target="#{{ calendar_list_id }}"
|
||||||
|
hx-swap="beforeend transition:true">
|
||||||
|
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||||
|
Afegeix una sessió
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
<div hx-get="/api/sessions/upcoming"
|
||||||
|
hx-trigger="load"
|
||||||
|
hx-target="this"
|
||||||
|
hx-swap="innerHTML">
|
||||||
|
</div>
|
||||||
|
<h3 class="text-3xl text-beige p-4">Historial</h3>
|
||||||
|
<div hx-get="/api/sessions/history"
|
||||||
|
hx-trigger="load"
|
||||||
|
hx-target="this"
|
||||||
|
hx-swap="innerHTML">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
33
folkugat_web/assets/templates/fragments/sessions/sessio.html
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{% include "fragments/menu.html" %}
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="m-12 grow max-w-4xl text-center">
|
||||||
|
<h3 class="text-3xl p-4">
|
||||||
|
{% set dn = date_names(session.date) %}
|
||||||
|
{{ dn.day_name }} {{ dn.day }} {{ dn.month_name }} de {{ dn.year }}
|
||||||
|
</h3>
|
||||||
|
{% if logged_in %}
|
||||||
|
{% if session.is_live %}
|
||||||
|
{% include "fragments/sessions/live/stop.html" %}
|
||||||
|
{% else %}
|
||||||
|
{% include "fragments/sessions/live/start.html" %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
<div class="text-left">
|
||||||
|
<h4 class="text-xl text-beige">Horari i lloc</h4>
|
||||||
|
De {{ session.start_time.strftime("%H:%M") }}
|
||||||
|
a {{ session.end_time.strftime("%H:%M") }}
|
||||||
|
{% if session.venue.name %}
|
||||||
|
a
|
||||||
|
{% if session.venue.url %}
|
||||||
|
<a href="{{ session.venue.url }}"
|
||||||
|
class="text-beige"
|
||||||
|
target="_blank">
|
||||||
|
{{ session.venue.name }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
{{ session.venue.name }}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<p>
|
||||||
|
{% if session == None %}
|
||||||
|
<a> L'últim Dimecres de cada mes </a>
|
||||||
|
{% else %}
|
||||||
|
<a>
|
||||||
|
{% set dn = date_names(session.date) %}
|
||||||
|
{{ dn.day_name }} {{ dn.day }} {{ dn.month_name }}
|
||||||
|
</a>
|
||||||
|
de {{ session.start_time.strftime("%H:%M") }}
|
||||||
|
a {{ session.end_time.strftime("%H:%M") }}
|
||||||
|
{% if session.venue.name %}
|
||||||
|
a {{ session.venue.name }}
|
||||||
|
<!-- {% if session.venue.url %} -->
|
||||||
|
<!-- <a href="{{ session.venue.url }}" -->
|
||||||
|
<!-- class="text-beige" -->
|
||||||
|
<!-- target="_blank"> -->
|
||||||
|
<!-- {{ session.venue.name }} -->
|
||||||
|
<!-- </a> -->
|
||||||
|
<!-- {% else %} -->
|
||||||
|
<!-- {{ session.venue.name }} -->
|
||||||
|
<!-- {% endif %} -->
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<ol id="{{ session_list_id }}"
|
||||||
|
class="flex flex-col items-center justify-center">
|
||||||
|
{% for session in sessions %}
|
||||||
|
{% include "fragments/sessions/session_row.html" %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if more_sessions or less_sessions %}
|
||||||
|
<li>
|
||||||
|
{% if more_sessions %}
|
||||||
|
<button title="Mostra més sessions"
|
||||||
|
class="text-beige mx-2"
|
||||||
|
hx-get="{{ get_sessions_url }}?limit={{ more_sessions }}"
|
||||||
|
hx-target="#{{ session_list_id }}"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% if less_sessions %}
|
||||||
|
<button title="Mostra menys sessions"
|
||||||
|
class="text-beige mx-2"
|
||||||
|
hx-get="{{ get_sessions_url }}?limit={{ less_sessions }}"
|
||||||
|
hx-target="#{{ session_list_id }}"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fa fa-caret-up" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ol>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<li class="border rounded border-beige
|
||||||
|
flex flex-row grow
|
||||||
|
p-2 m-2 w-full max-w-xl
|
||||||
|
relative"
|
||||||
|
id="session-row-{{session.id}}">
|
||||||
|
<a href="/session/{{session.id}}">
|
||||||
|
<div class="flex flex-row grow justify-center pl-10">
|
||||||
|
<div class="flex-1">
|
||||||
|
{% include "fragments/sessions/session_date.html" %}
|
||||||
|
</div>
|
||||||
|
<div class="ml-auto">
|
||||||
|
<a title="Més informació"
|
||||||
|
class="text-beige mx-1"
|
||||||
|
href="/sessio/{{session.id}}/">
|
||||||
|
<i class="fa fa-info-circle" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
{% if logged_in %}
|
||||||
|
<button title="Edita la sessió"
|
||||||
|
class="text-beige mx-1"
|
||||||
|
hx-get="/api/sessions/editor/{{session.id}}/edita"
|
||||||
|
hx-target="#session-row-{{session.id}}"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fa fa-pencil" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button title="Esborra la sessió"
|
||||||
|
class="text-beige mx-1"
|
||||||
|
hx-delete="/api/sessions/editor/{{session.id}}/"
|
||||||
|
hx-target="#session-row-{{session.id}}"
|
||||||
|
hx-swap="outerHTML swap:0.5s">
|
||||||
|
<i class="fa fa-times" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if session.id == next_session_id %}
|
||||||
|
<div class="absolute -top-5 -left-6
|
||||||
|
bg-beige text-xs font-bold
|
||||||
|
px-2 py-1
|
||||||
|
transition-opacity duration-200
|
||||||
|
rounded">
|
||||||
|
Propera<br>sessió
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<li id="session-editor-row-{{session.id}}"
|
||||||
|
class="border rounded border-beige
|
||||||
|
flex flex-col grow
|
||||||
|
py-2 px-10 m-2 w-full max-w-xl">
|
||||||
|
<form>
|
||||||
|
<div class="flex flex-row justify-end items-center">
|
||||||
|
<button title="Aplica els canvis"
|
||||||
|
class="text-beige mx-2"
|
||||||
|
hx-put="/api/sessions/editor/{{session.id}}/"
|
||||||
|
hx-target="#session-editor-row-{{session.id}}"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fa fa-check" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button title="Descarta els canvis"
|
||||||
|
class="text-beige mx-2"
|
||||||
|
hx-get="/api/sessions/editor/{{session.id}}/"
|
||||||
|
hx-target="#session-editor-row-{{session.id}}"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fa fa-times" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-start pl-5">
|
||||||
|
<div>
|
||||||
|
<label for="session-editor-row-{{session.id}}-date">
|
||||||
|
Data:
|
||||||
|
</label>
|
||||||
|
<input type="date" name="date"
|
||||||
|
value="{{ session.date.strftime('%Y-%m-%d') }}"
|
||||||
|
min="2022-01-01"
|
||||||
|
max="2050-12-31"
|
||||||
|
class="border-none focus:outline-none
|
||||||
|
text-yellow-50 text-center
|
||||||
|
bg-brown p-0 m-0
|
||||||
|
session-editor-{{session.id}}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="session-editor-row-{{session.id}}-start-time">
|
||||||
|
Hora d'inici:
|
||||||
|
</label>
|
||||||
|
<input type="time" name="start_time"
|
||||||
|
value="{{ session.start_time.strftime('%H:%M') }}"
|
||||||
|
step="60"
|
||||||
|
class="border-none focus:outline-none
|
||||||
|
text-yellow-50 text-center
|
||||||
|
bg-brown p-0 m-0
|
||||||
|
session-editor-{{session.id}}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="session-editor-row-{{session.id}}-end-time">
|
||||||
|
Hora final:
|
||||||
|
</label>
|
||||||
|
<input type="time" name="end_time"
|
||||||
|
value="{{ session.end_time.strftime('%H:%M') }}"
|
||||||
|
step="60"
|
||||||
|
class="border-none focus:outline-none
|
||||||
|
text-yellow-50 text-center
|
||||||
|
bg-brown p-0 m-0
|
||||||
|
session-editor-{{session.id}}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="session-editor-row-{{session.id}}-venue-name">
|
||||||
|
Lloc:
|
||||||
|
</label>
|
||||||
|
<input list="llocs" name="venue_name"
|
||||||
|
placeholder="nom del lloc"
|
||||||
|
value="{{ session.venue.name or "" }}"
|
||||||
|
class="border border-yellow-50 focus:outline-none
|
||||||
|
rounded
|
||||||
|
text-yellow-50 text-center
|
||||||
|
bg-brown p-0 m-0
|
||||||
|
session-editor-{{session.id}}"
|
||||||
|
/>
|
||||||
|
<datalist id="llocs">
|
||||||
|
<option value="la FEM">
|
||||||
|
<option value="Cal Temerari">
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="session-editor-row-{{session.id}}-venue-url">
|
||||||
|
Link:
|
||||||
|
</label>
|
||||||
|
<input placeholder="URL del lloc" name="venue_url"
|
||||||
|
value="{{ session.venue.url or "" }}"
|
||||||
|
class="border border-yellow-50 focus:outline-none
|
||||||
|
rounded
|
||||||
|
text-yellow-30 text-center
|
||||||
|
bg-brown p-0 m-0 my-1
|
||||||
|
session-editor-{{session.id}}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<form id="tema-lyric-{{ lyric_idx }}">
|
||||||
|
<h5 class="text-sm text-beige text-right">
|
||||||
|
<input name="title"
|
||||||
|
placeholder="Nom de la lletra"
|
||||||
|
value="{{ lyric.title }}"
|
||||||
|
class="border border-beige focus:outline-none
|
||||||
|
rounded
|
||||||
|
bg-brown px-2 "
|
||||||
|
/>
|
||||||
|
<button title="Desa els canvis"
|
||||||
|
class="mx-1"
|
||||||
|
hx-put="/api/tema/{{ tema.id }}/lyric/{{ lyric_idx }}"
|
||||||
|
hx-target="#tema-lyric-{{ lyric_idx }}"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fa fa-check" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button title="Descarta els canvis"
|
||||||
|
class="mx-1"
|
||||||
|
hx-get="/api/tema/{{ tema.id }}/lyric/{{ lyric_idx }}"
|
||||||
|
hx-target="#tema-lyric-{{ lyric_idx }}"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fa fa-times" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</h5>
|
||||||
|
<hr class="h-px mt-1 mb-3 bg-beige border-0">
|
||||||
|
<textarea name="lyric"
|
||||||
|
placeholder="Lletra"
|
||||||
|
rows="{{ lyric.content.count('\n') + 1 }}"
|
||||||
|
class="border border-beige focus:outline-none
|
||||||
|
w-full text-center
|
||||||
|
rounded
|
||||||
|
bg-brown p-2 m-0">
|
||||||
|
{{ lyric.content }}
|
||||||
|
</textarea>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<div class="flex flex-row flex-wrap justify-center"
|
||||||
|
id="tema-title">
|
||||||
|
<form>
|
||||||
|
<input name="title"
|
||||||
|
placeholder="Nom del tema"
|
||||||
|
value="{{ tema.title }}"
|
||||||
|
class="border border-beige focus:outline-none
|
||||||
|
rounded text-3xl
|
||||||
|
bg-brown p-2 m-0"
|
||||||
|
/>
|
||||||
|
<button title="Desa"
|
||||||
|
class="text-beige text-3xl mx-1"
|
||||||
|
hx-put="/api/tema/{{ tema.id }}/title"
|
||||||
|
hx-target="#tema-title"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fa fa-check" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button title="Descarta"
|
||||||
|
class="text-beige text-3xl mx-1"
|
||||||
|
hx-get="/api/tema/{{ tema.id }}/title"
|
||||||
|
hx-target="#tema-title"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fa fa-times" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
21
folkugat_web/assets/templates/fragments/tema/link.html
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<li class="flex flex-row items-start">
|
||||||
|
<div class="p-2 m-2 text-beige border border-beige rounded-md">
|
||||||
|
<a href="{{ link.url }}" target="_blank">
|
||||||
|
{% if link.subtype == LinkSubtype.SPOTIFY %}
|
||||||
|
{% include "icons/spotify.svg" %}
|
||||||
|
{% elif link.subtype == LinkSubtype.YOUTUBE %}
|
||||||
|
{% include "icons/youtube.svg" %}
|
||||||
|
{% elif link.subtype == LinkSubtype.PDF %}
|
||||||
|
{% include "icons/pdf.svg" %}
|
||||||
|
{% elif link.type == LinkType.AUDIO %}
|
||||||
|
{% include "icons/notes.svg" %}
|
||||||
|
{% else %}
|
||||||
|
{% include "icons/link.svg" %}
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="my-2">
|
||||||
|
<p class="text-sm text-beige">Partitura</p>
|
||||||
|
<p>Hola</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
27
folkugat_web/assets/templates/fragments/tema/links.html
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{% if logged_in or tema.links %}
|
||||||
|
<h4 class="text-xl text-beige">Enllaços</h4>
|
||||||
|
<hr class="h-px mt-1 mb-3 bg-beige border-0">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if tema.links %}
|
||||||
|
{% for link in tema.links %}
|
||||||
|
<ul class="flex flex-col justify-center"
|
||||||
|
id="new-link-target">
|
||||||
|
{% set link_idx = loop.index0 %}
|
||||||
|
{% include "fragments/tema/link.html" %}
|
||||||
|
</ul>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if logged_in %}
|
||||||
|
<div class="flex flex-row my-2 justify-end">
|
||||||
|
<button title="Afegeix una lletra"
|
||||||
|
class="text-sm text-beige text-right"
|
||||||
|
hx-post="/api/tema/{{ tema.id }}/link"
|
||||||
|
hx-target="#new-link-target"
|
||||||
|
hx-swap="beforebegin">
|
||||||
|
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||||
|
Afegeix un enllaç
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
25
folkugat_web/assets/templates/fragments/tema/lyric.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<div id="tema-lyric-{{ lyric_idx }}">
|
||||||
|
<h5 class="text-sm text-beige text-right">
|
||||||
|
{{ lyric.title }}
|
||||||
|
{% if logged_in %}
|
||||||
|
<button title="Modifica la lletra"
|
||||||
|
class="mx-1"
|
||||||
|
hx-get="/api/tema/{{ tema.id }}/editor/lyric/{{ lyric_idx }}"
|
||||||
|
hx-target="#tema-lyric-{{ lyric_idx }}"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fa fa-pencil" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button title="Esborra la lletra"
|
||||||
|
class="mx-1"
|
||||||
|
hx-delete="/api/tema/{{ tema.id }}/lyric/{{ lyric_idx }}"
|
||||||
|
hx-target="#tema-lyric-{{ lyric_idx }}"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fa fa-times" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</h5>
|
||||||
|
<hr class="h-px mt-1 mb-3 bg-beige border-0">
|
||||||
|
<div class="text-center">
|
||||||
|
{{ lyric.content.replace('\n', '<br>') | safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
24
folkugat_web/assets/templates/fragments/tema/lyrics.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{% if logged_in or tema.lyrics %}
|
||||||
|
<h4 class="text-xl text-beige">Lletra</h4>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if tema.lyrics %}
|
||||||
|
{% for lyric in tema.lyrics %}
|
||||||
|
{% set lyric_idx = loop.index0 %}
|
||||||
|
{% include "fragments/tema/lyric.html" %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if logged_in %}
|
||||||
|
<div id="new-lyric-target"></div>
|
||||||
|
<div class="flex flex-row my-2 justify-end">
|
||||||
|
<button title="Afegeix una lletra"
|
||||||
|
class="text-sm text-beige text-right"
|
||||||
|
hx-post="/api/tema/{{ tema.id }}/lyric"
|
||||||
|
hx-target="#new-lyric-target"
|
||||||
|
hx-swap="beforebegin">
|
||||||
|
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||||
|
Afegeix una lletra
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
34
folkugat_web/assets/templates/fragments/tema/pagina.html
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{% include "fragments/menu.html" %}
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="m-12 grow max-w-4xl text-center">
|
||||||
|
{% include "fragments/tema/title.html" %}
|
||||||
|
<div class="text-left">
|
||||||
|
|
||||||
|
<!-- {% if tema.scores() %} -->
|
||||||
|
<!-- temes -->
|
||||||
|
<!-- <h4 class="text-xl text-beige">Tema</h4> -->
|
||||||
|
<!-- <hr class="h-px mt-1 mb-3 bg-beige border-0"> -->
|
||||||
|
<!-- {% for file in tema.files %} -->
|
||||||
|
<!-- {% if file.type == FileType.PDF %} -->
|
||||||
|
<!-- {% set pdf_url = file.path %} -->
|
||||||
|
<!-- {% include "fragments/pdf_viewer.html" %} -->
|
||||||
|
<!-- {% endif %} -->
|
||||||
|
<!-- {% endfor %} -->
|
||||||
|
<!-- {% endif %} -->
|
||||||
|
|
||||||
|
{% include "fragments/tema/lyrics.html" %}
|
||||||
|
{% include "fragments/tema/links.html" %}
|
||||||
|
|
||||||
|
<!-- PROPERTIES -->
|
||||||
|
{% if tema.properties %}
|
||||||
|
<h4 class="text-xl mt-3 text-beige">Informació</h4>
|
||||||
|
<hr class="h-px mt-1 mb-3 bg-beige border-0">
|
||||||
|
{% for property in tema.properties %}
|
||||||
|
<p>
|
||||||
|
<i>{{ property.field.value.capitalize() }}</i>: {{ property.value }}
|
||||||
|
</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
13
folkugat_web/assets/templates/fragments/tema/title.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<div class="flex flex-row flex-wrap justify-center"
|
||||||
|
id="tema-title">
|
||||||
|
<h3 class="text-3xl p-4">{{ tema.title }}</h3>
|
||||||
|
{% if logged_in %}
|
||||||
|
<button title="Canvia el títol"
|
||||||
|
class="text-beige text-2xl"
|
||||||
|
hx-get="/api/tema/{{ tema.id }}/editor/title"
|
||||||
|
hx-target="#tema-title"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fa fa-pencil" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
24
folkugat_web/assets/templates/fragments/temes/pagina.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{% include "fragments/menu.html" %}
|
||||||
|
<div class="p-12 text-center flex flex-col items-center justify-center">
|
||||||
|
<h3 class="text-3xl text-beige p-4">Temes</h3>
|
||||||
|
<button title="Afegeix un tema"
|
||||||
|
class="text-beige m-2">
|
||||||
|
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||||
|
Afegeix un tema
|
||||||
|
</button>
|
||||||
|
<input type="text" name="query" value=""
|
||||||
|
placeholder="Busca una tema..."
|
||||||
|
class="rounded
|
||||||
|
text-yellow-50 bg-brown
|
||||||
|
border border-yellow-50
|
||||||
|
p-2 m-2"
|
||||||
|
hx-get="/api/temes/busca"
|
||||||
|
hx-trigger="revealed, keyup delay:500ms changed"
|
||||||
|
hx-target="#search-results">
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div id="search-results"
|
||||||
|
class="m-4 max-w-5xl
|
||||||
|
flex flex-wrap items-center justify-center">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
44
folkugat_web/assets/templates/fragments/temes/result.html
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<div class="flex-none px-4 py-2 w-80 h-80 m-2 border-solid border rounded-md bg-yellow-50 text-brown">
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<div class="text-xl text-beige">
|
||||||
|
<a href="/tema/{{ tema.id }}">
|
||||||
|
{{ tema.title }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="grow"></div>
|
||||||
|
<div class="flex flex-row">
|
||||||
|
</div>
|
||||||
|
<ul class="flex flex-row items-center justify-center">
|
||||||
|
{% if tema.links %}
|
||||||
|
{% for link in tema.links %}
|
||||||
|
<li class="p-2 m-2 text-beige border border-beige rounded-md">
|
||||||
|
<a href="{{ link.url }}" target="_blank">
|
||||||
|
{% if link.type == LinkType.AUDIO %}
|
||||||
|
{% if link.subtype == LinkSubtype.SPOTIFY %}
|
||||||
|
{% include "icons/spotify.svg" %}
|
||||||
|
{% elif link.subtype == LinkSubtype.YOUTUBE %}
|
||||||
|
{% include "icons/youtube.svg" %}
|
||||||
|
{% else %}
|
||||||
|
{% include "icons/notes.svg" %}
|
||||||
|
{% endif %}
|
||||||
|
{% elif link.type == LinkType.SCORE %}
|
||||||
|
{% if link.subtype == LinkSubtype.PDF %}
|
||||||
|
{% include "icons/pdf.svg" %}
|
||||||
|
{% else %}
|
||||||
|
{% include "icons/link.svg" %}
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% include "icons/link.svg" %}
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
<!-- {% if logged_in %} -->
|
||||||
|
<!-- <div> -->
|
||||||
|
<!-- Edita -->
|
||||||
|
<!-- </div> -->
|
||||||
|
<!-- {% endif %} -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
4
folkugat_web/assets/templates/icons/link.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-link-45deg" viewBox="0 0 16 16">
|
||||||
|
<path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1.002 1.002 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4.018 4.018 0 0 1-.128-1.287z"/>
|
||||||
|
<path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 531 B |
5
folkugat_web/assets/templates/icons/notes.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-music-note-beamed" viewBox="0 0 16 16">
|
||||||
|
<path d="M6 13c0 1.105-1.12 2-2.5 2S1 14.105 1 13c0-1.104 1.12-2 2.5-2s2.5.896 2.5 2m9-2c0 1.105-1.12 2-2.5 2s-2.5-.895-2.5-2 1.12-2 2.5-2 2.5.895 2.5 2"/>
|
||||||
|
<path fill-rule="evenodd" d="M14 11V2h1v9zM6 3v10H5V3z"/>
|
||||||
|
<path d="M5 2.905a1 1 0 0 1 .9-.995l8-.8a1 1 0 0 1 1.1.995V3L5 4z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 426 B |
4
folkugat_web/assets/templates/icons/pdf.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-file-earmark-pdf-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M5.523 12.424c.14-.082.293-.162.459-.238a7.878 7.878 0 0 1-.45.606c-.28.337-.498.516-.635.572a.266.266 0 0 1-.035.012.282.282 0 0 1-.026-.044c-.056-.11-.054-.216.04-.36.106-.165.319-.354.647-.548m2.455-1.647c-.119.025-.237.05-.356.078a21.148 21.148 0 0 0 .5-1.05 12.045 12.045 0 0 0 .51.858c-.217.032-.436.07-.654.114m2.525.939a3.881 3.881 0 0 1-.435-.41c.228.005.434.022.612.054.317.057.466.147.518.209a.095.095 0 0 1 .026.064.436.436 0 0 1-.06.2.307.307 0 0 1-.094.124.107.107 0 0 1-.069.015c-.09-.003-.258-.066-.498-.256M8.278 6.97c-.04.244-.108.524-.2.829a4.86 4.86 0 0 1-.089-.346c-.076-.353-.087-.63-.046-.822.038-.177.11-.248.196-.283a.517.517 0 0 1 .145-.04c.013.03.028.092.032.198.005.122-.007.277-.038.465z"/>
|
||||||
|
<path fill-rule="evenodd" d="M4 0h5.293A1 1 0 0 1 10 .293L13.707 4a1 1 0 0 1 .293.707V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2m5.5 1.5v2a1 1 0 0 0 1 1h2zM4.165 13.668c.09.18.23.343.438.419.207.075.412.04.58-.03.318-.13.635-.436.926-.786.333-.401.683-.927 1.021-1.51a11.651 11.651 0 0 1 1.997-.406c.3.383.61.713.91.95.28.22.603.403.934.417a.856.856 0 0 0 .51-.138c.155-.101.27-.247.354-.416.09-.181.145-.37.138-.563a.844.844 0 0 0-.2-.518c-.226-.27-.596-.4-.96-.465a5.76 5.76 0 0 0-1.335-.05 10.954 10.954 0 0 1-.98-1.686c.25-.66.437-1.284.52-1.794.036-.218.055-.426.048-.614a1.238 1.238 0 0 0-.127-.538.7.7 0 0 0-.477-.365c-.202-.043-.41 0-.601.077-.377.15-.576.47-.651.823-.073.34-.04.736.046 1.136.088.406.238.848.43 1.295a19.697 19.697 0 0 1-1.062 2.227 7.662 7.662 0 0 0-1.482.645c-.37.22-.699.48-.897.787-.21.326-.275.714-.08 1.103z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
3
folkugat_web/assets/templates/icons/spotify.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-spotify" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0m3.669 11.538a.498.498 0 0 1-.686.165c-1.879-1.147-4.243-1.407-7.028-.77a.499.499 0 0 1-.222-.973c3.048-.696 5.662-.397 7.77.892a.5.5 0 0 1 .166.686zm.979-2.178a.624.624 0 0 1-.858.205c-2.15-1.321-5.428-1.704-7.972-.932a.625.625 0 0 1-.362-1.194c2.905-.881 6.517-.454 8.986 1.063a.624.624 0 0 1 .206.858m.084-2.268C10.154 5.56 5.9 5.419 3.438 6.166a.748.748 0 1 1-.434-1.432c2.825-.857 7.523-.692 10.492 1.07a.747.747 0 1 1-.764 1.288z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 614 B |
3
folkugat_web/assets/templates/icons/youtube.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-youtube" viewBox="0 0 16 16">
|
||||||
|
<path d="M8.051 1.999h.089c.822.003 4.987.033 6.11.335a2.01 2.01 0 0 1 1.415 1.42c.101.38.172.883.22 1.402l.01.104.022.26.008.104c.065.914.073 1.77.074 1.957v.075c-.001.194-.01 1.108-.082 2.06l-.008.105-.009.104c-.05.572-.124 1.14-.235 1.558a2.007 2.007 0 0 1-1.415 1.42c-1.16.312-5.569.334-6.18.335h-.142c-.309 0-1.587-.006-2.927-.052l-.17-.006-.087-.004-.171-.007-.171-.007c-1.11-.049-2.167-.128-2.654-.26a2.007 2.007 0 0 1-1.415-1.419c-.111-.417-.185-.986-.235-1.558L.09 9.82l-.008-.104A31.4 31.4 0 0 1 0 7.68v-.123c.002-.215.01-.958.064-1.778l.007-.103.003-.052.008-.104.022-.26.01-.104c.048-.519.119-1.023.22-1.402a2.007 2.007 0 0 1 1.415-1.42c.487-.13 1.544-.21 2.654-.26l.17-.007.172-.006.086-.003.171-.007A99.788 99.788 0 0 1 7.858 2h.193zM6.4 5.209v4.818l4.157-2.408z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 913 B |
38
folkugat_web/assets/templates/index.html
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/>
|
||||||
|
|
||||||
|
<title>{{ page_title }}</title>
|
||||||
|
<meta name="description" content="{{ page_description }}"/>
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css"
|
||||||
|
integrity="sha512-z3gLpd7yknf1YoNbCzqRKc4qyor8gaKU1qmn+CShxbuBusANI9QpRohGBreCFkKxLhei6S9CQXFEbbKuqLg0DA=="
|
||||||
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer" />
|
||||||
|
|
||||||
|
<!-- Taiwind CSS -->
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', path='css/main.css') }}" type="text/css" />
|
||||||
|
|
||||||
|
<!-- HTMX -->
|
||||||
|
<script src="{{ url_for('static', path='js/htmx.min.js') }}"></script>
|
||||||
|
|
||||||
|
<!-- Favicon! -->
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', path='favicon/apple-touch-icon.png') }}">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', path='favicon/favicon-32x32.png') }}">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="{{ url_for('static', path='favicon/favicon-16x16.png') }}">
|
||||||
|
<link rel="manifest" href="/site.webmanifest">
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body class="bg-brown text-yellow-50">
|
||||||
|
<div id="header">
|
||||||
|
{% include "fragments/header.html" %}
|
||||||
|
</div>
|
||||||
|
<div class="{% if animate %} animate-fade-in-three opacity-0 {% endif %}">
|
||||||
|
{% include "fragments/content.html" %}
|
||||||
|
{% include "fragments/footer.html" %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
0
folkugat_web/config/__init__.py
Normal file
11
folkugat_web/config/auth.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import os
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
|
||||||
|
SESSION_DURATION = timedelta(minutes=30)
|
||||||
|
COOKIE_NAME = "nota_folkugat"
|
||||||
|
COOKIE_MAX_AGE = int(SESSION_DURATION.total_seconds())
|
||||||
|
ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "hola")
|
||||||
|
JWT_SECRET = os.getenv("JWT_SECRET", "".join(random.choice(string.ascii_letters) for _ in range(32)))
|
||||||
21
folkugat_web/config/calendari.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import dataclasses
|
||||||
|
|
||||||
|
CALENDAR_LIST_ID = "calendar-list"
|
||||||
|
HISTORY_LIST_ID = "history-list"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class PagingConfig:
|
||||||
|
initial_items: int
|
||||||
|
step: int
|
||||||
|
|
||||||
|
|
||||||
|
CALENDARI_PAGING_CONFIG = PagingConfig(
|
||||||
|
initial_items=3,
|
||||||
|
step=2
|
||||||
|
)
|
||||||
|
|
||||||
|
HISTORY_PAGING_CONFIG = PagingConfig(
|
||||||
|
initial_items=3,
|
||||||
|
step=2
|
||||||
|
)
|
||||||
23
folkugat_web/config/date.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
DAY_NAMES_CAT = {
|
||||||
|
0: "Dilluns",
|
||||||
|
1: "Dimarts",
|
||||||
|
2: "Dimecres",
|
||||||
|
3: "Dijous",
|
||||||
|
4: "Divendres",
|
||||||
|
5: "Dissabte",
|
||||||
|
6: "Diumenge",
|
||||||
|
}
|
||||||
|
MONTH_NAMES_CAT = {
|
||||||
|
1: "de Gener",
|
||||||
|
2: "de Febrer",
|
||||||
|
3: "de Març",
|
||||||
|
4: "d'Abril",
|
||||||
|
5: "de Maig",
|
||||||
|
6: "de Juny",
|
||||||
|
7: "de Juliol",
|
||||||
|
8: "de Agost",
|
||||||
|
9: "de Setembre",
|
||||||
|
10: "d'Octubre",
|
||||||
|
11: "de Novembre",
|
||||||
|
12: "de Desembre",
|
||||||
|
}
|
||||||
10
folkugat_web/config/db.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from folkugat_web.log import logger
|
||||||
|
|
||||||
|
DB_DIR = Path(os.getenv("DB_DIR", "/home/marc/tmp/folkugat")).resolve()
|
||||||
|
DB_FILE = DB_DIR / "folkugat.db"
|
||||||
|
DB_TEMES_DIR = DB_DIR / "temes"
|
||||||
|
|
||||||
|
logger.info(f"Using DB_DIR: {DB_DIR}")
|
||||||
10
folkugat_web/config/directories.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from folkugat_web.log import logger
|
||||||
|
|
||||||
|
APP_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
ASSETS_DIR = APP_DIR / "assets"
|
||||||
|
STATIC_DIR = ASSETS_DIR / "static"
|
||||||
|
TEMPLATES_DIR = ASSETS_DIR / "templates"
|
||||||
|
|
||||||
|
logger.info(f"Using APP_DIR: {APP_DIR}")
|
||||||
1
folkugat_web/config/nota.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
LOGGED_IN_FOOTER = "😎"
|
||||||
8
folkugat_web/config/search.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Minimum ngram length to compute, search string shorther than this limit are ignored
|
||||||
|
MIN_NGRAM_LENGTH = 3
|
||||||
|
|
||||||
|
# Maximum number of insertions/deletions to check for when comparing strings
|
||||||
|
QUERY_NGRAM_RANGE = 2
|
||||||
|
|
||||||
|
# Maximum distance to show a match as the result of a search
|
||||||
|
SEARCH_DISTANCE_THRESHOLD = 0.25
|
||||||
0
folkugat_web/dal/__init__.py
Normal file
1
folkugat_web/dal/sql/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from ._connection import get_connection, Connection
|
||||||
23
folkugat_web/dal/sql/_connection.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import sqlite3
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from typing import Iterator, Optional
|
||||||
|
|
||||||
|
from folkugat_web.config.db import DB_FILE
|
||||||
|
|
||||||
|
Connection = sqlite3.Connection
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def get_connection(con: Optional[Connection] = None) -> Iterator[Connection]:
|
||||||
|
if con:
|
||||||
|
yield con
|
||||||
|
else:
|
||||||
|
con = sqlite3.connect(DB_FILE)
|
||||||
|
try:
|
||||||
|
yield con
|
||||||
|
con.commit()
|
||||||
|
except Exception:
|
||||||
|
con.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
con.close()
|
||||||
8
folkugat_web/dal/sql/ddl.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from folkugat_web.dal.sql import get_connection, sessions
|
||||||
|
from folkugat_web.dal.sql.temes import ddl as temes_ddl
|
||||||
|
|
||||||
|
|
||||||
|
def create_db():
|
||||||
|
with get_connection() as con:
|
||||||
|
sessions.create_db(con)
|
||||||
|
temes_ddl.create_db(con)
|
||||||
194
folkugat_web/dal/sql/sessions.py
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from folkugat_web.dal.sql import Connection, get_connection
|
||||||
|
from folkugat_web.model import sessions as model
|
||||||
|
from folkugat_web.model.sql import OrderCol, Range
|
||||||
|
from folkugat_web.typing import OptionalListOrValue as OLV
|
||||||
|
|
||||||
|
|
||||||
|
def create_db(con: Optional[Connection] = None):
|
||||||
|
with get_connection(con) as con:
|
||||||
|
create_sessions_table(con)
|
||||||
|
|
||||||
|
|
||||||
|
def drop_sessions_table(con: Connection):
|
||||||
|
query = "DROP TABLE IF EXISTS sessions"
|
||||||
|
cur = con.cursor()
|
||||||
|
cur.execute(query)
|
||||||
|
|
||||||
|
|
||||||
|
def create_sessions_table(con: Connection):
|
||||||
|
query = """
|
||||||
|
CREATE TABLE IF NOT EXISTS sessions (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
date TEXT NOT NULL,
|
||||||
|
start_time TEXT NOT NULL,
|
||||||
|
end_time TEXT NOT NULL,
|
||||||
|
venue_name TEXT,
|
||||||
|
venue_url TEXT,
|
||||||
|
is_live BOOLEAN DEFAULT false
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
cur = con.cursor()
|
||||||
|
cur.execute(query)
|
||||||
|
|
||||||
|
|
||||||
|
def _session_to_row(sessio: model.Session) -> dict:
|
||||||
|
return {
|
||||||
|
'id': sessio.id,
|
||||||
|
'date': sessio.date,
|
||||||
|
'start_time': sessio.start_time.isoformat(),
|
||||||
|
'end_time': sessio.end_time.isoformat(),
|
||||||
|
'venue_name': sessio.venue.name,
|
||||||
|
'venue_url': sessio.venue.url,
|
||||||
|
'is_live': sessio.is_live,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _row_to_session(row: tuple) -> model.Session:
|
||||||
|
return model.Session(
|
||||||
|
id=row[0],
|
||||||
|
date=datetime.date.fromisoformat(row[1]),
|
||||||
|
start_time=datetime.time.fromisoformat(row[2]),
|
||||||
|
end_time=datetime.time.fromisoformat(row[3]),
|
||||||
|
venue=model.SessionVenue(
|
||||||
|
name=row[4],
|
||||||
|
url=row[5],
|
||||||
|
),
|
||||||
|
is_live=row[6],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def insert_session(session: model.Session, con: Optional[Connection] = None):
|
||||||
|
query = """
|
||||||
|
INSERT INTO sessions
|
||||||
|
(id, date, start_time, end_time, venue_name, venue_url, is_live)
|
||||||
|
VALUES
|
||||||
|
(:id, :date, :start_time, :end_time, :venue_name, :venue_url, :is_live)
|
||||||
|
RETURNING *
|
||||||
|
"""
|
||||||
|
data = _session_to_row(session)
|
||||||
|
with get_connection(con) as con:
|
||||||
|
cur = con.cursor()
|
||||||
|
cur.execute(query, data)
|
||||||
|
row = cur.fetchone()
|
||||||
|
return _row_to_session(row)
|
||||||
|
|
||||||
|
|
||||||
|
def update_session(session: model.Session, con: Optional[Connection] = None):
|
||||||
|
query = """
|
||||||
|
UPDATE sessions SET
|
||||||
|
date = :date, start_time = :start_time, end_time = :end_time,
|
||||||
|
venue_name = :venue_name, venue_url = :venue_url, is_live = :is_live
|
||||||
|
WHERE id = :id
|
||||||
|
"""
|
||||||
|
data = _session_to_row(session)
|
||||||
|
with get_connection(con) as con:
|
||||||
|
cur = con.cursor()
|
||||||
|
cur.execute(query, data)
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_clause(session_id: Optional[int] = None,
|
||||||
|
date_range: Optional[Range[datetime.date]] = None,
|
||||||
|
is_live: Optional[bool] = None) -> tuple[str, dict]:
|
||||||
|
filter_clauses = []
|
||||||
|
filter_data = {}
|
||||||
|
|
||||||
|
if session_id is not None:
|
||||||
|
filter_clauses.append(f"id = :session_id")
|
||||||
|
filter_data["session_id"] = session_id
|
||||||
|
|
||||||
|
if date_range:
|
||||||
|
if ub := date_range.upper_bound():
|
||||||
|
operator = "<=" if ub[1] else "<"
|
||||||
|
filter_clauses.append(f"date {operator} :date_ub")
|
||||||
|
filter_data["date_ub"] = ub[0]
|
||||||
|
if lb := date_range.lower_bound():
|
||||||
|
operator = ">=" if lb[1] else ">"
|
||||||
|
filter_clauses.append(f"date {operator} :date_lb")
|
||||||
|
filter_data["date_lb"] = lb[0]
|
||||||
|
|
||||||
|
if is_live is not None:
|
||||||
|
filter_clauses.append(f"is_live = :is_live")
|
||||||
|
filter_data["is_live"] = is_live
|
||||||
|
|
||||||
|
if filter_clauses:
|
||||||
|
filter_clause_str = " AND ".join(filter_clauses)
|
||||||
|
filter_clause = f"WHERE {filter_clause_str}"
|
||||||
|
return filter_clause, filter_data
|
||||||
|
else:
|
||||||
|
return "", {}
|
||||||
|
|
||||||
|
|
||||||
|
def _order_clause(order_by: OLV[OrderCol[model.SessionCols]]) -> str:
|
||||||
|
if not order_by:
|
||||||
|
return ""
|
||||||
|
if not isinstance(order_by, list):
|
||||||
|
order_by = [order_by]
|
||||||
|
|
||||||
|
order_clauses = [f"{ocol.column.value} {ocol.order.value}" for ocol in order_by]
|
||||||
|
order_clauses_str = " ".join(order_clauses)
|
||||||
|
return f"ORDER BY {order_clauses_str}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_sessions(session_id: Optional[int] = None,
|
||||||
|
date_range: Optional[Range[datetime.date]] = None,
|
||||||
|
is_live: Optional[bool] = None,
|
||||||
|
order_by: OLV[OrderCol[model.SessionCols]] = None,
|
||||||
|
limit: Optional[int] = None, offset: Optional[int] = None,
|
||||||
|
con: Optional[Connection] = None) -> list[model.Session]:
|
||||||
|
clauses = []
|
||||||
|
filter_clause, data = _filter_clause(session_id=session_id, date_range=date_range, is_live=is_live)
|
||||||
|
if filter_clause:
|
||||||
|
clauses.append(filter_clause)
|
||||||
|
if order_clause := _order_clause(order_by=order_by):
|
||||||
|
clauses.append(order_clause)
|
||||||
|
if limit is not None:
|
||||||
|
clauses.append("LIMIT :limit")
|
||||||
|
data["limit"] = limit
|
||||||
|
if offset is not None:
|
||||||
|
clauses.append("OFFSET :offset")
|
||||||
|
data["offset"] = offset
|
||||||
|
|
||||||
|
clauses_str = " ".join(clauses)
|
||||||
|
query = f"""
|
||||||
|
SELECT id, date, start_time, end_time, venue_name, venue_url, is_live
|
||||||
|
FROM sessions
|
||||||
|
{clauses_str}
|
||||||
|
"""
|
||||||
|
with get_connection(con) as con:
|
||||||
|
cur = con.cursor()
|
||||||
|
cur.execute(query, data)
|
||||||
|
return list(map(_row_to_session, cur.fetchall()))
|
||||||
|
|
||||||
|
|
||||||
|
def delete_session_by_id(session_id: int, con: Optional[Connection] = None):
|
||||||
|
query = """
|
||||||
|
DELETE FROM sessions
|
||||||
|
WHERE id = :id
|
||||||
|
"""
|
||||||
|
data = dict(id=session_id)
|
||||||
|
with get_connection(con) as con:
|
||||||
|
cur = con.cursor()
|
||||||
|
cur.execute(query, data)
|
||||||
|
|
||||||
|
|
||||||
|
def stop_live_sessions(con: Optional[Connection] = None):
|
||||||
|
query = """
|
||||||
|
UPDATE sessions SET is_live = false WHERE is_live = true
|
||||||
|
"""
|
||||||
|
with get_connection(con) as con:
|
||||||
|
cur = con.cursor()
|
||||||
|
cur.execute(query)
|
||||||
|
|
||||||
|
|
||||||
|
def set_live_session(session_id: int, con: Optional[Connection] = None):
|
||||||
|
query = """
|
||||||
|
UPDATE sessions SET is_live = true WHERE id = :id
|
||||||
|
"""
|
||||||
|
data = dict(id=session_id)
|
||||||
|
with get_connection(con) as con:
|
||||||
|
stop_live_sessions(con=con)
|
||||||
|
cur = con.cursor()
|
||||||
|
cur.execute(query, data)
|
||||||
0
folkugat_web/dal/sql/temes/__init__.py
Normal file
38
folkugat_web/dal/sql/temes/_conversion.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
|
||||||
|
from folkugat_web.model import temes as model
|
||||||
|
|
||||||
|
|
||||||
|
def tema_to_row(tema: model.Tema) -> dict:
|
||||||
|
return {
|
||||||
|
'id': tema.id,
|
||||||
|
'title': tema.title,
|
||||||
|
'properties': json.dumps(list(map(lambda p: p.to_dict(), tema.properties))),
|
||||||
|
'links': json.dumps(list(map(lambda l: l.to_dict(), tema.links))),
|
||||||
|
'lyrics': json.dumps(list(map(lambda l: l.to_dict(), tema.lyrics))),
|
||||||
|
'alternatives': json.dumps(tema.alternatives),
|
||||||
|
'ngrams': json.dumps(tema.ngrams),
|
||||||
|
'modification_date': tema.modification_date.isoformat(),
|
||||||
|
'creation_date': tema.creation_date.isoformat(),
|
||||||
|
'hidden': 1 if tema.hidden else 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def cell_to_ngrams(cell: str) -> model.NGrams:
|
||||||
|
return {int(n): ngrams_ for n, ngrams_ in json.loads(cell).items()}
|
||||||
|
|
||||||
|
|
||||||
|
def row_to_tema(row: tuple) -> model.Tema:
|
||||||
|
return model.Tema(
|
||||||
|
id=row[0],
|
||||||
|
title=row[1],
|
||||||
|
properties=list(map(model.Property.from_dict, json.loads(row[2]))),
|
||||||
|
links=list(map(model.Link.from_dict, json.loads(row[3]))),
|
||||||
|
lyrics=list(map(model.Lyrics.from_dict, json.loads(row[4]))),
|
||||||
|
alternatives=json.loads(row[5]),
|
||||||
|
ngrams=cell_to_ngrams(row[6]),
|
||||||
|
modification_date=datetime.datetime.fromisoformat(row[7]),
|
||||||
|
creation_date=datetime.datetime.fromisoformat(row[8]),
|
||||||
|
hidden=bool(row[9]),
|
||||||
|
)
|
||||||
40
folkugat_web/dal/sql/temes/ddl.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from folkugat_web import data
|
||||||
|
from folkugat_web.dal.sql import Connection, get_connection
|
||||||
|
|
||||||
|
from .write import insert_tema
|
||||||
|
|
||||||
|
|
||||||
|
def create_db(con: Optional[Connection] = None):
|
||||||
|
with get_connection(con) as con:
|
||||||
|
drop_temes_table(con)
|
||||||
|
create_temes_table(con)
|
||||||
|
|
||||||
|
for tema in data.TEMES:
|
||||||
|
insert_tema(tema, con)
|
||||||
|
|
||||||
|
|
||||||
|
def drop_temes_table(con: Connection):
|
||||||
|
query = "DROP TABLE IF EXISTS temes"
|
||||||
|
cur = con.cursor()
|
||||||
|
cur.execute(query)
|
||||||
|
|
||||||
|
|
||||||
|
def create_temes_table(con: Connection):
|
||||||
|
query = """
|
||||||
|
CREATE TABLE IF NOT EXISTS temes (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
properties TEXT,
|
||||||
|
links TEXT,
|
||||||
|
lyrics TEXT,
|
||||||
|
alternatives TEXT,
|
||||||
|
ngrams TEXT,
|
||||||
|
creation_date TEXT NOT NULL,
|
||||||
|
modification_date TEXT NOT NULL,
|
||||||
|
hidden INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
cur = con.cursor()
|
||||||
|
cur.execute(query)
|
||||||
49
folkugat_web/dal/sql/temes/query.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from folkugat_web.dal.sql import Connection, get_connection
|
||||||
|
from folkugat_web.model import temes as model
|
||||||
|
|
||||||
|
from ._conversion import cell_to_ngrams, row_to_tema
|
||||||
|
|
||||||
|
TEMA_ID_TO_NGRAMS_CACHE = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_tema_by_id(tema_id: int, con: Optional[Connection] = None) -> Optional[model.Tema]:
|
||||||
|
query = """
|
||||||
|
SELECT
|
||||||
|
id, title, properties, links, lyrics, alternatives, ngrams,
|
||||||
|
creation_date, modification_date, hidden
|
||||||
|
FROM temes
|
||||||
|
WHERE id = :id
|
||||||
|
"""
|
||||||
|
data = dict(id=tema_id)
|
||||||
|
with get_connection(con) as con:
|
||||||
|
cur = con.cursor()
|
||||||
|
cur.execute(query, data)
|
||||||
|
row = cur.fetchone()
|
||||||
|
return row_to_tema(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
def evict_tema_id_to_ngrams_cache():
|
||||||
|
global TEMA_ID_TO_NGRAMS_CACHE
|
||||||
|
TEMA_ID_TO_NGRAMS_CACHE = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_tema_id_to_ngrams(con: Optional[Connection] = None) -> dict[int, model.NGrams]:
|
||||||
|
global TEMA_ID_TO_NGRAMS_CACHE
|
||||||
|
if TEMA_ID_TO_NGRAMS_CACHE is None:
|
||||||
|
TEMA_ID_TO_NGRAMS_CACHE = _get_tema_id_to_ngrams(con)
|
||||||
|
return TEMA_ID_TO_NGRAMS_CACHE
|
||||||
|
|
||||||
|
|
||||||
|
def _get_tema_id_to_ngrams(con: Optional[Connection] = None) -> dict[int, model.NGrams]:
|
||||||
|
query = """
|
||||||
|
SELECT id, ngrams
|
||||||
|
FROM temes
|
||||||
|
WHERE hidden = 0
|
||||||
|
"""
|
||||||
|
with get_connection(con) as con:
|
||||||
|
cur = con.cursor()
|
||||||
|
cur.execute(query)
|
||||||
|
rows = cur.fetchall()
|
||||||
|
return {id_: cell_to_ngrams(ng) for id_, ng in rows}
|
||||||
44
folkugat_web/dal/sql/temes/write.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from folkugat_web.dal.sql import Connection, get_connection
|
||||||
|
from folkugat_web.model import temes as model
|
||||||
|
|
||||||
|
from ._conversion import row_to_tema, tema_to_row
|
||||||
|
from .query import evict_tema_id_to_ngrams_cache
|
||||||
|
|
||||||
|
|
||||||
|
def insert_tema(tema: model.Tema, con: Optional[Connection] = None) -> model.Tema:
|
||||||
|
query = """
|
||||||
|
INSERT INTO temes
|
||||||
|
(id, title, properties, links, lyrics, alternatives, ngrams,
|
||||||
|
creation_date, modification_date, hidden)
|
||||||
|
VALUES
|
||||||
|
(:id, :title, :properties, :links, :lyrics, :alternatives, :ngrams,
|
||||||
|
:creation_date, :modification_date, :hidden)
|
||||||
|
RETURNING *
|
||||||
|
"""
|
||||||
|
data = tema_to_row(tema)
|
||||||
|
with get_connection(con) as con:
|
||||||
|
cur = con.cursor()
|
||||||
|
cur.execute(query, data)
|
||||||
|
row = cur.fetchone()
|
||||||
|
evict_tema_id_to_ngrams_cache()
|
||||||
|
return row_to_tema(row)
|
||||||
|
|
||||||
|
|
||||||
|
def update_tema(tema: model.Tema, con: Optional[Connection] = None):
|
||||||
|
query = """
|
||||||
|
UPDATE temes
|
||||||
|
SET
|
||||||
|
title = :title, properties = :properties, links = :links, lyrics = :lyrics,
|
||||||
|
alternatives = :alternatives, ngrams = :ngrams, creation_date = :creation_date,
|
||||||
|
modification_date = :modification_date, hidden = :hidden
|
||||||
|
WHERE
|
||||||
|
id = :id
|
||||||
|
"""
|
||||||
|
data = tema_to_row(tema)
|
||||||
|
with get_connection(con) as con:
|
||||||
|
cur = con.cursor()
|
||||||
|
cur.execute(query, data)
|
||||||
|
evict_tema_id_to_ngrams_cache()
|
||||||
|
return
|
||||||
105
folkugat_web/data.py
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
from folkugat_web.model import temes as model
|
||||||
|
|
||||||
|
TEMES = [
|
||||||
|
# ---
|
||||||
|
model.Tema(
|
||||||
|
title="El Joan Petit",
|
||||||
|
hidden=False,
|
||||||
|
).with_ngrams(),
|
||||||
|
# ---
|
||||||
|
model.Tema(
|
||||||
|
title="Pasdoble de Muntanya (Cançó amb el nom molt llarg)",
|
||||||
|
links=[
|
||||||
|
model.Link(
|
||||||
|
type=model.LinkType.AUDIO,
|
||||||
|
subtype=model.LinkSubtype.SPOTIFY,
|
||||||
|
url="https://open.spotify.com/track/4j9Krf19c5USmMvVUCoeWa?si=3023d1d83f814886",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
hidden=False,
|
||||||
|
).with_ngrams(),
|
||||||
|
# ---
|
||||||
|
model.Tema(
|
||||||
|
title="Astrid Waltz",
|
||||||
|
alternatives=["vals"],
|
||||||
|
links=[
|
||||||
|
model.Link(
|
||||||
|
type=model.LinkType.OTHER,
|
||||||
|
subtype=None,
|
||||||
|
url="https://marc.sastre.cat/folkugat",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
hidden=False,
|
||||||
|
).with_ngrams(),
|
||||||
|
# ---
|
||||||
|
model.Tema(
|
||||||
|
title="Ook Pik Waltz",
|
||||||
|
alternatives=["vals"],
|
||||||
|
hidden=False,
|
||||||
|
).with_ngrams(),
|
||||||
|
# ---
|
||||||
|
model.Tema(
|
||||||
|
title="Pasdoble Patumaire",
|
||||||
|
hidden=False,
|
||||||
|
).with_ngrams(),
|
||||||
|
# ---
|
||||||
|
model.Tema(
|
||||||
|
title="El Gitano",
|
||||||
|
links=[
|
||||||
|
model.Link(
|
||||||
|
type=model.LinkType.SCORE,
|
||||||
|
subtype=model.LinkSubtype.PDF,
|
||||||
|
url="/db/temes/1/tema.pdf",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
hidden=False,
|
||||||
|
).with_ngrams(),
|
||||||
|
# ---
|
||||||
|
model.Tema(
|
||||||
|
title="Pasdoble de Gitanes",
|
||||||
|
alternatives=["entrada"],
|
||||||
|
hidden=False,
|
||||||
|
).with_ngrams(),
|
||||||
|
# ---
|
||||||
|
model.Tema(
|
||||||
|
title="Jota Comunera",
|
||||||
|
hidden=False,
|
||||||
|
).with_ngrams(),
|
||||||
|
# ---
|
||||||
|
model.Tema(
|
||||||
|
title="Malaguenya de Barxeta",
|
||||||
|
lyrics=[
|
||||||
|
model.Lyrics(
|
||||||
|
title="Malaguenya de Barxeta",
|
||||||
|
content="""
|
||||||
|
Mira si he corregut terres
|
||||||
|
que he estat en Alfarrasí,
|
||||||
|
en Adzaneta i Albaida,
|
||||||
|
en el Palomar i ací.
|
||||||
|
|
||||||
|
Omplim el sarró de pa,
|
||||||
|
si vols que et guarde les cabres,
|
||||||
|
que les figues ja s'acaben
|
||||||
|
i raïm ja no hi ha.
|
||||||
|
|
||||||
|
L'altre dia jo somiava
|
||||||
|
que ja era realitat
|
||||||
|
un món sense violència
|
||||||
|
ple de pau i llibertat.
|
||||||
|
|
||||||
|
Vinc del cor de la Costera,
|
||||||
|
el poble dels socarrats,
|
||||||
|
d'allà on renaix de les cendres
|
||||||
|
el meu País Valencià.
|
||||||
|
""".strip(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
properties=[
|
||||||
|
model.Property(
|
||||||
|
model.PropertyField.AUTOR,
|
||||||
|
"Pep Jimeno 'Botifarra'"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
hidden=False,
|
||||||
|
).with_ngrams(),
|
||||||
|
]
|
||||||
0
folkugat_web/fragments/__init__.py
Normal file
45
folkugat_web/fragments/live.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
from fastapi import Request
|
||||||
|
from folkugat_web.model import sessions as model
|
||||||
|
from folkugat_web.services import sessions as service
|
||||||
|
from folkugat_web.templates import templates
|
||||||
|
|
||||||
|
|
||||||
|
def sessio_en_directe(request: Request):
|
||||||
|
session = service.get_live_session()
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"fragments/marquee.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"session": session,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def start_live_session(request: Request, session_id: int):
|
||||||
|
service.set_live_session(session_id=session_id)
|
||||||
|
session = model.Session(id=session_id)
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"fragments/sessions/live/stop.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"session": session,
|
||||||
|
},
|
||||||
|
headers={
|
||||||
|
"HX-Trigger": "reload-marquee"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def stop_live_session(request: Request, session_id: int):
|
||||||
|
service.stop_live_sessions()
|
||||||
|
session = model.Session(id=session_id)
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"fragments/sessions/live/start.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"session": session,
|
||||||
|
},
|
||||||
|
headers={
|
||||||
|
"HX-Trigger": "reload-marquee"
|
||||||
|
}
|
||||||
|
)
|
||||||
35
folkugat_web/fragments/nota.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from folkugat_web.config import nota as config
|
||||||
|
from folkugat_web.templates import templates
|
||||||
|
|
||||||
|
|
||||||
|
def input(request, value=None):
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"fragments/nota/input.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"value": value,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def footer(request, value, logged_in):
|
||||||
|
response = templates.TemplateResponse(
|
||||||
|
"fragments/nota/footer.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"value": config.LOGGED_IN_FOOTER if logged_in else value,
|
||||||
|
"logged_in": logged_in,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def nota(request):
|
||||||
|
response = templates.TemplateResponse(
|
||||||
|
"fragments/nota/nota.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response.headers["HX-Refresh"] = "true"
|
||||||
|
return response
|
||||||
144
folkugat_web/fragments/sessions.py
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import Request
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
from folkugat_web.config import calendari as config
|
||||||
|
from folkugat_web.model import sessions as model
|
||||||
|
from folkugat_web.model.pagines import Pages
|
||||||
|
from folkugat_web.services import sessions as service
|
||||||
|
from folkugat_web.templates import templates
|
||||||
|
|
||||||
|
|
||||||
|
def sessions_pagina(request, logged_in):
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"fragments/sessions/pagina.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"logged_in": logged_in,
|
||||||
|
"Pages": Pages,
|
||||||
|
"menu_selected_id": Pages.Sessions,
|
||||||
|
"calendar_list_id": config.CALENDAR_LIST_ID,
|
||||||
|
"history_list_id": config.HISTORY_LIST_ID,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sessions_calendari(request: Request, limit: int, logged_in: bool):
|
||||||
|
sessions = service.get_next_sessions(limit=limit+1)
|
||||||
|
has_more_sessions = len(sessions) > limit
|
||||||
|
sessions = sessions[:limit]
|
||||||
|
next_session_id = sessions[0].id if sessions else None
|
||||||
|
return _sessions_list(
|
||||||
|
request=request,
|
||||||
|
sessions=sessions,
|
||||||
|
has_more_sessions=has_more_sessions,
|
||||||
|
paging_config=config.CALENDARI_PAGING_CONFIG,
|
||||||
|
list_id=config.CALENDAR_LIST_ID,
|
||||||
|
next_session_id=next_session_id,
|
||||||
|
get_sessions_url="/api/sessions/upcoming",
|
||||||
|
limit=limit,
|
||||||
|
logged_in=logged_in
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sessions_historial(request: Request, limit: int, logged_in: bool):
|
||||||
|
sessions = service.get_sessions_history(limit=limit+1)
|
||||||
|
has_more_sessions = len(sessions) > limit
|
||||||
|
sessions = sessions[:limit]
|
||||||
|
return _sessions_list(
|
||||||
|
request=request,
|
||||||
|
sessions=sessions,
|
||||||
|
has_more_sessions=has_more_sessions,
|
||||||
|
paging_config=config.HISTORY_PAGING_CONFIG,
|
||||||
|
list_id=config.HISTORY_LIST_ID,
|
||||||
|
get_sessions_url="/api/sessions/history",
|
||||||
|
limit=limit,
|
||||||
|
logged_in=logged_in
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _sessions_list(request: Request, sessions: list[model.Session], has_more_sessions: bool,
|
||||||
|
paging_config: config.PagingConfig, list_id: str, get_sessions_url: str,
|
||||||
|
limit: int, logged_in: bool, next_session_id: Optional[int] = None):
|
||||||
|
if has_more_sessions:
|
||||||
|
more_sessions = limit + paging_config.step
|
||||||
|
else:
|
||||||
|
more_sessions = None
|
||||||
|
|
||||||
|
if len(sessions) > paging_config.initial_items:
|
||||||
|
less_sessions = max(paging_config.initial_items, len(sessions) - paging_config.step)
|
||||||
|
else:
|
||||||
|
less_sessions = None
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"fragments/sessions/session_list.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"logged_in": logged_in,
|
||||||
|
"get_sessions_url": get_sessions_url,
|
||||||
|
"sessions": sessions,
|
||||||
|
"session_list_id": list_id,
|
||||||
|
"next_session_id": next_session_id,
|
||||||
|
"more_sessions": more_sessions,
|
||||||
|
"less_sessions": less_sessions,
|
||||||
|
"limit": limit,
|
||||||
|
"date_names": service.get_date_names,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sessio(request: Request, session_id: int, logged_in: bool):
|
||||||
|
session = service.get_session(session_id=session_id)
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"fragments/sessions/sessio.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"logged_in": logged_in,
|
||||||
|
"Pages": Pages,
|
||||||
|
"session": session,
|
||||||
|
"date_names": service.get_date_names,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sessions_editor_row(request: Request, session_date: Optional[model.Session] = None, session_id: Optional[int] = None):
|
||||||
|
if session_date is None:
|
||||||
|
if session_id is None:
|
||||||
|
raise ValueError("Must either give session or session_id")
|
||||||
|
session_date = service.get_session(session_id=session_id)
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"fragments/sessions/session_row.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"session": session_date,
|
||||||
|
"date_names": service.get_date_names,
|
||||||
|
"logged_in": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sessions_editor_row_editing(request, session_id: int):
|
||||||
|
session = service.get_session(session_id)
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"fragments/sessions/session_row_editing.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"session": session,
|
||||||
|
"date_names": service.get_date_names,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sessions_editor_insert_row(request, session_date: Optional[model.Session] = None):
|
||||||
|
session_date = service.insert_session(session_date or service.new_session())
|
||||||
|
return sessions_editor_row(request, session_date=session_date)
|
||||||
|
|
||||||
|
|
||||||
|
def sessions_editor_post_row(request, session_date: model.Session):
|
||||||
|
service.set_session(session_date)
|
||||||
|
return sessions_editor_row(request, session_date=session_date)
|
||||||
|
|
||||||
|
|
||||||
|
def sessions_editor_delete_row(session_id: int):
|
||||||
|
service.delete_session(session_id)
|
||||||
|
return HTMLResponse()
|
||||||
73
folkugat_web/fragments/tema.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import Request
|
||||||
|
from folkugat_web.model import temes as model
|
||||||
|
from folkugat_web.services.temes import query as temes_q
|
||||||
|
from folkugat_web.templates import templates
|
||||||
|
|
||||||
|
|
||||||
|
def title(request: Request, logged_in: bool, tema: Optional[model.Tema] = None, tema_id: Optional[int] = None):
|
||||||
|
if tema is None:
|
||||||
|
if tema_id is None:
|
||||||
|
raise ValueError("Either 'tema' or 'tema_id' must be given!")
|
||||||
|
tema = temes_q.get_tema_by_id(tema_id)
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"fragments/tema/title.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"logged_in": logged_in,
|
||||||
|
"tema": tema,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def title_editor(request: Request, logged_in: bool, tema_id: int):
|
||||||
|
tema = temes_q.get_tema_by_id(tema_id)
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"fragments/tema/editor/title.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"logged_in": logged_in,
|
||||||
|
"tema": tema,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def lyric(request: Request, logged_in: bool, tema_id: int, lyric_idx: int):
|
||||||
|
tema = temes_q.get_tema_by_id(tema_id)
|
||||||
|
if tema is None:
|
||||||
|
raise ValueError(f"No tune exists for tema_id: {tema_id}")
|
||||||
|
if len(tema.lyrics) < lyric_idx:
|
||||||
|
raise ValueError(f'Lyric index out of bounds')
|
||||||
|
lyric = tema.lyrics[lyric_idx]
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"fragments/tema/lyric.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"logged_in": logged_in,
|
||||||
|
"tema": tema,
|
||||||
|
"lyric_idx": lyric_idx,
|
||||||
|
"lyric": lyric,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def lyric_editor(request: Request, logged_in: bool, tema_id: int, lyric_idx: int):
|
||||||
|
tema = temes_q.get_tema_by_id(tema_id)
|
||||||
|
if tema is None:
|
||||||
|
raise ValueError(f"No tune exists for tema_id: {tema_id}")
|
||||||
|
if len(tema.lyrics) < lyric_idx:
|
||||||
|
raise ValueError(f'Lyric index out of bounds')
|
||||||
|
lyric = tema.lyrics[lyric_idx]
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"fragments/tema/editor/lyric.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"logged_in": logged_in,
|
||||||
|
"tema": tema,
|
||||||
|
"lyric_idx": lyric_idx,
|
||||||
|
"lyric": lyric,
|
||||||
|
}
|
||||||
|
)
|
||||||
54
folkugat_web/fragments/temes.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
from fastapi import Request
|
||||||
|
from folkugat_web.model import temes as model
|
||||||
|
from folkugat_web.model.pagines import Pages
|
||||||
|
from folkugat_web.services.temes import query as temes_q
|
||||||
|
from folkugat_web.services.temes import search as temes_s
|
||||||
|
from folkugat_web.templates import templates
|
||||||
|
|
||||||
|
|
||||||
|
def temes_pagina(request: Request, logged_in: bool):
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"fragments/temes/pagina.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"logged_in": logged_in,
|
||||||
|
"Pages": Pages,
|
||||||
|
"menu_selected_id": Pages.Temes,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def temes_busca_result(request: Request, tema: model.Tema, logged_in: bool):
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"fragments/temes/result.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"logged_in": logged_in,
|
||||||
|
"tema": tema,
|
||||||
|
"LinkSubtype": model.LinkSubtype,
|
||||||
|
"LinkType": model.LinkType,
|
||||||
|
}
|
||||||
|
).body.decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def temes_busca(request: Request, query: str, logged_in: bool):
|
||||||
|
temes = temes_s.busca_temes(query)
|
||||||
|
return '\n'.join(
|
||||||
|
[temes_busca_result(request, tema, logged_in)
|
||||||
|
for tema in temes]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def tema(request: Request, tema_id: int, logged_in: bool):
|
||||||
|
tema = temes_q.get_tema_by_id(tema_id)
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"fragments/tema/pagina.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"logged_in": logged_in,
|
||||||
|
"Pages": Pages,
|
||||||
|
"LinkSubtype": model.LinkSubtype,
|
||||||
|
"LinkType": model.LinkType,
|
||||||
|
"tema": tema,
|
||||||
|
}
|
||||||
|
)
|
||||||
6
folkugat_web/log.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.NOTSET)
|
||||||
|
|
||||||
|
logger = logging.getLogger('folkugat-web')
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
57
folkugat_web/main.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import subprocess
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
|
from folkugat_web import log
|
||||||
|
from folkugat_web.config import directories, db
|
||||||
|
from folkugat_web.dal.sql.ddl import create_db
|
||||||
|
from folkugat_web.api import router
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(_: FastAPI):
|
||||||
|
"""
|
||||||
|
Context manager for FastAPI app. It will run all code before `yield`
|
||||||
|
on app startup, and will run code after `yeld` on app shutdown.
|
||||||
|
"""
|
||||||
|
|
||||||
|
log.logger.info("Running Tailwind CSS...")
|
||||||
|
try:
|
||||||
|
subprocess.run([
|
||||||
|
"tailwindcss",
|
||||||
|
"-i",
|
||||||
|
directories.STATIC_DIR / "src" / "tw.css",
|
||||||
|
"-o",
|
||||||
|
directories.STATIC_DIR / "css" / "main.css",
|
||||||
|
# "--minify"
|
||||||
|
])
|
||||||
|
except Exception as e:
|
||||||
|
log.logger.error(f"Error running tailwindcss: {e}")
|
||||||
|
log.logger.info("Tailwind CSS done")
|
||||||
|
|
||||||
|
log.logger.info("Creating DB...")
|
||||||
|
create_db()
|
||||||
|
log.logger.info("DB created")
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def get_app() -> FastAPI:
|
||||||
|
app = FastAPI(lifespan=lifespan)
|
||||||
|
app.include_router(router)
|
||||||
|
|
||||||
|
app.mount("/static", StaticFiles(directory=directories.STATIC_DIR), name="static")
|
||||||
|
app.mount("/db/temes", StaticFiles(directory=db.DB_TEMES_DIR), name="temes")
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
app = get_app()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
uvicorn.run(app, host="127.0.0.1", port=8000)
|
||||||