Tune editor
This commit is contained in:
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1741865919,
|
||||||
|
"narHash": "sha256-4thdbnP6dlbdq+qZWTsm4ffAwoS8Tiq1YResB+RP6WE=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "573c650e8a14b2faa0041645ab18aed7e60f0c9a",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
46
flake.nix
46
flake.nix
@@ -13,41 +13,51 @@
|
|||||||
|
|
||||||
# custom-python = import ./custom-python.nix { inherit pkgs; };
|
# custom-python = import ./custom-python.nix { inherit pkgs; };
|
||||||
|
|
||||||
|
coreDependencies = with pythonPackages; [
|
||||||
|
# IDE tools
|
||||||
|
pylsp-mypy
|
||||||
|
isort
|
||||||
|
autopep8
|
||||||
|
# Development tools
|
||||||
|
pytest
|
||||||
|
setuptools
|
||||||
|
ipython
|
||||||
|
];
|
||||||
|
|
||||||
projectDependencies = with pythonPackages; [
|
projectDependencies = with pythonPackages; [
|
||||||
# API
|
# API
|
||||||
fastapi
|
fastapi
|
||||||
|
python-multipart
|
||||||
jinja2
|
jinja2
|
||||||
uvicorn
|
uvicorn
|
||||||
|
# Files
|
||||||
|
magic
|
||||||
# Auth
|
# Auth
|
||||||
pyjwt
|
pyjwt
|
||||||
# Search
|
# Search
|
||||||
levenshtein
|
levenshtein
|
||||||
# Test
|
|
||||||
pytest
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
pythonEnv = python.withPackages (
|
||||||
|
ps: projectDependencies ++ coreDependencies
|
||||||
|
);
|
||||||
|
|
||||||
in {
|
in {
|
||||||
devShells.default = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
nativeBuildInputs = [ pkgs.bashInteractive ];
|
# nativeBuildInputs = [ pkgs.bashInteractive ];
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
# Core python dependencies
|
# Python dependencies
|
||||||
python
|
python
|
||||||
pythonPackages.pip
|
pythonEnv
|
||||||
pythonPackages.virtualenv
|
pyright
|
||||||
# Other dependencies
|
|
||||||
nodePackages_latest.tailwindcss
|
|
||||||
# IDE tools
|
|
||||||
pythonPackages.pylsp-mypy
|
|
||||||
pythonPackages.isort
|
|
||||||
pythonPackages.autopep8
|
|
||||||
nodePackages.pyright
|
|
||||||
# Development tools
|
|
||||||
black
|
black
|
||||||
pythonPackages.ipython
|
# Project dependencies
|
||||||
pythonPackages.pytest
|
nodePackages_latest.tailwindcss
|
||||||
pythonPackages.setuptools
|
];
|
||||||
] ++ projectDependencies;
|
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
export PYTHONPATH=${pythonEnv}/${python.sitePackages}:$PYTHONPATH
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,3 +41,48 @@ def link_editor(
|
|||||||
tema_id=tema_id,
|
tema_id=tema_id,
|
||||||
link_id=link_id,
|
link_id=link_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/tema/{tema_id}/editor/link/{link_id}/url")
|
||||||
|
def link_editor_url_input(
|
||||||
|
request: Request,
|
||||||
|
logged_in: auth.RequireLogin,
|
||||||
|
tema_id: int,
|
||||||
|
link_id: int,
|
||||||
|
):
|
||||||
|
return tema.link_editor_url(
|
||||||
|
request=request,
|
||||||
|
logged_in=logged_in,
|
||||||
|
tema_id=tema_id,
|
||||||
|
link_id=link_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/tema/{tema_id}/editor/link/{link_id}/file")
|
||||||
|
def link_editor_file_input(
|
||||||
|
request: Request,
|
||||||
|
logged_in: auth.RequireLogin,
|
||||||
|
tema_id: int,
|
||||||
|
link_id: int,
|
||||||
|
):
|
||||||
|
return tema.link_editor_file(
|
||||||
|
request=request,
|
||||||
|
logged_in=logged_in,
|
||||||
|
tema_id=tema_id,
|
||||||
|
link_id=link_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/tema/{tema_id}/editor/property/{property_id}")
|
||||||
|
def property_editor(
|
||||||
|
request: Request,
|
||||||
|
logged_in: auth.RequireLogin,
|
||||||
|
tema_id: int,
|
||||||
|
property_id: int,
|
||||||
|
):
|
||||||
|
return tema.property_editor(
|
||||||
|
request=request,
|
||||||
|
logged_in=logged_in,
|
||||||
|
tema_id=tema_id,
|
||||||
|
property_id=property_id,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated, Optional
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Request, UploadFile
|
||||||
from fastapi.params import Form, Param
|
from fastapi.params import File, Form, Param
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse
|
||||||
from folkugat_web.api import router
|
from folkugat_web.api import router
|
||||||
|
from folkugat_web.config import db
|
||||||
from folkugat_web.fragments import tema, temes
|
from folkugat_web.fragments import tema, temes
|
||||||
from folkugat_web.model import temes as model
|
from folkugat_web.model import temes as model
|
||||||
from folkugat_web.services import auth
|
from folkugat_web.services import auth, files
|
||||||
from folkugat_web.services.temes import write as temes_w
|
from folkugat_web.services.temes import write as temes_w
|
||||||
from folkugat_web.services.temes.links import guess_link_type
|
from folkugat_web.services.temes.links import guess_link_type
|
||||||
from folkugat_web.templates import templates
|
from folkugat_web.templates import templates
|
||||||
@@ -30,6 +31,22 @@ def contingut(request: Request, logged_in: auth.LoggedIn, tema_id: int):
|
|||||||
return temes.tema(request, tema_id, logged_in)
|
return temes.tema(request, tema_id, logged_in)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/api/tema/{tema_id}")
|
||||||
|
def delete_tema(request: Request, _: auth.RequireLogin, tema_id: int):
|
||||||
|
temes_w.delete_tema(tema_id=tema_id)
|
||||||
|
return HTMLResponse(headers={
|
||||||
|
'HX-Redirect': '/temes'
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/tema")
|
||||||
|
def create_tema(request: Request, _: auth.RequireLogin, title: Annotated[str, Form()] = ""):
|
||||||
|
new_tema = temes_w.create_tema(title=title)
|
||||||
|
return HTMLResponse(headers={
|
||||||
|
'HX-Redirect': f'/tema/{new_tema.id}'
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/tema/{tema_id}/title")
|
@router.get("/api/tema/{tema_id}/title")
|
||||||
def title(request: Request, logged_in: auth.LoggedIn, tema_id: int):
|
def title(request: Request, logged_in: auth.LoggedIn, tema_id: int):
|
||||||
return tema.title(request=request, tema_id=tema_id, logged_in=logged_in)
|
return tema.title(request=request, tema_id=tema_id, logged_in=logged_in)
|
||||||
@@ -98,24 +115,28 @@ def link(request: Request, logged_in: auth.LoggedIn, tema_id: int, link_id: int)
|
|||||||
|
|
||||||
|
|
||||||
@router.put("/api/tema/{tema_id}/link/{link_id}")
|
@router.put("/api/tema/{tema_id}/link/{link_id}")
|
||||||
def set_link(
|
async def set_link(
|
||||||
request: Request,
|
request: Request,
|
||||||
logged_in: auth.RequireLogin,
|
logged_in: auth.RequireLogin,
|
||||||
tema_id: int,
|
tema_id: int,
|
||||||
link_id: int,
|
link_id: int,
|
||||||
content_type: Annotated[model.ContentType, Form()],
|
content_type: Annotated[model.ContentType, Form()],
|
||||||
url: Annotated[str, Form()] = "",
|
title: Annotated[Optional[str], Form()] = None,
|
||||||
title: Annotated[str, Form()] = "",
|
url: Annotated[Optional[str], Form()] = None,
|
||||||
|
upload_file: Annotated[Optional[UploadFile], File()] = None,
|
||||||
):
|
):
|
||||||
link_type = guess_link_type(url)
|
if upload_file:
|
||||||
|
url = await files.store_file(tema_id=tema_id, upload_file=upload_file)
|
||||||
|
|
||||||
|
link_type = guess_link_type(url or '')
|
||||||
new_link = model.Link(
|
new_link = model.Link(
|
||||||
id=link_id,
|
id=link_id,
|
||||||
content_type=content_type,
|
content_type=content_type,
|
||||||
link_type=link_type,
|
link_type=link_type,
|
||||||
url=url,
|
url=url or '',
|
||||||
title=title,
|
title=title or '',
|
||||||
)
|
)
|
||||||
temes_w.update_link(tema_id=tema_id, link_id=link_id, link=new_link)
|
new_tema = temes_w.update_link(tema_id=tema_id, link_id=link_id, link=new_link)
|
||||||
return tema.link(request=request, logged_in=logged_in, tema_id=tema_id, link_id=link_id)
|
return tema.link(request=request, logged_in=logged_in, tema_id=tema_id, link_id=link_id)
|
||||||
|
|
||||||
|
|
||||||
@@ -144,7 +165,11 @@ def delete_link(
|
|||||||
link_id: int,
|
link_id: int,
|
||||||
):
|
):
|
||||||
temes_w.delete_link(tema_id=tema_id, link_id=link_id)
|
temes_w.delete_link(tema_id=tema_id, link_id=link_id)
|
||||||
return HTMLResponse()
|
return HTMLResponse(
|
||||||
|
headers={
|
||||||
|
"HX-Trigger": f"reload-tema-{tema_id}-score"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/tema/{tema_id}/link/{link_id}/icon")
|
@router.get("/api/tema/{tema_id}/link/{link_id}/icon")
|
||||||
@@ -164,3 +189,79 @@ def link_icon(
|
|||||||
url=url,
|
url=url,
|
||||||
content_type=content_type,
|
content_type=content_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/tema/{tema_id}/score")
|
||||||
|
def get_score(
|
||||||
|
request: Request,
|
||||||
|
logged_in: auth.LoggedIn,
|
||||||
|
tema_id: int,
|
||||||
|
):
|
||||||
|
return tema.score(
|
||||||
|
request=request,
|
||||||
|
logged_in=logged_in,
|
||||||
|
tema_id=tema_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/tema/{tema_id}/property/{property_id}")
|
||||||
|
def property_(request: Request, logged_in: auth.LoggedIn, tema_id: int, property_id: int):
|
||||||
|
return tema.property_(request=request, logged_in=logged_in, tema_id=tema_id, property_id=property_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/api/tema/{tema_id}/property/{property_id}")
|
||||||
|
def set_property(
|
||||||
|
request: Request,
|
||||||
|
logged_in: auth.RequireLogin,
|
||||||
|
tema_id: int,
|
||||||
|
property_id: int,
|
||||||
|
field: Annotated[model.PropertyField, Form()],
|
||||||
|
value: Annotated[str, Form()],
|
||||||
|
):
|
||||||
|
new_property = model.Property(id=property_id, field=field, value=value.strip())
|
||||||
|
temes_w.update_property(tema_id=tema_id, property_id=property_id, prop=new_property)
|
||||||
|
return tema.property_(request=request, logged_in=logged_in, tema_id=tema_id, property_id=property_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/tema/{tema_id}/property")
|
||||||
|
def add_property(
|
||||||
|
request: Request,
|
||||||
|
logged_in: auth.RequireLogin,
|
||||||
|
tema_id: int,
|
||||||
|
):
|
||||||
|
new_tema = temes_w.add_property(tema_id=tema_id)
|
||||||
|
property_id = new_tema.properties[-1].id
|
||||||
|
if property_id is None:
|
||||||
|
raise RuntimeError("Invalid property_id on newly created property!")
|
||||||
|
return tema.property_editor(
|
||||||
|
request=request,
|
||||||
|
logged_in=logged_in,
|
||||||
|
tema_id=tema_id,
|
||||||
|
property_id=property_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/api/tema/{tema_id}/property/{property_id}")
|
||||||
|
def delete_property(_: auth.RequireLogin, tema_id: int, property_id: int):
|
||||||
|
temes_w.delete_property(tema_id=tema_id, property_id=property_id)
|
||||||
|
return HTMLResponse()
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/api/tema/{tema_id}/visible")
|
||||||
|
def set_visible(request: Request, logged_in: auth.RequireLogin, tema_id: int):
|
||||||
|
new_tema = temes_w.set_visibility(tema_id=tema_id, hidden=False)
|
||||||
|
return tema.visibility(
|
||||||
|
request=request,
|
||||||
|
logged_in=logged_in,
|
||||||
|
tema=new_tema,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/api/tema/{tema_id}/invisible")
|
||||||
|
def set_invisible(request: Request, logged_in: auth.RequireLogin, tema_id: int):
|
||||||
|
new_tema = temes_w.set_visibility(tema_id=tema_id, hidden=True)
|
||||||
|
return tema.visibility(
|
||||||
|
request=request,
|
||||||
|
logged_in=logged_in,
|
||||||
|
tema=new_tema,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
from typing import Annotated, Optional
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
|
from fastapi.params import Param
|
||||||
from folkugat_web.api import router
|
from folkugat_web.api import router
|
||||||
from folkugat_web.fragments import temes
|
from folkugat_web.fragments import temes
|
||||||
from folkugat_web.services import auth
|
from folkugat_web.services import auth
|
||||||
@@ -6,13 +9,17 @@ from folkugat_web.templates import templates
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/temes")
|
@router.get("/temes")
|
||||||
def page(request: Request, logged_in: auth.LoggedIn):
|
def page(
|
||||||
|
request: Request,
|
||||||
|
logged_in: auth.LoggedIn,
|
||||||
|
query: Annotated[str, Param()] = "",
|
||||||
|
):
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"index.html",
|
"index.html",
|
||||||
{
|
{
|
||||||
"request": request,
|
"request": request,
|
||||||
"page_title": "Folkugat",
|
"page_title": "Folkugat",
|
||||||
"content": "/api/content/temes",
|
"content": f"/api/content/temes?query={query}",
|
||||||
"logged_in": logged_in,
|
"logged_in": logged_in,
|
||||||
"animate": False,
|
"animate": False,
|
||||||
}
|
}
|
||||||
@@ -20,8 +27,12 @@ def page(request: Request, logged_in: auth.LoggedIn):
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/api/content/temes")
|
@router.get("/api/content/temes")
|
||||||
def content(request: Request, logged_in: auth.LoggedIn):
|
def content(
|
||||||
return temes.temes_pagina(request, logged_in)
|
request: Request,
|
||||||
|
logged_in: auth.LoggedIn,
|
||||||
|
query: Annotated[str, Param()] = "",
|
||||||
|
):
|
||||||
|
return temes.temes_pagina(request, logged_in, query)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/temes/busca")
|
@router.get("/api/temes/busca")
|
||||||
|
|||||||
@@ -1,5 +1,113 @@
|
|||||||
|
*, ::before, ::after {
|
||||||
|
--tw-border-spacing-x: 0;
|
||||||
|
--tw-border-spacing-y: 0;
|
||||||
|
--tw-translate-x: 0;
|
||||||
|
--tw-translate-y: 0;
|
||||||
|
--tw-rotate: 0;
|
||||||
|
--tw-skew-x: 0;
|
||||||
|
--tw-skew-y: 0;
|
||||||
|
--tw-scale-x: 1;
|
||||||
|
--tw-scale-y: 1;
|
||||||
|
--tw-pan-x: ;
|
||||||
|
--tw-pan-y: ;
|
||||||
|
--tw-pinch-zoom: ;
|
||||||
|
--tw-scroll-snap-strictness: proximity;
|
||||||
|
--tw-gradient-from-position: ;
|
||||||
|
--tw-gradient-via-position: ;
|
||||||
|
--tw-gradient-to-position: ;
|
||||||
|
--tw-ordinal: ;
|
||||||
|
--tw-slashed-zero: ;
|
||||||
|
--tw-numeric-figure: ;
|
||||||
|
--tw-numeric-spacing: ;
|
||||||
|
--tw-numeric-fraction: ;
|
||||||
|
--tw-ring-inset: ;
|
||||||
|
--tw-ring-offset-width: 0px;
|
||||||
|
--tw-ring-offset-color: #fff;
|
||||||
|
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||||
|
--tw-ring-offset-shadow: 0 0 #0000;
|
||||||
|
--tw-ring-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow-colored: 0 0 #0000;
|
||||||
|
--tw-blur: ;
|
||||||
|
--tw-brightness: ;
|
||||||
|
--tw-contrast: ;
|
||||||
|
--tw-grayscale: ;
|
||||||
|
--tw-hue-rotate: ;
|
||||||
|
--tw-invert: ;
|
||||||
|
--tw-saturate: ;
|
||||||
|
--tw-sepia: ;
|
||||||
|
--tw-drop-shadow: ;
|
||||||
|
--tw-backdrop-blur: ;
|
||||||
|
--tw-backdrop-brightness: ;
|
||||||
|
--tw-backdrop-contrast: ;
|
||||||
|
--tw-backdrop-grayscale: ;
|
||||||
|
--tw-backdrop-hue-rotate: ;
|
||||||
|
--tw-backdrop-invert: ;
|
||||||
|
--tw-backdrop-opacity: ;
|
||||||
|
--tw-backdrop-saturate: ;
|
||||||
|
--tw-backdrop-sepia: ;
|
||||||
|
--tw-contain-size: ;
|
||||||
|
--tw-contain-layout: ;
|
||||||
|
--tw-contain-paint: ;
|
||||||
|
--tw-contain-style: ;
|
||||||
|
}
|
||||||
|
|
||||||
|
::backdrop {
|
||||||
|
--tw-border-spacing-x: 0;
|
||||||
|
--tw-border-spacing-y: 0;
|
||||||
|
--tw-translate-x: 0;
|
||||||
|
--tw-translate-y: 0;
|
||||||
|
--tw-rotate: 0;
|
||||||
|
--tw-skew-x: 0;
|
||||||
|
--tw-skew-y: 0;
|
||||||
|
--tw-scale-x: 1;
|
||||||
|
--tw-scale-y: 1;
|
||||||
|
--tw-pan-x: ;
|
||||||
|
--tw-pan-y: ;
|
||||||
|
--tw-pinch-zoom: ;
|
||||||
|
--tw-scroll-snap-strictness: proximity;
|
||||||
|
--tw-gradient-from-position: ;
|
||||||
|
--tw-gradient-via-position: ;
|
||||||
|
--tw-gradient-to-position: ;
|
||||||
|
--tw-ordinal: ;
|
||||||
|
--tw-slashed-zero: ;
|
||||||
|
--tw-numeric-figure: ;
|
||||||
|
--tw-numeric-spacing: ;
|
||||||
|
--tw-numeric-fraction: ;
|
||||||
|
--tw-ring-inset: ;
|
||||||
|
--tw-ring-offset-width: 0px;
|
||||||
|
--tw-ring-offset-color: #fff;
|
||||||
|
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||||
|
--tw-ring-offset-shadow: 0 0 #0000;
|
||||||
|
--tw-ring-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow-colored: 0 0 #0000;
|
||||||
|
--tw-blur: ;
|
||||||
|
--tw-brightness: ;
|
||||||
|
--tw-contrast: ;
|
||||||
|
--tw-grayscale: ;
|
||||||
|
--tw-hue-rotate: ;
|
||||||
|
--tw-invert: ;
|
||||||
|
--tw-saturate: ;
|
||||||
|
--tw-sepia: ;
|
||||||
|
--tw-drop-shadow: ;
|
||||||
|
--tw-backdrop-blur: ;
|
||||||
|
--tw-backdrop-brightness: ;
|
||||||
|
--tw-backdrop-contrast: ;
|
||||||
|
--tw-backdrop-grayscale: ;
|
||||||
|
--tw-backdrop-hue-rotate: ;
|
||||||
|
--tw-backdrop-invert: ;
|
||||||
|
--tw-backdrop-opacity: ;
|
||||||
|
--tw-backdrop-saturate: ;
|
||||||
|
--tw-backdrop-sepia: ;
|
||||||
|
--tw-contain-size: ;
|
||||||
|
--tw-contain-layout: ;
|
||||||
|
--tw-contain-paint: ;
|
||||||
|
--tw-contain-style: ;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
! tailwindcss v3.3.3 | MIT License | https://tailwindcss.com
|
! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -32,9 +140,11 @@
|
|||||||
4. Use the user's configured `sans` font-family by default.
|
4. Use the user's configured `sans` font-family by default.
|
||||||
5. Use the user's configured `sans` font-feature-settings by default.
|
5. Use the user's configured `sans` font-feature-settings by default.
|
||||||
6. Use the user's configured `sans` font-variation-settings by default.
|
6. Use the user's configured `sans` font-variation-settings by default.
|
||||||
|
7. Disable tap highlights on iOS
|
||||||
*/
|
*/
|
||||||
|
|
||||||
html {
|
html,
|
||||||
|
:host {
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
/* 1 */
|
/* 1 */
|
||||||
-webkit-text-size-adjust: 100%;
|
-webkit-text-size-adjust: 100%;
|
||||||
@@ -44,12 +154,14 @@ html {
|
|||||||
-o-tab-size: 4;
|
-o-tab-size: 4;
|
||||||
tab-size: 4;
|
tab-size: 4;
|
||||||
/* 3 */
|
/* 3 */
|
||||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
/* 4 */
|
/* 4 */
|
||||||
font-feature-settings: normal;
|
font-feature-settings: normal;
|
||||||
/* 5 */
|
/* 5 */
|
||||||
font-variation-settings: normal;
|
font-variation-settings: normal;
|
||||||
/* 6 */
|
/* 6 */
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
/* 7 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -121,8 +233,10 @@ strong {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
1. Use the user's configured `mono` font family by default.
|
1. Use the user's configured `mono` font-family by default.
|
||||||
2. Correct the odd `em` font sizing in all browsers.
|
2. Use the user's configured `mono` font-feature-settings by default.
|
||||||
|
3. Use the user's configured `mono` font-variation-settings by default.
|
||||||
|
4. Correct the odd `em` font sizing in all browsers.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
code,
|
code,
|
||||||
@@ -131,8 +245,12 @@ samp,
|
|||||||
pre {
|
pre {
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
/* 1 */
|
/* 1 */
|
||||||
font-size: 1em;
|
font-feature-settings: normal;
|
||||||
/* 2 */
|
/* 2 */
|
||||||
|
font-variation-settings: normal;
|
||||||
|
/* 3 */
|
||||||
|
font-size: 1em;
|
||||||
|
/* 4 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -201,6 +319,8 @@ textarea {
|
|||||||
/* 1 */
|
/* 1 */
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
/* 1 */
|
/* 1 */
|
||||||
|
letter-spacing: inherit;
|
||||||
|
/* 1 */
|
||||||
color: inherit;
|
color: inherit;
|
||||||
/* 1 */
|
/* 1 */
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -224,9 +344,9 @@ select {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
button,
|
button,
|
||||||
[type='button'],
|
input:where([type='button']),
|
||||||
[type='reset'],
|
input:where([type='reset']),
|
||||||
[type='submit'] {
|
input:where([type='submit']) {
|
||||||
-webkit-appearance: button;
|
-webkit-appearance: button;
|
||||||
/* 1 */
|
/* 1 */
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@@ -430,108 +550,16 @@ video {
|
|||||||
|
|
||||||
/* Make elements with the HTML hidden attribute stay hidden by default */
|
/* Make elements with the HTML hidden attribute stay hidden by default */
|
||||||
|
|
||||||
[hidden] {
|
[hidden]:where(:not([hidden="until-found"])) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
*, ::before, ::after {
|
.visible {
|
||||||
--tw-border-spacing-x: 0;
|
visibility: visible;
|
||||||
--tw-border-spacing-y: 0;
|
|
||||||
--tw-translate-x: 0;
|
|
||||||
--tw-translate-y: 0;
|
|
||||||
--tw-rotate: 0;
|
|
||||||
--tw-skew-x: 0;
|
|
||||||
--tw-skew-y: 0;
|
|
||||||
--tw-scale-x: 1;
|
|
||||||
--tw-scale-y: 1;
|
|
||||||
--tw-pan-x: ;
|
|
||||||
--tw-pan-y: ;
|
|
||||||
--tw-pinch-zoom: ;
|
|
||||||
--tw-scroll-snap-strictness: proximity;
|
|
||||||
--tw-gradient-from-position: ;
|
|
||||||
--tw-gradient-via-position: ;
|
|
||||||
--tw-gradient-to-position: ;
|
|
||||||
--tw-ordinal: ;
|
|
||||||
--tw-slashed-zero: ;
|
|
||||||
--tw-numeric-figure: ;
|
|
||||||
--tw-numeric-spacing: ;
|
|
||||||
--tw-numeric-fraction: ;
|
|
||||||
--tw-ring-inset: ;
|
|
||||||
--tw-ring-offset-width: 0px;
|
|
||||||
--tw-ring-offset-color: #fff;
|
|
||||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
|
||||||
--tw-ring-offset-shadow: 0 0 #0000;
|
|
||||||
--tw-ring-shadow: 0 0 #0000;
|
|
||||||
--tw-shadow: 0 0 #0000;
|
|
||||||
--tw-shadow-colored: 0 0 #0000;
|
|
||||||
--tw-blur: ;
|
|
||||||
--tw-brightness: ;
|
|
||||||
--tw-contrast: ;
|
|
||||||
--tw-grayscale: ;
|
|
||||||
--tw-hue-rotate: ;
|
|
||||||
--tw-invert: ;
|
|
||||||
--tw-saturate: ;
|
|
||||||
--tw-sepia: ;
|
|
||||||
--tw-drop-shadow: ;
|
|
||||||
--tw-backdrop-blur: ;
|
|
||||||
--tw-backdrop-brightness: ;
|
|
||||||
--tw-backdrop-contrast: ;
|
|
||||||
--tw-backdrop-grayscale: ;
|
|
||||||
--tw-backdrop-hue-rotate: ;
|
|
||||||
--tw-backdrop-invert: ;
|
|
||||||
--tw-backdrop-opacity: ;
|
|
||||||
--tw-backdrop-saturate: ;
|
|
||||||
--tw-backdrop-sepia: ;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
::backdrop {
|
.invisible {
|
||||||
--tw-border-spacing-x: 0;
|
visibility: hidden;
|
||||||
--tw-border-spacing-y: 0;
|
|
||||||
--tw-translate-x: 0;
|
|
||||||
--tw-translate-y: 0;
|
|
||||||
--tw-rotate: 0;
|
|
||||||
--tw-skew-x: 0;
|
|
||||||
--tw-skew-y: 0;
|
|
||||||
--tw-scale-x: 1;
|
|
||||||
--tw-scale-y: 1;
|
|
||||||
--tw-pan-x: ;
|
|
||||||
--tw-pan-y: ;
|
|
||||||
--tw-pinch-zoom: ;
|
|
||||||
--tw-scroll-snap-strictness: proximity;
|
|
||||||
--tw-gradient-from-position: ;
|
|
||||||
--tw-gradient-via-position: ;
|
|
||||||
--tw-gradient-to-position: ;
|
|
||||||
--tw-ordinal: ;
|
|
||||||
--tw-slashed-zero: ;
|
|
||||||
--tw-numeric-figure: ;
|
|
||||||
--tw-numeric-spacing: ;
|
|
||||||
--tw-numeric-fraction: ;
|
|
||||||
--tw-ring-inset: ;
|
|
||||||
--tw-ring-offset-width: 0px;
|
|
||||||
--tw-ring-offset-color: #fff;
|
|
||||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
|
||||||
--tw-ring-offset-shadow: 0 0 #0000;
|
|
||||||
--tw-ring-shadow: 0 0 #0000;
|
|
||||||
--tw-shadow: 0 0 #0000;
|
|
||||||
--tw-shadow-colored: 0 0 #0000;
|
|
||||||
--tw-blur: ;
|
|
||||||
--tw-brightness: ;
|
|
||||||
--tw-contrast: ;
|
|
||||||
--tw-grayscale: ;
|
|
||||||
--tw-hue-rotate: ;
|
|
||||||
--tw-invert: ;
|
|
||||||
--tw-saturate: ;
|
|
||||||
--tw-sepia: ;
|
|
||||||
--tw-drop-shadow: ;
|
|
||||||
--tw-backdrop-blur: ;
|
|
||||||
--tw-backdrop-brightness: ;
|
|
||||||
--tw-backdrop-contrast: ;
|
|
||||||
--tw-backdrop-grayscale: ;
|
|
||||||
--tw-backdrop-hue-rotate: ;
|
|
||||||
--tw-backdrop-invert: ;
|
|
||||||
--tw-backdrop-opacity: ;
|
|
||||||
--tw-backdrop-saturate: ;
|
|
||||||
--tw-backdrop-sepia: ;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.static {
|
.static {
|
||||||
@@ -574,10 +602,6 @@ video {
|
|||||||
margin: 0.75rem;
|
margin: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.m-4 {
|
|
||||||
margin: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.m-6 {
|
.m-6 {
|
||||||
margin: 1.5rem;
|
margin: 1.5rem;
|
||||||
}
|
}
|
||||||
@@ -645,18 +669,18 @@ video {
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.h-4\/5 {
|
.h-4\/5 {
|
||||||
height: 80%;
|
height: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h-80 {
|
|
||||||
height: 20rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-full {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-px {
|
.h-px {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
}
|
}
|
||||||
@@ -669,14 +693,14 @@ video {
|
|||||||
width: 50%;
|
width: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.w-80 {
|
|
||||||
width: 20rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-full {
|
.w-full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.min-w-full {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.max-w-3xl {
|
.max-w-3xl {
|
||||||
max-width: 48rem;
|
max-width: 48rem;
|
||||||
}
|
}
|
||||||
@@ -685,10 +709,6 @@ video {
|
|||||||
max-width: 56rem;
|
max-width: 56rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.max-w-5xl {
|
|
||||||
max-width: 64rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.max-w-xl {
|
.max-w-xl {
|
||||||
max-width: 36rem;
|
max-width: 36rem;
|
||||||
}
|
}
|
||||||
@@ -815,6 +835,10 @@ video {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gap-2 {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.overflow-hidden {
|
.overflow-hidden {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -839,8 +863,8 @@ video {
|
|||||||
border-width: 0px;
|
border-width: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.border-solid {
|
.border-b {
|
||||||
border-style: solid;
|
border-bottom-width: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.border-none {
|
.border-none {
|
||||||
@@ -849,27 +873,22 @@ video {
|
|||||||
|
|
||||||
.border-beige {
|
.border-beige {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(178 124 9 / var(--tw-border-opacity));
|
border-color: rgb(178 124 9 / var(--tw-border-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.border-yellow-50 {
|
.border-yellow-50 {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(254 252 232 / var(--tw-border-opacity));
|
border-color: rgb(254 252 232 / var(--tw-border-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-beige {
|
.bg-beige {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(178 124 9 / var(--tw-bg-opacity));
|
background-color: rgb(178 124 9 / var(--tw-bg-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-brown {
|
.bg-brown {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(62 56 52 / var(--tw-bg-opacity));
|
background-color: rgb(62 56 52 / var(--tw-bg-opacity, 1));
|
||||||
}
|
|
||||||
|
|
||||||
.bg-yellow-50 {
|
|
||||||
--tw-bg-opacity: 1;
|
|
||||||
background-color: rgb(254 252 232 / var(--tw-bg-opacity));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.p-0 {
|
.p-0 {
|
||||||
@@ -972,22 +991,17 @@ video {
|
|||||||
|
|
||||||
.text-beige {
|
.text-beige {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(178 124 9 / var(--tw-text-opacity));
|
color: rgb(178 124 9 / var(--tw-text-opacity, 1));
|
||||||
}
|
|
||||||
|
|
||||||
.text-brown {
|
|
||||||
--tw-text-opacity: 1;
|
|
||||||
color: rgb(62 56 52 / var(--tw-text-opacity));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-white {
|
.text-white {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-yellow-50 {
|
.text-yellow-50 {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(254 252 232 / var(--tw-text-opacity));
|
color: rgb(254 252 232 / var(--tw-text-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
.opacity-0 {
|
.opacity-0 {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<li id="tema-link-{{ link.id }}">
|
<li id="tema-link-{{ link.id }}">
|
||||||
<form class="flex flex-row items-start">
|
<form class="flex flex-row items-start"
|
||||||
|
hx-encoding='multipart/form-data'>
|
||||||
{% include "fragments/tema/link_icon.html" %}
|
{% include "fragments/tema/link_icon.html" %}
|
||||||
<div class="grow my-2">
|
<div class="flex flex-col grow my-2">
|
||||||
<p class="text-sm text-beige">
|
<div class="text-sm text-beige">
|
||||||
<select id="tema-link-{{ link.id }}-content-type"
|
<select name="content_type"
|
||||||
name="content_type"
|
|
||||||
value="{{ link.content_type.value.capitalize() }}"
|
value="{{ link.content_type.value.capitalize() }}"
|
||||||
class="border border-yellow-50 focus:outline-none
|
class="border border-yellow-50 focus:outline-none
|
||||||
rounded
|
rounded
|
||||||
@@ -21,8 +21,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</div>
|
||||||
<p>
|
<div>
|
||||||
<input name="title"
|
<input name="title"
|
||||||
placeholder="Títol de l'enllaç"
|
placeholder="Títol de l'enllaç"
|
||||||
value="{{ link.title }}"
|
value="{{ link.title }}"
|
||||||
@@ -30,21 +30,8 @@
|
|||||||
rounded w-full
|
rounded w-full
|
||||||
bg-brown p-1 my-1"
|
bg-brown p-1 my-1"
|
||||||
/>
|
/>
|
||||||
</p>
|
</div>
|
||||||
<p>
|
{% include "fragments/tema/editor/link_url.html" %}
|
||||||
<input name="url"
|
|
||||||
placeholder="URL de l'enllaç"
|
|
||||||
value="{{ link.url }}"
|
|
||||||
class="border border-beige focus:outline-none
|
|
||||||
rounded w-full
|
|
||||||
bg-brown p-1 my-1"
|
|
||||||
hx-get="/api/tema/{{ tema.id }}/link/{{ link_id }}/icon"
|
|
||||||
hx-trigger="keyup delay:500ms changed"
|
|
||||||
hx-target="#link-icon-{{ link.id }}"
|
|
||||||
hx-include="[name='content_type']"
|
|
||||||
hx-swap="outerHTML"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="m-2 text-sm text-beige">
|
<div class="m-2 text-sm text-beige">
|
||||||
<button title="Desa els canvis"
|
<button title="Desa els canvis"
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<div id="link-editor-{{ link_id }}-file"
|
||||||
|
class="flex flex-row gap-2">
|
||||||
|
<input type='file'
|
||||||
|
class="border border-beige focus:outline-none
|
||||||
|
rounded grow
|
||||||
|
bg-brown p-1 my-1"
|
||||||
|
name='upload_file'>
|
||||||
|
<button title="Afegeix un enllaç"
|
||||||
|
class="border border-beige rounded px-2 py-1 my-1"
|
||||||
|
hx-get="/api/tema/{{ tema.id }}/editor/link/{{ link_id }}/url"
|
||||||
|
hx-target="#link-editor-{{ link_id }}-file"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>
|
||||||
|
<i class="fa fa-link" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<div id="link-editor-{{ link_id }}-url"
|
||||||
|
class="flex flex-row gap-2">
|
||||||
|
<input name="url"
|
||||||
|
placeholder="URL de l'enllaç"
|
||||||
|
value="{{ link.url }}"
|
||||||
|
class="border border-beige focus:outline-none
|
||||||
|
rounded grow
|
||||||
|
bg-brown p-1 my-1"
|
||||||
|
hx-get="/api/tema/{{ tema.id }}/link/{{ link_id }}/icon"
|
||||||
|
hx-trigger="keyup delay:500ms changed"
|
||||||
|
hx-target="#link-icon-{{ link.id }}"
|
||||||
|
hx-include="[name='content_type']"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
/>
|
||||||
|
<button title="Puja un fitxer"
|
||||||
|
class="border border-beige rounded py-1 px-2 my-1"
|
||||||
|
hx-get="/api/tema/{{ tema.id }}/editor/link/{{ link_id }}/file"
|
||||||
|
hx-target="#link-editor-{{ link_id }}-url"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>
|
||||||
|
<i class="fa fa-upload" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<form id="tema-property-{{ property.id }}"
|
||||||
|
class="flex flex-row items-center">
|
||||||
|
<div class="px-2">
|
||||||
|
<select name="field"
|
||||||
|
value="{{ property.field.value.capitalize() }}"
|
||||||
|
class="border border-yellow-50 focus:outline-none
|
||||||
|
rounded
|
||||||
|
text-yellow-50 text-center
|
||||||
|
bg-brown p-1 m-0">
|
||||||
|
<option value="{{ property.field.value }}">
|
||||||
|
{{ property.field.value.capitalize() }}
|
||||||
|
</option>
|
||||||
|
{% for pf in PropertyField %}
|
||||||
|
{% if pf != property.field %}
|
||||||
|
<option value="{{ pf.value }}">
|
||||||
|
{{ pf.value.capitalize() }}
|
||||||
|
</option>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="grow mx-2">
|
||||||
|
<input name="value"
|
||||||
|
placeholder="Informació"
|
||||||
|
value="{{ property.value }}"
|
||||||
|
class="border border-beige focus:outline-none
|
||||||
|
rounded w-full
|
||||||
|
bg-brown px-2 py-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button title="Desa els canvis"
|
||||||
|
class="text-sm text-beige mx-1"
|
||||||
|
hx-put="/api/tema/{{ tema.id }}/property/{{ property.id }}"
|
||||||
|
hx-target="#tema-property-{{ property.id }}"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fa fa-check" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button title="Descarta els canvis"
|
||||||
|
class="text-sm text-beige mx-1"
|
||||||
|
hx-get="/api/tema/{{ tema.id }}/property/{{ property.id }}"
|
||||||
|
hx-target="#tema-property-{{ property.id }}"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fa fa-times" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
@@ -13,12 +13,14 @@
|
|||||||
<div class="grow"></div>
|
<div class="grow"></div>
|
||||||
<div class="m-2 text-sm text-beige">
|
<div class="m-2 text-sm text-beige">
|
||||||
<button title="Modifica l'enllaç"
|
<button title="Modifica l'enllaç"
|
||||||
|
class="mx-1"
|
||||||
hx-get="/api/tema/{{ tema.id }}/editor/link/{{ link.id }}"
|
hx-get="/api/tema/{{ tema.id }}/editor/link/{{ link.id }}"
|
||||||
hx-target="#tema-link-{{ link.id }}"
|
hx-target="#tema-link-{{ link.id }}"
|
||||||
hx-swap="outerHTML">
|
hx-swap="outerHTML">
|
||||||
<i class="fa fa-pencil" aria-hidden="true"></i>
|
<i class="fa fa-pencil" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
<button title="Esborra l'enllaç"
|
<button title="Esborra l'enllaç"
|
||||||
|
class="mx-1"
|
||||||
hx-delete="/api/tema/{{ tema.id }}/link/{{ link.id }}"
|
hx-delete="/api/tema/{{ tema.id }}/link/{{ link.id }}"
|
||||||
hx-target="#tema-link-{{ link.id }}"
|
hx-target="#tema-link-{{ link.id }}"
|
||||||
hx-swap="outerHTML">
|
hx-swap="outerHTML">
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
{% if logged_in or tema.links %}
|
{% if logged_in or tema.links %}
|
||||||
<h4 class="text-xl text-beige">Enllaços</h4>
|
<h4 class="text-xl text-beige">Enllaços</h4>
|
||||||
<hr class="h-px mt-1 mb-3 bg-beige border-0">
|
<hr class="h-px mt-1 mb-3 bg-beige border-0">
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if tema.links %}
|
|
||||||
<ul class="flex flex-col justify-center"
|
<ul class="flex flex-col justify-center"
|
||||||
id="new-link-target">
|
id="new-link-target">
|
||||||
{% for link in tema.links %}
|
{% for link in tema.links %}
|
||||||
|
|||||||
@@ -4,19 +4,17 @@
|
|||||||
{% include "fragments/tema/title.html" %}
|
{% include "fragments/tema/title.html" %}
|
||||||
<div class="text-left">
|
<div class="text-left">
|
||||||
|
|
||||||
|
<div id="tema-{{ tema.id }}-score"
|
||||||
|
hx-get="/api/tema/{{ tema.id }}/score"
|
||||||
|
hx-trigger="load, reload-tema-{{ tema.id }}-score from:body"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% include "fragments/tema/lyrics.html" %}
|
{% include "fragments/tema/lyrics.html" %}
|
||||||
{% include "fragments/tema/links.html" %}
|
{% include "fragments/tema/links.html" %}
|
||||||
|
{% include "fragments/tema/properties.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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
24
folkugat_web/assets/templates/fragments/tema/properties.html
Normal file
24
folkugat_web/assets/templates/fragments/tema/properties.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{% if logged_in or tema.properties %}
|
||||||
|
<h4 class="text-xl mt-3 text-beige">Informació</h4>
|
||||||
|
<hr class="h-px mt-1 mb-3 bg-beige border-0">
|
||||||
|
|
||||||
|
<ul class="flex flex-col"
|
||||||
|
id="new-property-target">
|
||||||
|
{% for property in tema.properties %}
|
||||||
|
{% include "fragments/tema/property.html" %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if logged_in %}
|
||||||
|
<div class="flex flex-row my-2 justify-end">
|
||||||
|
<button title="Afegeix informació"
|
||||||
|
class="text-sm text-beige text-right"
|
||||||
|
hx-post="/api/tema/{{ tema.id }}/property"
|
||||||
|
hx-target="#new-property-target"
|
||||||
|
hx-swap="beforeend transition:true">
|
||||||
|
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||||
|
Afegeix informació
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
27
folkugat_web/assets/templates/fragments/tema/property.html
Normal file
27
folkugat_web/assets/templates/fragments/tema/property.html
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<div id="tema-property-{{ property.id }}"
|
||||||
|
class="flex flex-row">
|
||||||
|
<div class="px-2">
|
||||||
|
<i>{{ property.field.value.capitalize() }}:</i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ property.value }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if logged_in %}
|
||||||
|
<div class="grow"></div>
|
||||||
|
<button title="Modifica la informació"
|
||||||
|
class="text-sm text-beige mx-1"
|
||||||
|
hx-get="/api/tema/{{ tema.id }}/editor/property/{{ property.id }}"
|
||||||
|
hx-target="#tema-property-{{ property.id }}"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fa fa-pencil" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button title="Esborra la informació"
|
||||||
|
class="text-sm text-beige mx-1"
|
||||||
|
hx-delete="/api/tema/{{ tema.id }}/property/{{ property.id }}"
|
||||||
|
hx-target="#tema-property-{{ property.id }}"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fa fa-times" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
15
folkugat_web/assets/templates/fragments/tema/score.html
Normal file
15
folkugat_web/assets/templates/fragments/tema/score.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{% set score = tema.score() %}
|
||||||
|
{% if score %}
|
||||||
|
<h4 class="text-xl text-beige">Partitura</h4>
|
||||||
|
<hr class="h-px mt-1 mb-3 bg-beige border-0">
|
||||||
|
{% if score.link_type == LinkType.PDF %}
|
||||||
|
{% set pdf_url = score.url %}
|
||||||
|
{% include "fragments/pdf_viewer.html" %}
|
||||||
|
{% elif score.link_type == LinkType.IMAGE %}
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<img class="m-2"
|
||||||
|
src="{{ score.url }}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
@@ -3,11 +3,18 @@
|
|||||||
<h3 class="text-3xl p-4">{{ tema.title }}</h3>
|
<h3 class="text-3xl p-4">{{ tema.title }}</h3>
|
||||||
{% if logged_in %}
|
{% if logged_in %}
|
||||||
<button title="Canvia el títol"
|
<button title="Canvia el títol"
|
||||||
class="text-beige text-2xl"
|
class="text-beige text-2xl mx-2"
|
||||||
hx-get="/api/tema/{{ tema.id }}/editor/title"
|
hx-get="/api/tema/{{ tema.id }}/editor/title"
|
||||||
hx-target="#tema-title"
|
hx-target="#tema-title"
|
||||||
hx-swap="outerHTML">
|
hx-swap="outerHTML">
|
||||||
<i class="fa fa-pencil" aria-hidden="true"></i>
|
<i class="fa fa-pencil" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
|
{% include "fragments/tema/visibility.html" %}
|
||||||
|
<button title="Canvia el títol"
|
||||||
|
class="text-beige text-2xl mx-2"
|
||||||
|
hx-delete="/api/tema/{{ tema.id }}"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fa fa-times" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
15
folkugat_web/assets/templates/fragments/tema/visibility.html
Normal file
15
folkugat_web/assets/templates/fragments/tema/visibility.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{% if not tema.hidden %}
|
||||||
|
<button title="Tema visible (amaga'l)"
|
||||||
|
class="text-beige text-2xl mx-2"
|
||||||
|
hx-put="/api/tema/{{ tema.id }}/invisible"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fa fa-eye" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<button title="Tema invisible (mostra'l)"
|
||||||
|
class="text-beige text-2xl mx-2"
|
||||||
|
hx-put="/api/tema/{{ tema.id }}/visible"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fa fa-eye-slash" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
{% include "fragments/menu.html" %}
|
{% include "fragments/menu.html" %}
|
||||||
<div class="p-12 text-center flex flex-col items-center justify-center">
|
<div class="p-12 text-center flex flex-col items-center justify-center">
|
||||||
<h3 class="text-3xl text-beige p-4">Temes</h3>
|
<h3 class="text-3xl text-beige p-4">Temes</h3>
|
||||||
<button title="Afegeix un tema"
|
<input id="query-input"
|
||||||
class="text-beige m-2">
|
type="text" name="query" value="{{ query }}"
|
||||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
|
||||||
Afegeix un tema
|
|
||||||
</button>
|
|
||||||
<input type="text" name="query" value=""
|
|
||||||
placeholder="Busca una tema..."
|
placeholder="Busca una tema..."
|
||||||
class="rounded
|
class="rounded
|
||||||
text-yellow-50 bg-brown
|
text-yellow-50 bg-brown
|
||||||
@@ -15,10 +11,16 @@
|
|||||||
hx-get="/api/temes/busca"
|
hx-get="/api/temes/busca"
|
||||||
hx-trigger="revealed, keyup delay:500ms changed"
|
hx-trigger="revealed, keyup delay:500ms changed"
|
||||||
hx-target="#search-results">
|
hx-target="#search-results">
|
||||||
<div class="flex justify-center">
|
{% if logged_in %}
|
||||||
<div id="search-results"
|
<button title="Afegeix un tema"
|
||||||
class="m-4 max-w-5xl
|
class="text-beige m-2"
|
||||||
flex flex-wrap items-center justify-center">
|
hx-post="/api/tema"
|
||||||
</div>
|
hx-include="[name=query]"
|
||||||
</div>
|
hx-vals="js:{title: document.getElementById('query-input').value}"
|
||||||
|
>
|
||||||
|
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||||
|
Afegeix un tema
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
<div id="search-results" class="flex-col"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
<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.content_type == ContentType.AUDIO %}
|
|
||||||
{% if link.link_type == LinkType.SPOTIFY %}
|
|
||||||
{% include "icons/spotify.svg" %}
|
|
||||||
{% elif link.link_type == LinkType.YOUTUBE %}
|
|
||||||
{% include "icons/youtube.svg" %}
|
|
||||||
{% else %}
|
|
||||||
{% include "icons/notes.svg" %}
|
|
||||||
{% endif %}
|
|
||||||
{% elif link.content_type == ContentType.PARTITURA %}
|
|
||||||
{% if link.link_type == LinkType.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>
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<ul class="flex flex-row items-center">
|
||||||
|
{% if tema.links %}
|
||||||
|
{% for link in tema.links %}
|
||||||
|
<li class="px-2 text-beige">
|
||||||
|
<a href="{{ link.url }}" target="_blank">
|
||||||
|
{% if link.content_type == ContentType.AUDIO %}
|
||||||
|
{% if link.link_type == LinkType.SPOTIFY %}
|
||||||
|
<i class="fa fa-music" aria-hidden="true"></i>
|
||||||
|
{% elif link.link_type == LinkType.YOUTUBE %}
|
||||||
|
<i class="fa fa-play" aria-hidden="true"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="fa fa-music" aria-hidden="true"></i>
|
||||||
|
{% endif %}
|
||||||
|
{% elif link.content_type == ContentType.PARTITURA %}
|
||||||
|
{% if link.link_type == LinkType.PDF %}
|
||||||
|
<i class="fa fa-file" aria-hidden="true"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="fa fa-link" aria-hidden="true"></i>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<i class="fa fa-link" aria-hidden="true"></i>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
19
folkugat_web/assets/templates/fragments/temes/results.html
Normal file
19
folkugat_web/assets/templates/fragments/temes/results.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<table id="search-results"
|
||||||
|
class="text-left min-w-full w-full">
|
||||||
|
<tr class="border-b border-beige">
|
||||||
|
<td class="font-bold py-2 px-4">Nom</td>
|
||||||
|
<td class="font-bold py-2 px-4">Enllaços</td>
|
||||||
|
</tr>
|
||||||
|
{% for tema in temes %}
|
||||||
|
<tr class="border-b border-beige">
|
||||||
|
<td class="py-2 px-4">
|
||||||
|
<a href="/tema/{{ tema.id }}">
|
||||||
|
{{ tema.title }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-4">
|
||||||
|
{% include "fragments/temes/result_links.html" %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
@@ -5,6 +5,15 @@ from folkugat_web.log import logger
|
|||||||
|
|
||||||
DB_DIR = Path(os.getenv("DB_DIR", "/home/marc/tmp/folkugat")).resolve()
|
DB_DIR = Path(os.getenv("DB_DIR", "/home/marc/tmp/folkugat")).resolve()
|
||||||
DB_FILE = DB_DIR / "folkugat.db"
|
DB_FILE = DB_DIR / "folkugat.db"
|
||||||
DB_TEMES_DIR = DB_DIR / "temes"
|
|
||||||
|
|
||||||
logger.info(f"Using DB_DIR: {DB_DIR}")
|
logger.info(f"Using DB_DIR: {DB_DIR}")
|
||||||
|
|
||||||
|
# Files db
|
||||||
|
|
||||||
|
DB_FILES_DIR = DB_DIR / "fitxer"
|
||||||
|
DB_FILES_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
DB_FILES_URL = "/db/fitxer"
|
||||||
|
|
||||||
|
_MB = 1024 * 1024
|
||||||
|
FILE_MAX_SIZE = 10 * _MB
|
||||||
|
|||||||
@@ -8,11 +8,10 @@ from .write import insert_tema
|
|||||||
|
|
||||||
def create_db(con: Optional[Connection] = None):
|
def create_db(con: Optional[Connection] = None):
|
||||||
with get_connection(con) as con:
|
with get_connection(con) as con:
|
||||||
drop_temes_table(con)
|
|
||||||
create_temes_table(con)
|
create_temes_table(con)
|
||||||
|
|
||||||
for tema in data.TEMES:
|
# for tema in data.TEMES:
|
||||||
insert_tema(tema, con)
|
# insert_tema(tema, con)
|
||||||
|
|
||||||
|
|
||||||
def drop_temes_table(con: Connection):
|
def drop_temes_table(con: Connection):
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ def _get_tema_id_to_ngrams(con: Optional[Connection] = None) -> dict[int, model.
|
|||||||
query = """
|
query = """
|
||||||
SELECT id, ngrams
|
SELECT id, ngrams
|
||||||
FROM temes
|
FROM temes
|
||||||
WHERE hidden = 0
|
|
||||||
"""
|
"""
|
||||||
with get_connection(con) as con:
|
with get_connection(con) as con:
|
||||||
cur = con.cursor()
|
cur = con.cursor()
|
||||||
|
|||||||
@@ -42,3 +42,16 @@ def update_tema(tema: model.Tema, con: Optional[Connection] = None):
|
|||||||
cur.execute(query, data)
|
cur.execute(query, data)
|
||||||
evict_tema_id_to_ngrams_cache()
|
evict_tema_id_to_ngrams_cache()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def delete_tema(tema_id: int, con: Optional[Connection] = None):
|
||||||
|
query = """
|
||||||
|
DELETE FROM temes
|
||||||
|
WHERE id = :id
|
||||||
|
"""
|
||||||
|
data = dict(id=tema_id)
|
||||||
|
with get_connection(con) as con:
|
||||||
|
cur = con.cursor()
|
||||||
|
cur.execute(query, data)
|
||||||
|
evict_tema_id_to_ngrams_cache()
|
||||||
|
return
|
||||||
|
|||||||
@@ -86,6 +86,9 @@ def link(request: Request, logged_in: bool, tema_id: int, link_id: int):
|
|||||||
"link": link,
|
"link": link,
|
||||||
"LinkType": model.LinkType,
|
"LinkType": model.LinkType,
|
||||||
"ContentType": model.ContentType,
|
"ContentType": model.ContentType,
|
||||||
|
},
|
||||||
|
headers={
|
||||||
|
"HX-Trigger": f"reload-tema-{tema_id}-score"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -110,14 +113,46 @@ def link_editor(request: Request, logged_in: bool, tema_id: int, link_id: int):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def link_icon(
|
def link_editor_url(request: Request, logged_in: bool, tema_id: int, link_id: int):
|
||||||
request: Request,
|
tema = temes_q.get_tema_by_id(tema_id)
|
||||||
logged_in: bool,
|
if tema is None:
|
||||||
tema_id: int,
|
raise ValueError(f"No tune exists for tema_id: {tema_id}")
|
||||||
link_id: int,
|
link = tema.links.get(link_id)
|
||||||
url: str,
|
return templates.TemplateResponse(
|
||||||
content_type: model.ContentType,
|
"fragments/tema/editor/link_url.html",
|
||||||
):
|
{
|
||||||
|
"request": request,
|
||||||
|
"logged_in": logged_in,
|
||||||
|
"tema": tema,
|
||||||
|
"link_id": link_id,
|
||||||
|
"link": link,
|
||||||
|
"LinkType": model.LinkType,
|
||||||
|
"ContentType": model.ContentType,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def link_editor_file(request: Request, logged_in: bool, tema_id: int, link_id: 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}")
|
||||||
|
link = tema.links.get(link_id)
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"fragments/tema/editor/link_file.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"logged_in": logged_in,
|
||||||
|
"tema": tema,
|
||||||
|
"link_id": link_id,
|
||||||
|
"link": link,
|
||||||
|
"LinkType": model.LinkType,
|
||||||
|
"ContentType": model.ContentType,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def link_icon(request: Request, logged_in: bool, tema_id: int, link_id: int, url: str,
|
||||||
|
content_type: model.ContentType):
|
||||||
link = model.Link(
|
link = model.Link(
|
||||||
id=link_id,
|
id=link_id,
|
||||||
content_type=content_type,
|
content_type=content_type,
|
||||||
@@ -136,3 +171,67 @@ def link_icon(
|
|||||||
"ContentType": model.ContentType,
|
"ContentType": model.ContentType,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def score(request: Request, logged_in: bool, tema_id: 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}")
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"fragments/tema/score.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"logged_in": logged_in,
|
||||||
|
"tema": tema,
|
||||||
|
"LinkType": model.LinkType,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def property_(request: Request, logged_in: bool, tema_id: int, property_id: 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}")
|
||||||
|
prop = tema.properties.get(property_id)
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"fragments/tema/property.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"logged_in": logged_in,
|
||||||
|
"tema": tema,
|
||||||
|
"property_id": property_id,
|
||||||
|
"property": prop,
|
||||||
|
"PropertyField": model.PropertyField,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def property_editor(request: Request, logged_in: bool, tema_id: int, property_id: 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}")
|
||||||
|
prop = tema.properties.get(property_id)
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"fragments/tema/editor/property.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"logged_in": logged_in,
|
||||||
|
"tema": tema,
|
||||||
|
"property_id": property_id,
|
||||||
|
"property": prop,
|
||||||
|
"PropertyField": model.PropertyField,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def visibility(request: Request, logged_in: bool, tema: model.Tema):
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"fragments/tema/visibility.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"logged_in": logged_in,
|
||||||
|
"tema": tema,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
@@ -6,36 +6,30 @@ from folkugat_web.services.temes import search as temes_s
|
|||||||
from folkugat_web.templates import templates
|
from folkugat_web.templates import templates
|
||||||
|
|
||||||
|
|
||||||
def temes_pagina(request: Request, logged_in: bool):
|
def temes_pagina(request: Request, logged_in: bool, query: str):
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"fragments/temes/pagina.html",
|
"fragments/temes/pagina.html",
|
||||||
{
|
{
|
||||||
"request": request,
|
"request": request,
|
||||||
"logged_in": logged_in,
|
"logged_in": logged_in,
|
||||||
|
"query": query,
|
||||||
"Pages": Pages,
|
"Pages": Pages,
|
||||||
"menu_selected_id": Pages.Temes,
|
"menu_selected_id": Pages.Temes,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def temes_busca_result(request: Request, tema: model.Tema, logged_in: bool):
|
def temes_busca(request: Request, query: str, logged_in: bool):
|
||||||
|
temes = temes_s.busca_temes(query=query, hidden=logged_in)
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"fragments/temes/result.html",
|
"fragments/temes/results.html",
|
||||||
{
|
{
|
||||||
"request": request,
|
"request": request,
|
||||||
"logged_in": logged_in,
|
"logged_in": logged_in,
|
||||||
"tema": tema,
|
"temes": temes,
|
||||||
"LinkType": model.LinkType,
|
"LinkType": model.LinkType,
|
||||||
"ContentType": model.ContentType,
|
"ContentType": model.ContentType,
|
||||||
}
|
}
|
||||||
).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]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ from fastapi import FastAPI
|
|||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from folkugat_web import log
|
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
|
from folkugat_web.api import router
|
||||||
|
from folkugat_web.config import db, directories
|
||||||
|
from folkugat_web.dal.sql.ddl import create_db
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
@@ -43,7 +43,7 @@ def get_app() -> FastAPI:
|
|||||||
app.include_router(router)
|
app.include_router(router)
|
||||||
|
|
||||||
app.mount("/static", StaticFiles(directory=directories.STATIC_DIR), name="static")
|
app.mount("/static", StaticFiles(directory=directories.STATIC_DIR), name="static")
|
||||||
app.mount("/db/temes", StaticFiles(directory=db.DB_TEMES_DIR), name="temes")
|
app.mount(db.DB_FILES_URL, StaticFiles(directory=db.DB_FILES_DIR), name="fitxers")
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|||||||
@@ -123,3 +123,6 @@ class Tema:
|
|||||||
def with_ngrams(self):
|
def with_ngrams(self):
|
||||||
self.compute_ngrams()
|
self.compute_ngrams()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def score(self) -> Optional[Link]:
|
||||||
|
return next(filter(lambda l: l.content_type is ContentType.PARTITURA, self.links), None)
|
||||||
|
|||||||
58
folkugat_web/services/files.py
Normal file
58
folkugat_web/services/files.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import mimetypes
|
||||||
|
import re
|
||||||
|
import uuid
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import magic
|
||||||
|
from fastapi import HTTPException, UploadFile
|
||||||
|
from folkugat_web.config import db
|
||||||
|
|
||||||
|
|
||||||
|
async def get_mimetype(upload_file: UploadFile) -> str:
|
||||||
|
info = magic.detect_from_content(await upload_file.read(2048))
|
||||||
|
await upload_file.seek(0)
|
||||||
|
return info.mime_type
|
||||||
|
|
||||||
|
|
||||||
|
ACCEPTED_MIMETYPES = [
|
||||||
|
re.compile(r"image/.+"),
|
||||||
|
re.compile(r".+/pdf"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def check_mimetype(mimetype: str) -> None:
|
||||||
|
if not any(regex.match(mimetype) for regex in ACCEPTED_MIMETYPES):
|
||||||
|
raise HTTPException(status_code=400, detail=f"Unsupported file type: {mimetype}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_db_file_path(filepath: Path) -> str:
|
||||||
|
return f"{db.DB_FILES_URL}/{filepath.relative_to(db.DB_FILES_DIR)}"
|
||||||
|
|
||||||
|
|
||||||
|
async def store_file(tema_id: int, upload_file: UploadFile) -> str:
|
||||||
|
if not upload_file.size:
|
||||||
|
raise HTTPException(status_code=400, detail="Couldn't find out the size of the file")
|
||||||
|
if upload_file.size > db.FILE_MAX_SIZE:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"The uploaded file is too big (max size = {db.FILE_MAX_SIZE} bytes)",
|
||||||
|
)
|
||||||
|
|
||||||
|
mimetype = await get_mimetype(upload_file)
|
||||||
|
check_mimetype(mimetype)
|
||||||
|
|
||||||
|
extension = mimetypes.guess_extension(mimetype) or ""
|
||||||
|
filename = str(uuid.uuid4().hex) + extension
|
||||||
|
filedir = db.DB_FILES_DIR / str(tema_id)
|
||||||
|
filedir.mkdir(exist_ok=True)
|
||||||
|
filepath = filedir / filename
|
||||||
|
|
||||||
|
with open(filepath, "wb") as f:
|
||||||
|
f.write(await upload_file.read())
|
||||||
|
|
||||||
|
return get_db_file_path(filepath)
|
||||||
|
|
||||||
|
|
||||||
|
def list_files(tema_id: str) -> list[str]:
|
||||||
|
filedir = db.DB_FILES_DIR / str(tema_id)
|
||||||
|
return [get_db_file_path(f) for f in filedir.iterdir()]
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import functools
|
||||||
import time
|
import time
|
||||||
|
import typing
|
||||||
|
from collections.abc import Callable, Iterable, Iterator
|
||||||
|
|
||||||
import Levenshtein
|
import Levenshtein
|
||||||
from folkugat_web.config import search as config
|
from folkugat_web.config import search as config
|
||||||
@@ -18,9 +21,10 @@ def get_query_word_similarity(query_word: str, text_ngrams: model.NGrams) -> sea
|
|||||||
candidate_ngrams = ((m, ngram)
|
candidate_ngrams = ((m, ngram)
|
||||||
for m, ngrams in map(lambda i: (i, text_ngrams.get(i, [])), ns)
|
for m, ngrams in map(lambda i: (i, text_ngrams.get(i, [])), ns)
|
||||||
for ngram in ngrams)
|
for ngram in ngrams)
|
||||||
return min(search_model.SearchMatch(distance=Levenshtein.distance(query_word, ngram)/m,
|
return min((search_model.SearchMatch(distance=Levenshtein.distance(query_word, ngram)/m,
|
||||||
ngram=ngram)
|
ngram=ngram)
|
||||||
for m, ngram in candidate_ngrams)
|
for m, ngram in candidate_ngrams),
|
||||||
|
default=search_model.SearchMatch(distance=float("inf"), ngram=""))
|
||||||
|
|
||||||
|
|
||||||
def get_query_similarity(query: str, ngrams: model.NGrams) -> search_model.SearchMatch:
|
def get_query_similarity(query: str, ngrams: model.NGrams) -> search_model.SearchMatch:
|
||||||
@@ -44,14 +48,24 @@ def build_result(query: str, entry: tuple[int, model.NGrams]) -> search_model.Qu
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def busca_temes(query: str) -> list[model.Tema]:
|
T = typing.TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
def _thread(it: Iterable[T], *funcs: Callable[[Iterable], Iterable]) -> Iterable:
|
||||||
|
return functools.reduce(lambda i, fn: fn(i), funcs, it)
|
||||||
|
|
||||||
|
|
||||||
|
def busca_temes(query: str, hidden: bool = False, limit: int = 20, offset: int = 0) -> list[model.Tema]:
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
with get_connection() as con:
|
with get_connection() as con:
|
||||||
tema_id_to_ngrams = temes_q.get_tema_id_to_ngrams(con)
|
result = _thread(
|
||||||
search_results = (build_result(query, entry) for entry in tema_id_to_ngrams.items())
|
temes_q.get_tema_id_to_ngrams(con).items(),
|
||||||
filtered_results = filter(lambda qr: qr.distance <= config.SEARCH_DISTANCE_THRESHOLD, search_results)
|
lambda tema_id_to_ngrams: (build_result(query, entry) for entry in tema_id_to_ngrams),
|
||||||
# filtered_results = filter(lambda qr: True, search_results)
|
lambda results: filter(lambda qr: qr.distance <= config.SEARCH_DISTANCE_THRESHOLD, results),
|
||||||
sorted_results = sorted(filtered_results, key=lambda qr: qr.distance)
|
lambda results: sorted(results, key=lambda qr: qr.distance),
|
||||||
sorted_temes = list(filter(None, map(lambda qr: temes_q.get_tema_by_id(qr.id, con), sorted_results)))
|
lambda results: filter(None, map(lambda qr: temes_q.get_tema_by_id(qr.id, con), results)),
|
||||||
|
lambda results: filter(lambda t: hidden or not t.hidden, results),
|
||||||
|
)
|
||||||
|
result = list(result)[offset:offset + limit]
|
||||||
logger.info(f"Search time: { int((time.time() - t0) * 1000) } ms")
|
logger.info(f"Search time: { int((time.time() - t0) * 1000) } ms")
|
||||||
return sorted_temes
|
return result
|
||||||
|
|||||||
@@ -6,6 +6,14 @@ from folkugat_web.dal.sql.temes import write as temes_w
|
|||||||
from folkugat_web.model import temes as model
|
from folkugat_web.model import temes as model
|
||||||
|
|
||||||
|
|
||||||
|
def create_tema(title: str = "") -> model.Tema:
|
||||||
|
return temes_w.insert_tema(tema=model.Tema(title=title))
|
||||||
|
|
||||||
|
|
||||||
|
def delete_tema(tema_id: int) -> None:
|
||||||
|
tema = temes_w.delete_tema(tema_id=tema_id)
|
||||||
|
|
||||||
|
|
||||||
def update_title(tema_id: int, title: str) -> model.Tema:
|
def update_title(tema_id: int, title: str) -> model.Tema:
|
||||||
with get_connection() as con:
|
with get_connection() as con:
|
||||||
tema = temes_q.get_tema_by_id(tema_id=tema_id, con=con)
|
tema = temes_q.get_tema_by_id(tema_id=tema_id, con=con)
|
||||||
@@ -98,3 +106,53 @@ def add_link(tema_id: int) -> model.Tema:
|
|||||||
temes_w.update_tema(tema=tema, con=con)
|
temes_w.update_tema(tema=tema, con=con)
|
||||||
|
|
||||||
return tema
|
return tema
|
||||||
|
|
||||||
|
|
||||||
|
def update_property(tema_id: int, property_id: int, prop: model.Property) -> model.Tema:
|
||||||
|
with get_connection() as con:
|
||||||
|
tema = temes_q.get_tema_by_id(tema_id=tema_id, con=con)
|
||||||
|
if tema is None:
|
||||||
|
raise ValueError(f"No tune found with tema_id = {tema_id}!")
|
||||||
|
|
||||||
|
tema.properties.replace(property_id, prop)
|
||||||
|
temes_w.update_tema(tema=tema, con=con)
|
||||||
|
|
||||||
|
return tema
|
||||||
|
|
||||||
|
|
||||||
|
def delete_property(tema_id: int, property_id: int) -> model.Tema:
|
||||||
|
with get_connection() as con:
|
||||||
|
tema = temes_q.get_tema_by_id(tema_id=tema_id, con=con)
|
||||||
|
if tema is None:
|
||||||
|
raise ValueError(f"No tune found with tema_id = {tema_id}!")
|
||||||
|
|
||||||
|
tema.properties.delete(property_id)
|
||||||
|
temes_w.update_tema(tema=tema, con=con)
|
||||||
|
|
||||||
|
return tema
|
||||||
|
|
||||||
|
|
||||||
|
def add_property(tema_id: int) -> model.Tema:
|
||||||
|
with get_connection() as con:
|
||||||
|
tema = temes_q.get_tema_by_id(tema_id=tema_id, con=con)
|
||||||
|
if tema is None:
|
||||||
|
raise ValueError(f"No tune found with tema_id = {tema_id}!")
|
||||||
|
tema.properties.append(model.Property(
|
||||||
|
id=None,
|
||||||
|
field=model.PropertyField.AUTOR,
|
||||||
|
value="",
|
||||||
|
))
|
||||||
|
temes_w.update_tema(tema=tema, con=con)
|
||||||
|
|
||||||
|
return tema
|
||||||
|
|
||||||
|
|
||||||
|
def set_visibility(tema_id: int, hidden: bool) -> model.Tema:
|
||||||
|
with get_connection() as con:
|
||||||
|
tema = temes_q.get_tema_by_id(tema_id=tema_id, con=con)
|
||||||
|
if tema is None:
|
||||||
|
raise ValueError(f"No tune found with tema_id = {tema_id}!")
|
||||||
|
tema.hidden = hidden
|
||||||
|
temes_w.update_tema(tema=tema, con=con)
|
||||||
|
|
||||||
|
return tema
|
||||||
|
|||||||
Reference in New Issue
Block a user