Migrated links, lyrics and properties

This commit is contained in:
marc
2025-03-23 21:46:04 +01:00
parent c097811e40
commit d596861a2e
47 changed files with 1403 additions and 844 deletions

View File

@@ -59,6 +59,7 @@
shellHook = '' shellHook = ''
export PYTHONPATH=${pythonEnv}/${python.sitePackages}:$PYTHONPATH export PYTHONPATH=${pythonEnv}/${python.sitePackages}:$PYTHONPATH
export PYTHONPATH=$(pwd):$PYTHONPATH
''; '';
}; };
}); });

View File

@@ -1 +1 @@
from . import editor, index from . import editor, index, links, lyrics, properties

View File

@@ -11,78 +11,3 @@ def title_editor(
tema_id: int, tema_id: int,
): ):
return tema.title_editor(request=request, logged_in=logged_in, tema_id=tema_id) return tema.title_editor(request=request, logged_in=logged_in, tema_id=tema_id)
@router.get("/api/tema/{tema_id}/editor/lyric/{lyric_id}")
def lyric_editor(
request: Request,
logged_in: auth.RequireLogin,
tema_id: int,
lyric_id: int,
):
return tema.lyric_editor(
request=request,
logged_in=logged_in,
tema_id=tema_id,
lyric_id=lyric_id,
)
@router.get("/api/tema/{tema_id}/editor/link/{link_id}")
def link_editor(
request: Request,
logged_in: auth.RequireLogin,
tema_id: int,
link_id: int,
):
return tema.link_editor(
request=request,
logged_in=logged_in,
tema_id=tema_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,
)

View File

@@ -1,15 +1,18 @@
from typing import Annotated from typing import Annotated
from fastapi import Request, UploadFile from fastapi import HTTPException, Request
from fastapi.params import File, Form, Param from fastapi.params import Form
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.fragments import tema, temes 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 import auth, files from folkugat_web.services.temes import links as links_service
from folkugat_web.services.temes import lyrics as lyrics_service
from folkugat_web.services.temes import properties as properties_service
from folkugat_web.services.temes import query as temes_q
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.templates import templates from folkugat_web.templates import templates
from folkugat_web.utils import FnChain
@router.get("/tema/{tema_id}") @router.get("/tema/{tema_id}")
@@ -27,7 +30,17 @@ def page(request: Request, logged_in: auth.LoggedIn, tema_id: int):
@router.get("/api/tema/{tema_id}") @router.get("/api/tema/{tema_id}")
def contingut(request: Request, logged_in: auth.LoggedIn, tema_id: int): def contingut(request: Request, logged_in: auth.LoggedIn, tema_id: int):
return temes.tema(request, tema_id, logged_in) tema = temes_q.get_tema_by_id(tema_id)
if not tema:
raise HTTPException(status_code=404, detail="Could not find tune")
tema = (
FnChain.transform(tema) |
temes_q.tema_compute_stats |
links_service.add_links_to_tema |
lyrics_service.add_lyrics_to_tema |
properties_service.add_properties_to_tema
).result()
return temes.tema(request, logged_in, tema)
@router.delete("/api/tema/{tema_id}") @router.delete("/api/tema/{tema_id}")
@@ -62,190 +75,6 @@ def set_title(
return tema.title(request=request, tema=new_tema, logged_in=logged_in) return tema.title(request=request, tema=new_tema, logged_in=logged_in)
@router.get("/api/tema/{tema_id}/lyric/{lyric_id}")
def lyric(request: Request, logged_in: auth.LoggedIn, tema_id: int, lyric_id: int):
return tema.lyric(request=request, logged_in=logged_in, tema_id=tema_id, lyric_id=lyric_id)
@router.put("/api/tema/{tema_id}/lyric/{lyric_id}")
def set_lyric(
request: Request,
logged_in: auth.RequireLogin,
tema_id: int,
lyric_id: int,
title: Annotated[str, Form()],
lyric: Annotated[str, Form()],
):
new_lyric = model.Lyrics(id=lyric_id, title=title, content=lyric.strip())
_ = temes_w.update_lyric(tema_id=tema_id, lyric_id=lyric_id, lyric=new_lyric)
return tema.lyric(request=request, logged_in=logged_in, tema_id=tema_id, lyric_id=lyric_id)
@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_id = new_tema.lyrics[-1].id
if lyric_id is None:
raise RuntimeError("Invalid lyric_id on newly created lyric!")
return tema.lyric_editor(
request=request,
logged_in=logged_in,
tema_id=tema_id,
lyric_id=lyric_id,
)
@router.delete("/api/tema/{tema_id}/lyric/{lyric_id}")
def delete_lyric(
tema_id: int,
lyric_id: int,
):
_ = temes_w.delete_lyric(tema_id=tema_id, lyric_id=lyric_id)
return HTMLResponse()
@router.get("/api/tema/{tema_id}/link/{link_id}")
def link(request: Request, logged_in: auth.LoggedIn, tema_id: int, link_id: int):
return tema.link(request=request, logged_in=logged_in, tema_id=tema_id, link_id=link_id)
@router.put("/api/tema/{tema_id}/link/{link_id}")
async def set_link(
request: Request,
logged_in: auth.RequireLogin,
tema_id: int,
link_id: int,
content_type: Annotated[model.ContentType, Form()],
title: Annotated[str | None, Form()] = None,
url: Annotated[str | None, Form()] = None,
upload_file: Annotated[UploadFile | None, File()] = None,
):
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(
id=link_id,
content_type=content_type,
link_type=link_type,
url=url or '',
title=title or '',
)
_ = 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)
@router.post("/api/tema/{tema_id}/link")
def add_link(
request: Request,
logged_in: auth.RequireLogin,
tema_id: int,
):
new_tema = temes_w.add_link(tema_id=tema_id)
link_id = new_tema.links[-1].id
if link_id is None:
raise RuntimeError("Invalid link_id on newly created link!")
return tema.link_editor(
request=request,
logged_in=logged_in,
tema_id=tema_id,
link_id=link_id,
)
@router.delete("/api/tema/{tema_id}/link/{link_id}")
def delete_link(
_: auth.RequireLogin,
tema_id: int,
link_id: int,
):
_tema = temes_w.delete_link(tema_id=tema_id, link_id=link_id)
return HTMLResponse(
headers={
"HX-Trigger": f"reload-tema-{tema_id}-score"
}
)
@router.get("/api/tema/{tema_id}/link/{link_id}/icon")
def link_icon(
request: Request,
logged_in: auth.LoggedIn,
tema_id: int,
link_id: int,
content_type: Annotated[model.ContentType, Param()],
url: Annotated[str, Param()],
):
return tema.link_icon(
request=request,
logged_in=logged_in,
tema_id=tema_id,
link_id=link_id,
url=url,
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):
_tema = temes_w.delete_property(tema_id=tema_id, property_id=property_id)
return HTMLResponse()
@router.put("/api/tema/{tema_id}/visible") @router.put("/api/tema/{tema_id}/visible")
def set_visible(request: Request, logged_in: auth.RequireLogin, tema_id: int): def set_visible(request: Request, logged_in: auth.RequireLogin, tema_id: int):
new_tema = temes_w.set_visibility(tema_id=tema_id, hidden=False) new_tema = temes_w.set_visibility(tema_id=tema_id, hidden=False)

View File

@@ -0,0 +1,160 @@
from typing import Annotated
from fastapi import HTTPException, Request, UploadFile
from fastapi.params import File, Form, Param
from fastapi.responses import HTMLResponse
from folkugat_web.api import router
from folkugat_web.fragments.tema import links as links_fragments
from folkugat_web.model import temes as model
from folkugat_web.services import auth, files
from folkugat_web.services.temes import links as links_service
@router.get("/api/tema/{tema_id}/link/{link_id}")
def link(request: Request, logged_in: auth.LoggedIn, tema_id: int, link_id: int):
link = links_service.get_link_by_id(link_id=link_id, tema_id=tema_id)
if not link:
raise HTTPException(status_code=404, detail="Could not find link!")
return links_fragments.link(request=request, logged_in=logged_in, link=link)
@router.put("/api/tema/{tema_id}/link/{link_id}")
async def set_link(
request: Request,
logged_in: auth.RequireLogin,
tema_id: int,
link_id: int,
content_type: Annotated[model.ContentType, Form()],
title: Annotated[str | None, Form()] = None,
url: Annotated[str | None, Form()] = None,
upload_file: Annotated[UploadFile | None, File()] = None,
):
if upload_file:
url = await files.store_file(tema_id=tema_id, upload_file=upload_file)
link_type = links_service.guess_link_type(url or '')
new_link = model.Link(
id=link_id,
tema_id=tema_id,
content_type=content_type,
link_type=link_type,
url=(url or '').strip(),
title=(title or '').strip(),
)
links_service.update_link(link=new_link)
return links_fragments.link(request=request, logged_in=logged_in, link=new_link)
@router.post("/api/tema/{tema_id}/link")
def create_link(
request: Request,
logged_in: auth.RequireLogin,
tema_id: int,
):
link = links_service.create_link(tema_id=tema_id)
return links_fragments.link_editor(
request=request,
logged_in=logged_in,
link=link,
)
@router.delete("/api/tema/{tema_id}/link/{link_id}")
def delete_link(
_logged_in: auth.RequireLogin,
tema_id: int,
link_id: int,
):
links_service.delete_link(link_id=link_id, tema_id=tema_id)
return HTMLResponse(
headers={
"HX-Trigger": f"reload-tema-{tema_id}-score"
}
)
@router.get("/api/tema/{tema_id}/link/{link_id}/icon")
def link_icon(
request: Request,
logged_in: auth.LoggedIn,
tema_id: int,
link_id: int,
content_type: Annotated[model.ContentType, Param()],
url: Annotated[str, Param()],
):
link = model.Link(
id=link_id,
tema_id=tema_id,
content_type=content_type,
link_type=links_service.guess_link_type(url),
url=url,
)
return links_fragments.link_icon(
request=request,
logged_in=logged_in,
link=link,
)
@router.get("/api/tema/{tema_id}/score")
def get_score(
request: Request,
logged_in: auth.LoggedIn,
tema_id: int,
):
return links_fragments.score(
request=request,
logged_in=logged_in,
tema_id=tema_id,
)
@router.get("/api/tema/{tema_id}/editor/link/{link_id}")
def link_editor(
request: Request,
logged_in: auth.RequireLogin,
tema_id: int,
link_id: int,
):
link = links_service.get_link_by_id(link_id=link_id, tema_id=tema_id)
if not link:
raise HTTPException(status_code=404, detail="Could not find link!")
return links_fragments.link_editor(
request=request,
logged_in=logged_in,
link=link,
)
@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,
):
link = links_service.get_link_by_id(link_id=link_id, tema_id=tema_id)
if not link:
raise HTTPException(status_code=404, detail="Could not find link!")
return links_fragments.link_editor_url(
request=request,
logged_in=logged_in,
link=link,
)
@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,
):
link = links_service.get_link_by_id(link_id=link_id, tema_id=tema_id)
if not link:
raise HTTPException(status_code=404, detail="Could not find link!")
return links_fragments.link_editor_file(
request=request,
logged_in=logged_in,
link=link,
)

View File

@@ -0,0 +1,80 @@
import dataclasses
from typing import Annotated
from fastapi import HTTPException, Request
from fastapi.params import Form
from fastapi.responses import HTMLResponse
from folkugat_web.api import router
from folkugat_web.fragments.tema import lyrics as lyrics_fragments
from folkugat_web.services import auth
from folkugat_web.services.temes import lyrics as lyrics_service
@router.get("/api/tema/{tema_id}/lyric/{lyric_id}")
def lyric(
request: Request,
logged_in: auth.LoggedIn,
tema_id: int,
lyric_id: int,
):
lyric = lyrics_service.get_lyric_by_id(lyric_id=lyric_id, tema_id=tema_id)
if not lyric:
raise HTTPException(status_code=404, detail="Could not find lyric!")
return lyrics_fragments.lyric(request=request, logged_in=logged_in, lyric=lyric)
@router.put("/api/tema/{tema_id}/lyric/{lyric_id}")
def set_lyric(
request: Request,
logged_in: auth.RequireLogin,
tema_id: int,
lyric_id: int,
title: Annotated[str, Form()],
content: Annotated[str, Form()],
):
lyric = lyrics_service.get_lyric_by_id(lyric_id=lyric_id, tema_id=tema_id)
if not lyric:
raise HTTPException(status_code=404, detail="Could not find lyric!")
new_lyric = dataclasses.replace(lyric, title=title, content=content.strip())
lyrics_service.update_lyric(lyric=new_lyric)
return lyrics_fragments.lyric(request=request, logged_in=logged_in, lyric=new_lyric)
@router.post("/api/tema/{tema_id}/lyric")
def add_lyric(
request: Request,
logged_in: auth.RequireLogin,
tema_id: int,
):
lyric = lyrics_service.create_lyric(tema_id=tema_id)
return lyrics_fragments.lyric_editor(
request=request,
logged_in=logged_in,
lyric=lyric,
)
@router.delete("/api/tema/{tema_id}/lyric/{lyric_id}")
def delete_lyric(
tema_id: int,
lyric_id: int,
):
lyrics_service.delete_lyric(lyric_id=lyric_id, tema_id=tema_id)
return HTMLResponse()
@router.get("/api/tema/{tema_id}/editor/lyric/{lyric_id}")
def lyric_editor(
request: Request,
logged_in: auth.RequireLogin,
tema_id: int,
lyric_id: int,
):
lyric = lyrics_service.get_lyric_by_id(lyric_id=lyric_id, tema_id=tema_id)
if not lyric:
raise HTTPException(status_code=404, detail="Could not find lyric!")
return lyrics_fragments.lyric_editor(
request=request,
logged_in=logged_in,
lyric=lyric,
)

View File

@@ -0,0 +1,73 @@
import dataclasses
from typing import Annotated
from fastapi import HTTPException, Request
from fastapi.params import Form
from fastapi.responses import HTMLResponse
from folkugat_web.api import router
from folkugat_web.fragments.tema import properties as properties_fragments
from folkugat_web.model import temes as model
from folkugat_web.services import auth
from folkugat_web.services.temes import properties as properties_service
@router.get("/api/tema/{tema_id}/property/{property_id}")
def property_(request: Request, logged_in: auth.LoggedIn, tema_id: int, property_id: int):
property = properties_service.get_property_by_id(property_id=property_id, tema_id=tema_id)
if not property:
raise HTTPException(status_code=404, detail="Could not find lyric!")
return properties_fragments.property_(request=request, logged_in=logged_in, property=property)
@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()],
):
property = properties_service.get_property_by_id(property_id=property_id, tema_id=tema_id)
if not property:
raise HTTPException(status_code=404, detail="Could not find lyric!")
new_property = dataclasses.replace(property, field=field, value=value.strip())
properties_service.update_property(property=new_property)
return properties_fragments.property_(request=request, logged_in=logged_in, property=new_property)
@router.post("/api/tema/{tema_id}/property")
def add_property(
request: Request,
logged_in: auth.RequireLogin,
tema_id: int,
):
property = properties_service.create_property(tema_id=tema_id)
return properties_fragments.property_editor(
request=request,
logged_in=logged_in,
property=property,
)
@router.delete("/api/tema/{tema_id}/property/{property_id}")
def delete_property(_: auth.RequireLogin, tema_id: int, property_id: int):
properties_service.delete_property(property_id=property_id, tema_id=tema_id)
return HTMLResponse()
@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,
):
property = properties_service.get_property_by_id(property_id=property_id, tema_id=tema_id)
if not property:
raise HTTPException(status_code=404, detail="Could not find lyric!")
return properties_fragments.property_editor(
request=request,
logged_in=logged_in,
property=property,
)

View File

@@ -37,4 +37,7 @@
<div> <div>
<span><span id="page_num">?</span> / <span id="page_count">?</span></span> <span><span id="page_num">?</span> / <span id="page_count">?</span></span>
</div> </div>
<div class="text-beige border rounded border-beige m-2 p-1">
<a href="{{ pdf_url }}" target="_blank">Obre el PDF</a>
</div>
</div> </div>

View File

@@ -36,14 +36,14 @@
<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"
class="mx-1" class="mx-1"
hx-put="/api/tema/{{ tema.id }}/link/{{ link.id }}" hx-put="/api/tema/{{ link.tema_id }}/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-check" aria-hidden="true"></i> <i class="fa fa-check" aria-hidden="true"></i>
</button> </button>
<button title="Descarta els canvis" <button title="Descarta els canvis"
class="mx-1" class="mx-1"
hx-get="/api/tema/{{ tema.id }}/link/{{ link.id }}" hx-get="/api/tema/{{ link.tema_id }}/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-times" aria-hidden="true"></i> <i class="fa fa-times" aria-hidden="true"></i>

View File

@@ -1,4 +1,4 @@
<div id="link-editor-{{ link_id }}-file" <div id="link-editor-{{ link.id }}-file"
class="flex flex-row gap-2"> class="flex flex-row gap-2">
<input type='file' <input type='file'
class="border border-beige focus:outline-none class="border border-beige focus:outline-none
@@ -7,8 +7,8 @@
name='upload_file'> name='upload_file'>
<button title="Afegeix un enllaç" <button title="Afegeix un enllaç"
class="border border-beige rounded px-2 py-1 my-1" class="border border-beige rounded px-2 py-1 my-1"
hx-get="/api/tema/{{ tema.id }}/editor/link/{{ link_id }}/url" hx-get="/api/tema/{{ link.tema_id }}/editor/link/{{ link.id }}/url"
hx-target="#link-editor-{{ link_id }}-file" hx-target="#link-editor-{{ link.id }}-file"
hx-swap="outerHTML" hx-swap="outerHTML"
> >
<i class="fa fa-link" aria-hidden="true"></i> <i class="fa fa-link" aria-hidden="true"></i>

View File

@@ -1,4 +1,4 @@
<div id="link-editor-{{ link_id }}-url" <div id="link-editor-{{ link.id }}-url"
class="flex flex-row gap-2"> class="flex flex-row gap-2">
<input name="url" <input name="url"
placeholder="URL de l'enllaç" placeholder="URL de l'enllaç"
@@ -6,7 +6,7 @@
class="border border-beige focus:outline-none class="border border-beige focus:outline-none
rounded grow rounded grow
bg-brown p-1 my-1" bg-brown p-1 my-1"
hx-get="/api/tema/{{ tema.id }}/link/{{ link_id }}/icon" hx-get="/api/tema/{{ link.tema_id }}/link/{{ link.id }}/icon"
hx-trigger="keyup delay:500ms changed" hx-trigger="keyup delay:500ms changed"
hx-target="#link-icon-{{ link.id }}" hx-target="#link-icon-{{ link.id }}"
hx-include="[name='content_type']" hx-include="[name='content_type']"
@@ -14,8 +14,8 @@
/> />
<button title="Puja un fitxer" <button title="Puja un fitxer"
class="border border-beige rounded py-1 px-2 my-1" class="border border-beige rounded py-1 px-2 my-1"
hx-get="/api/tema/{{ tema.id }}/editor/link/{{ link_id }}/file" hx-get="/api/tema/{{ link.tema_id }}/editor/link/{{ link.id }}/file"
hx-target="#link-editor-{{ link_id }}-url" hx-target="#link-editor-{{ link.id }}-url"
hx-swap="outerHTML" hx-swap="outerHTML"
> >
<i class="fa fa-upload" aria-hidden="true"></i> <i class="fa fa-upload" aria-hidden="true"></i>

View File

@@ -9,21 +9,21 @@
/> />
<button title="Desa els canvis" <button title="Desa els canvis"
class="mx-1" class="mx-1"
hx-put="/api/tema/{{ tema.id }}/lyric/{{ lyric.id }}" hx-put="/api/tema/{{ lyric.tema_id }}/lyric/{{ lyric.id }}"
hx-target="#tema-lyric-{{ lyric.id }}" hx-target="#tema-lyric-{{ lyric.id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">
<i class="fa fa-check" aria-hidden="true"></i> <i class="fa fa-check" aria-hidden="true"></i>
</button> </button>
<button title="Descarta els canvis" <button title="Descarta els canvis"
class="mx-1" class="mx-1"
hx-get="/api/tema/{{ tema.id }}/lyric/{{ lyric.id }}" hx-get="/api/tema/{{ lyric.tema_id }}/lyric/{{ lyric.id }}"
hx-target="#tema-lyric-{{ lyric.id }}" hx-target="#tema-lyric-{{ lyric.id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">
<i class="fa fa-times" aria-hidden="true"></i> <i class="fa fa-times" aria-hidden="true"></i>
</button> </button>
</h5> </h5>
<hr class="h-px mt-1 mb-3 bg-beige border-0"> <hr class="h-px mt-1 mb-3 bg-beige border-0">
<textarea name="lyric" <textarea name="content"
placeholder="Lletra" placeholder="Lletra"
rows="{{ lyric.content.count('\n') + 1 }}" rows="{{ lyric.content.count('\n') + 1 }}"
class="border border-beige focus:outline-none class="border border-beige focus:outline-none

View File

@@ -30,14 +30,14 @@
</div> </div>
<button title="Desa els canvis" <button title="Desa els canvis"
class="text-sm text-beige mx-1" class="text-sm text-beige mx-1"
hx-put="/api/tema/{{ tema.id }}/property/{{ property.id }}" hx-put="/api/tema/{{ property.tema_id }}/property/{{ property.id }}"
hx-target="#tema-property-{{ property.id }}" hx-target="#tema-property-{{ property.id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">
<i class="fa fa-check" aria-hidden="true"></i> <i class="fa fa-check" aria-hidden="true"></i>
</button> </button>
<button title="Descarta els canvis" <button title="Descarta els canvis"
class="text-sm text-beige mx-1" class="text-sm text-beige mx-1"
hx-get="/api/tema/{{ tema.id }}/property/{{ property.id }}" hx-get="/api/tema/{{ property.tema_id }}/property/{{ property.id }}"
hx-target="#tema-property-{{ property.id }}" hx-target="#tema-property-{{ property.id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">
<i class="fa fa-times" aria-hidden="true"></i> <i class="fa fa-times" aria-hidden="true"></i>

View File

@@ -14,14 +14,14 @@
<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" class="mx-1"
hx-get="/api/tema/{{ tema.id }}/editor/link/{{ link.id }}" hx-get="/api/tema/{{ link.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" class="mx-1"
hx-delete="/api/tema/{{ tema.id }}/link/{{ link.id }}" hx-delete="/api/tema/{{ link.tema_id }}/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-times" aria-hidden="true"></i> <i class="fa fa-times" aria-hidden="true"></i>

View File

@@ -11,7 +11,7 @@
{% if logged_in %} {% if logged_in %}
<div class="flex flex-row my-2 justify-end"> <div class="flex flex-row my-2 justify-end">
<button title="Afegeix una lletra" <button title="Afegeix un enllaç"
class="text-sm text-beige text-right" class="text-sm text-beige text-right"
hx-post="/api/tema/{{ tema.id }}/link" hx-post="/api/tema/{{ tema.id }}/link"
hx-target="#new-link-target" hx-target="#new-link-target"

View File

@@ -4,14 +4,14 @@
{% if logged_in %} {% if logged_in %}
<button title="Modifica la lletra" <button title="Modifica la lletra"
class="mx-1" class="mx-1"
hx-get="/api/tema/{{ tema.id }}/editor/lyric/{{ lyric.id }}" hx-get="/api/tema/{{ lyric.tema_id }}/editor/lyric/{{ lyric.id }}"
hx-target="#tema-lyric-{{ lyric.id }}" hx-target="#tema-lyric-{{ lyric.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 la lletra" <button title="Esborra la lletra"
class="mx-1" class="mx-1"
hx-delete="/api/tema/{{ tema.id }}/lyric/{{ lyric.id }}" hx-delete="/api/tema/{{ lyric.tema_id }}/lyric/{{ lyric.id }}"
hx-target="#tema-lyric-{{ lyric.id }}" hx-target="#tema-lyric-{{ lyric.id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">
<i class="fa fa-times" aria-hidden="true"></i> <i class="fa fa-times" aria-hidden="true"></i>

View File

@@ -11,14 +11,14 @@
<div class="grow"></div> <div class="grow"></div>
<button title="Modifica la informació" <button title="Modifica la informació"
class="text-sm text-beige mx-1" class="text-sm text-beige mx-1"
hx-get="/api/tema/{{ tema.id }}/editor/property/{{ property.id }}" hx-get="/api/tema/{{ property.tema_id }}/editor/property/{{ property.id }}"
hx-target="#tema-property-{{ property.id }}" hx-target="#tema-property-{{ property.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 la informació" <button title="Esborra la informació"
class="text-sm text-beige mx-1" class="text-sm text-beige mx-1"
hx-delete="/api/tema/{{ tema.id }}/property/{{ property.id }}" hx-delete="/api/tema/{{ property.tema_id }}/property/{{ property.id }}"
hx-target="#tema-property-{{ property.id }}" hx-target="#tema-property-{{ property.id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">
<i class="fa fa-times" aria-hidden="true"></i> <i class="fa fa-times" aria-hidden="true"></i>

View File

@@ -1,6 +1,6 @@
import sqlite3 import sqlite3
from collections.abc import Iterator
from contextlib import contextmanager from contextlib import contextmanager
from typing import Iterator, Optional
from folkugat_web.config.db import DB_FILE from folkugat_web.config.db import DB_FILE
@@ -13,6 +13,7 @@ def get_connection(con: Connection | None = None) -> Iterator[Connection]:
yield con yield con
else: else:
con = sqlite3.connect(DB_FILE) con = sqlite3.connect(DB_FILE)
_ = con.execute('PRAGMA foreign_keys = ON;')
try: try:
yield con yield con
con.commit() con.commit()

View File

@@ -6,12 +6,6 @@ def create_db(con: Connection | None = None):
create_playlists_table(con) create_playlists_table(con)
def drop_playlists_table(con: Connection):
query = "DROP TABLE IF EXISTS playlists"
cur = con.cursor()
_ = cur.execute(query)
def create_playlists_table(con: Connection): def create_playlists_table(con: Connection):
query = """ query = """
CREATE TABLE IF NOT EXISTS playlists ( CREATE TABLE IF NOT EXISTS playlists (

View File

@@ -6,12 +6,6 @@ def create_db(con: Connection | None = None):
create_sessions_table(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): def create_sessions_table(con: Connection):
query = """ query = """
CREATE TABLE IF NOT EXISTS sessions ( CREATE TABLE IF NOT EXISTS sessions (

View File

@@ -2,21 +2,16 @@ import datetime
import json import json
from typing import TypedDict from typing import TypedDict
from folkugat_web.model import IndexedList
from folkugat_web.model import search as search_model from folkugat_web.model import search as search_model
from folkugat_web.model import temes as model from folkugat_web.model import temes as model
TemaRowTuple = tuple[int, str, str, str, str, str, str, str, str, int] TemaRowTuple = tuple[int, str, str, str, str, int]
class TemaRowDict(TypedDict): class TemaRowDict(TypedDict):
id: int | None id: int | None
title: str title: str
properties: str
links: str
lyrics: str
alternatives: str alternatives: str
ngrams: str
modification_date: str modification_date: str
creation_date: str creation_date: str
hidden: int hidden: int
@@ -26,31 +21,24 @@ def tema_to_row(tema: model.Tema) -> TemaRowDict:
return { return {
'id': tema.id, 'id': tema.id,
'title': tema.title, '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), 'alternatives': json.dumps(tema.alternatives),
'ngrams': json.dumps(tema.ngrams),
'modification_date': tema.modification_date.isoformat(), 'modification_date': tema.modification_date.isoformat(),
'creation_date': tema.creation_date.isoformat(), 'creation_date': tema.creation_date.isoformat(),
'hidden': 1 if tema.hidden else 0, 'hidden': 1 if tema.hidden else 0,
} }
def cell_to_ngrams(cell: str) -> search_model.NGrams: def cell_to_ngrams(cell_str: str) -> search_model.NGrams:
return {int(n): ngrams_ for n, ngrams_ in json.loads(cell).items()} cell: dict[str, list[str]] = json.loads(cell_str)
return {int(n): ngrams_ for n, ngrams_ in cell.items()}
def row_to_tema(row: TemaRowTuple) -> model.Tema: def row_to_tema(row: TemaRowTuple) -> model.Tema:
return model.Tema( return model.Tema(
id=row[0], id=row[0],
title=row[1], title=row[1],
properties=IndexedList(map(model.Property.from_dict, json.loads(row[2]))), alternatives=json.loads(row[2]),
links=IndexedList(map(model.Link.from_dict, json.loads(row[3]))), modification_date=datetime.datetime.fromisoformat(row[3]),
lyrics=IndexedList(map(model.Lyrics.from_dict, json.loads(row[4]))), creation_date=datetime.datetime.fromisoformat(row[4]),
alternatives=json.loads(row[5]), hidden=bool(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]),
) )

View File

@@ -4,12 +4,9 @@ from folkugat_web.dal.sql import Connection, get_connection
def create_db(con: Connection | None = None): def create_db(con: Connection | None = None):
with get_connection(con) as con: with get_connection(con) as con:
create_temes_table(con) create_temes_table(con)
create_links_table(con)
create_lyrics_table(con)
def drop_temes_table(con: Connection): create_properties_table(con)
query = "DROP TABLE IF EXISTS temes"
cur = con.cursor()
_ = cur.execute(query)
def create_temes_table(con: Connection): def create_temes_table(con: Connection):
@@ -17,11 +14,7 @@ def create_temes_table(con: Connection):
CREATE TABLE IF NOT EXISTS temes ( CREATE TABLE IF NOT EXISTS temes (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
title TEXT NOT NULL, title TEXT NOT NULL,
properties TEXT,
links TEXT,
lyrics TEXT,
alternatives TEXT, alternatives TEXT,
ngrams TEXT,
creation_date TEXT NOT NULL, creation_date TEXT NOT NULL,
modification_date TEXT NOT NULL, modification_date TEXT NOT NULL,
hidden INTEGER NOT NULL hidden INTEGER NOT NULL
@@ -29,3 +22,47 @@ def create_temes_table(con: Connection):
""" """
cur = con.cursor() cur = con.cursor()
_ = cur.execute(query) _ = cur.execute(query)
def create_links_table(con: Connection):
query = """
CREATE TABLE IF NOT EXISTS tema_links (
id INTEGER PRIMARY KEY,
tema_id INTEGER,
content_type TEXT NOT NULL,
link_type TEXT,
title TEXT NOT NULL,
url TEXT NOT NULL,
FOREIGN KEY(tema_id) REFERENCES temes(id) ON DELETE CASCADE
)
"""
cur = con.cursor()
_ = cur.execute(query)
def create_lyrics_table(con: Connection):
query = """
CREATE TABLE IF NOT EXISTS tema_lyrics (
id INTEGER PRIMARY KEY,
tema_id INTEGER,
title TEXT NOT NULL,
content TEXT NOT NULL,
FOREIGN KEY(tema_id) REFERENCES temes(id) ON DELETE CASCADE
)
"""
cur = con.cursor()
_ = cur.execute(query)
def create_properties_table(con: Connection):
query = """
CREATE TABLE IF NOT EXISTS tema_properties (
id INTEGER PRIMARY KEY,
tema_id INTEGER,
field TEXT NOT NULL,
value TEXT NOT NULL,
FOREIGN KEY(tema_id) REFERENCES temes(id) ON DELETE CASCADE
)
"""
cur = con.cursor()
_ = cur.execute(query)

View File

@@ -0,0 +1,137 @@
from collections.abc import Iterable
from typing import TypedDict
from folkugat_web.dal.sql import Connection, get_connection
from folkugat_web.model import temes as model
from folkugat_web.utils import map_none
LinkRowTuple = tuple[int, int, str, str | None, str, str]
class LinkRowDict(TypedDict):
id: int | None
tema_id: int
content_type: str
link_type: str | None
title: str
url: str
def link_to_row(link: model.Link) -> LinkRowDict:
return {
"id": link.id,
"tema_id": link.tema_id,
"content_type": link.content_type.value,
"link_type": map_none(lambda lt: lt.value, link.link_type),
"title": link.title,
"url": link.url,
}
def row_to_link(row: LinkRowTuple) -> model.Link:
return model.Link(
id=row[0],
tema_id=row[1],
content_type=model.ContentType(row[2]),
link_type=map_none(model.LinkType, row[3]),
title=row[4],
url=row[5],
)
class QueryData(TypedDict, total=False):
id: int
tema_id: int
def _filter_clause(
link_id: int | None,
tema_id: int | None,
) -> tuple[str, QueryData]:
filter_clauses: list[str] = []
filter_data: QueryData = {}
if link_id is not None:
filter_clauses.append("id = :id")
filter_data["id"] = link_id
if tema_id is not None:
filter_clauses.append("tema_id = :tema_id")
filter_data["tema_id"] = tema_id
filter_clause = " AND ".join(filter_clauses)
return filter_clause, filter_data
def get_links(link_id: int | None = None, tema_id: int | None = None, con: Connection | None = None) -> Iterable[model.Link]:
filter_clause, data = _filter_clause(link_id=link_id, tema_id=tema_id)
if filter_clause:
filter_clause = f"WHERE {filter_clause}"
query = f"""
SELECT
id, tema_id, content_type, link_type, title, url
FROM tema_links
{filter_clause}
"""
with get_connection(con) as con:
cur = con.cursor()
_ = cur.execute(query, data)
return map(row_to_link, cur.fetchall())
def insert_link(link: model.Link, con: Connection | None = None) -> model.Link:
data = link_to_row(link)
query = f"""
INSERT INTO tema_links
(id, tema_id, content_type, link_type, title, url)
VALUES
(:id, :tema_id, :content_type, :link_type, :title, :url)
RETURNING *
"""
with get_connection(con) as con:
cur = con.cursor()
_ = cur.execute(query, data)
row: LinkRowTuple = cur.fetchone()
return row_to_link(row)
def create_link(tema_id: int, con: Connection | None = None) -> model.Link:
new_link = model.Link(
id=None,
tema_id=tema_id,
content_type=model.ContentType.PARTITURA,
link_type=None,
url="",
title="",
)
return insert_link(new_link, con=con)
def update_link(link: model.Link, con: Connection | None = None):
data = link_to_row(link)
query = """
UPDATE tema_links
SET
tema_id = :tema_id, content_type = :content_type, link_type = :link_type,
title = :title, url = :url
WHERE
id = :id
"""
with get_connection(con) as con:
cur = con.cursor()
_ = cur.execute(query, data)
def delete_link(link_id: int, tema_id: int | None = None, con: Connection | None = None):
filter_clause, data = _filter_clause(link_id=link_id, tema_id=tema_id)
if filter_clause:
filter_clause = f"WHERE {filter_clause}"
query = f"""
DELETE FROM tema_links
{filter_clause}
"""
with get_connection(con) as con:
cur = con.cursor()
_ = cur.execute(query, data)

View File

@@ -0,0 +1,127 @@
from collections.abc import Iterable
from typing import TypedDict
from folkugat_web.dal.sql import Connection, get_connection
from folkugat_web.model import temes as model
LyricRowTuple = tuple[int, int, str, str]
class LyricRowDict(TypedDict):
id: int | None
tema_id: int
title: str
content: str
def lyric_to_row(lyric: model.Lyrics) -> LyricRowDict:
return {
"id": lyric.id,
"tema_id": lyric.tema_id,
"title": lyric.title,
"content": lyric.content,
}
def row_to_lyric(row: LyricRowTuple) -> model.Lyrics:
return model.Lyrics(
id=row[0],
tema_id=row[1],
title=row[2],
content=row[3],
)
class QueryData(TypedDict, total=False):
id: int
tema_id: int
def _filter_clause(
lyric_id: int | None,
tema_id: int | None,
) -> tuple[str, QueryData]:
filter_clauses: list[str] = []
filter_data: QueryData = {}
if lyric_id is not None:
filter_clauses.append("id = :id")
filter_data["id"] = lyric_id
if tema_id is not None:
filter_clauses.append("tema_id = :tema_id")
filter_data["tema_id"] = tema_id
filter_clause = " AND ".join(filter_clauses)
return filter_clause, filter_data
def get_lyrics(lyric_id: int | None = None, tema_id: int | None = None, con: Connection | None = None) -> Iterable[model.Lyrics]:
filter_clause, data = _filter_clause(lyric_id=lyric_id, tema_id=tema_id)
if filter_clause:
filter_clause = f"WHERE {filter_clause}"
query = f"""
SELECT
id, tema_id, title, content
FROM tema_lyrics
{filter_clause}
"""
with get_connection(con) as con:
cur = con.cursor()
_ = cur.execute(query, data)
return map(row_to_lyric, cur.fetchall())
def insert_lyric(lyric: model.Lyrics, con: Connection | None = None) -> model.Lyrics:
data = lyric_to_row(lyric)
query = f"""
INSERT INTO tema_lyrics
(id, tema_id, title, content)
VALUES
(:id, :tema_id, :title, :content)
RETURNING *
"""
with get_connection(con) as con:
cur = con.cursor()
_ = cur.execute(query, data)
row: LyricRowTuple = cur.fetchone()
return row_to_lyric(row)
def create_lyric(tema_id: int, title: str | None = None, con: Connection | None = None) -> model.Lyrics:
new_lyric = model.Lyrics(
id=None,
tema_id=tema_id,
title=title or "",
content="",
)
return insert_lyric(new_lyric, con=con)
def update_lyric(lyric: model.Lyrics, con: Connection | None = None):
data = lyric_to_row(lyric)
query = """
UPDATE tema_lyrics
SET
tema_id = :tema_id, title = :title, content = :content
WHERE
id = :id
"""
with get_connection(con) as con:
cur = con.cursor()
_ = cur.execute(query, data)
def delete_lyric(lyric_id: int, tema_id: int | None = None, con: Connection | None = None):
filter_clause, data = _filter_clause(lyric_id=lyric_id, tema_id=tema_id)
if filter_clause:
filter_clause = f"WHERE {filter_clause}"
query = f"""
DELETE FROM tema_lyrics
{filter_clause}
"""
with get_connection(con) as con:
cur = con.cursor()
_ = cur.execute(query, data)

View File

@@ -0,0 +1,127 @@
from collections.abc import Iterable
from typing import TypedDict
from folkugat_web.dal.sql import Connection, get_connection
from folkugat_web.model import temes as model
PropertyRowTuple = tuple[int, int, str, str]
class PropertyRowDict(TypedDict):
id: int | None
tema_id: int
field: str
value: str
def property_to_row(property: model.Property) -> PropertyRowDict:
return {
"id": property.id,
"tema_id": property.tema_id,
"field": property.field.value,
"value": property.value,
}
def row_to_property(row: PropertyRowTuple) -> model.Property:
return model.Property(
id=row[0],
tema_id=row[1],
field=model.PropertyField(row[2]),
value=row[3],
)
class QueryData(TypedDict, total=False):
id: int
tema_id: int
def _filter_clause(
property_id: int | None,
tema_id: int | None,
) -> tuple[str, QueryData]:
filter_clauses: list[str] = []
filter_data: QueryData = {}
if property_id is not None:
filter_clauses.append("id = :id")
filter_data["id"] = property_id
if tema_id is not None:
filter_clauses.append("tema_id = :tema_id")
filter_data["tema_id"] = tema_id
filter_clause = " AND ".join(filter_clauses)
return filter_clause, filter_data
def get_properties(property_id: int | None = None, tema_id: int | None = None, con: Connection | None = None) -> Iterable[model.Property]:
filter_clause, data = _filter_clause(property_id=property_id, tema_id=tema_id)
if filter_clause:
filter_clause = f"WHERE {filter_clause}"
query = f"""
SELECT
id, tema_id, field, value
FROM tema_properties
{filter_clause}
"""
with get_connection(con) as con:
cur = con.cursor()
_ = cur.execute(query, data)
return map(row_to_property, cur.fetchall())
def insert_property(property: model.Property, con: Connection | None = None) -> model.Property:
data = property_to_row(property)
query = f"""
INSERT INTO tema_properties
(id, tema_id, field, value)
VALUES
(:id, :tema_id, :field, :value)
RETURNING *
"""
with get_connection(con) as con:
cur = con.cursor()
_ = cur.execute(query, data)
row: PropertyRowTuple = cur.fetchone()
return row_to_property(row)
def create_property(tema_id: int, field: model.PropertyField | None = None, con: Connection | None = None) -> model.Property:
new_property = model.Property(
id=None,
tema_id=tema_id,
field=field or model.PropertyField.AUTOR,
value="",
)
return insert_property(new_property, con=con)
def update_property(property: model.Property, con: Connection | None = None):
data = property_to_row(property)
query = """
UPDATE tema_properties
SET
tema_id = :tema_id, field = :field, value = :value
WHERE
id = :id
"""
with get_connection(con) as con:
cur = con.cursor()
_ = cur.execute(query, data)
def delete_property(property_id: int, tema_id: int | None = None, con: Connection | None = None):
filter_clause, data = _filter_clause(property_id=property_id, tema_id=tema_id)
if filter_clause:
filter_clause = f"WHERE {filter_clause}"
query = f"""
DELETE FROM tema_properties
{filter_clause}
"""
with get_connection(con) as con:
cur = con.cursor()
_ = cur.execute(query, data)

View File

@@ -10,8 +10,7 @@ _tema_id_to_ngrams_cache: dict[int, search_model.NGrams] | None = None
def get_tema_by_id(tema_id: int, con: Connection | None = None) -> model.Tema | None: def get_tema_by_id(tema_id: int, con: Connection | None = None) -> model.Tema | None:
query = """ query = """
SELECT SELECT
id, title, properties, links, lyrics, alternatives, ngrams, id, title, alternatives, creation_date, modification_date, hidden
creation_date, modification_date, hidden
FROM temes FROM temes
WHERE id = :id WHERE id = :id
""" """

View File

@@ -1,4 +1,7 @@
import json
from folkugat_web.dal.sql import Connection, get_connection from folkugat_web.dal.sql import Connection, get_connection
from folkugat_web.model import search as search_model
from folkugat_web.model import temes as model from folkugat_web.model import temes as model
from . import conversion from . import conversion
@@ -8,12 +11,10 @@ from .query import evict_tema_id_to_ngrams_cache
def insert_tema(tema: model.Tema, con: Connection | None = None) -> model.Tema: def insert_tema(tema: model.Tema, con: Connection | None = None) -> model.Tema:
query = """ query = """
INSERT INTO temes INSERT INTO temes
(id, title, properties, links, lyrics, alternatives, ngrams, (id, title, alternatives, creation_date, modification_date, hidden)
creation_date, modification_date, hidden)
VALUES VALUES
(:id, :title, :properties, :links, :lyrics, :alternatives, :ngrams, (:id, :title, :alternatives, :creation_date, :modification_date, :hidden)
:creation_date, :modification_date, :hidden) RETURNING id, title, alternatives, creation_date, modification_date, hidden
RETURNING *
""" """
data = conversion.tema_to_row(tema) data = conversion.tema_to_row(tema)
with get_connection(con) as con: with get_connection(con) as con:
@@ -28,13 +29,27 @@ def update_tema(tema: model.Tema, con: Connection | None = None):
query = """ query = """
UPDATE temes UPDATE temes
SET SET
title = :title, properties = :properties, links = :links, lyrics = :lyrics, title = :title, alternatives = :alternatives, creation_date = :creation_date,
alternatives = :alternatives, ngrams = :ngrams, creation_date = :creation_date,
modification_date = :modification_date, hidden = :hidden modification_date = :modification_date, hidden = :hidden
WHERE WHERE
id = :id id = :id
""" """
data = conversion.tema_to_row(tema) data = conversion.tema_to_row(tema)
with get_connection(con) as con:
cur = con.cursor()
_ = cur.execute(query, data)
return
def update_ngrams(tema: model.Tema, con: Connection | None = None):
query = """
UPDATE temes
SET
ngrams = :ngrams
WHERE
id = :id
"""
data = dict(id=tema.id, ngrams=json.dumps(tema.ngrams()))
with get_connection(con) as con: with get_connection(con) as con:
cur = con.cursor() cur = con.cursor()
_ = cur.execute(query, data) _ = cur.execute(query, data)

View File

@@ -1,239 +0,0 @@
from fastapi import HTTPException, Request
from folkugat_web.model import temes as model
from folkugat_web.services.temes import query as temes_q
from folkugat_web.services.temes.links import guess_link_type
from folkugat_web.templates import templates
def title(request: Request, logged_in: bool, tema: model.Tema | None = None, tema_id: int | None = 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)
if not tema:
raise HTTPException(status_code=404, detail="Could not find tune")
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)
if not tema:
raise HTTPException(status_code=404, detail="Could not find tune")
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_id: int):
tema = temes_q.get_tema_by_id(tema_id)
if not tema:
raise HTTPException(status_code=404, detail="Could not find tune")
lyric = tema.lyrics.get(lyric_id)
return templates.TemplateResponse(
"fragments/tema/lyric.html",
{
"request": request,
"logged_in": logged_in,
"tema": tema,
"lyric_id": lyric_id,
"lyric": lyric,
}
)
def lyric_editor(request: Request, logged_in: bool, tema_id: int, lyric_id: int):
tema = temes_q.get_tema_by_id(tema_id)
if not tema:
raise HTTPException(status_code=404, detail="Could not find tune")
lyric = tema.lyrics.get(lyric_id)
return templates.TemplateResponse(
"fragments/tema/editor/lyric.html",
{
"request": request,
"logged_in": logged_in,
"tema": tema,
"lyric_id": lyric_id,
"lyric": lyric,
}
)
def link(request: Request, logged_in: bool, tema_id: int, link_id: int):
tema = temes_q.get_tema_by_id(tema_id)
if not tema:
raise HTTPException(status_code=404, detail="Could not find tune")
link = tema.links.get(link_id)
return templates.TemplateResponse(
"fragments/tema/link.html",
{
"request": request,
"logged_in": logged_in,
"tema": tema,
"link_id": link_id,
"link": link,
"LinkType": model.LinkType,
"ContentType": model.ContentType,
},
headers={
"HX-Trigger": f"reload-tema-{tema_id}-score"
}
)
def link_editor(request: Request, logged_in: bool, tema_id: int, link_id: int):
tema = temes_q.get_tema_by_id(tema_id)
if not tema:
raise HTTPException(status_code=404, detail="Could not find tune")
link = tema.links.get(link_id)
return templates.TemplateResponse(
"fragments/tema/editor/link.html",
{
"request": request,
"logged_in": logged_in,
"tema": tema,
"link_id": link_id,
"link": link,
"LinkType": model.LinkType,
"ContentType": model.ContentType,
}
)
def link_editor_url(request: Request, logged_in: bool, tema_id: int, link_id: int):
tema = temes_q.get_tema_by_id(tema_id)
if not tema:
raise HTTPException(status_code=404, detail="Could not find tune")
link = tema.links.get(link_id)
return templates.TemplateResponse(
"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 not tema:
raise HTTPException(status_code=404, detail="Could not find tune")
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(
id=link_id,
content_type=content_type,
link_type=guess_link_type(url),
url=url,
)
return templates.TemplateResponse(
"fragments/tema/link_icon.html",
{
"request": request,
"logged_in": logged_in,
"tema_id": tema_id,
"link_id": link_id,
"link": link,
"LinkType": model.LinkType,
"ContentType": model.ContentType,
}
)
def score(request: Request, logged_in: bool, tema_id: int):
tema = temes_q.get_tema_by_id(tema_id)
if not tema:
raise HTTPException(status_code=404, detail="Could not find tune")
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 not tema:
raise HTTPException(status_code=404, detail="Could not find tune")
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 not tema:
raise HTTPException(status_code=404, detail="Could not find tune")
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,
}
)

View File

@@ -0,0 +1,84 @@
from fastapi import HTTPException, 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: model.Tema | None = None, tema_id: int | None = 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)
if not tema:
raise HTTPException(status_code=404, detail="Could not find tune")
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)
if not tema:
raise HTTPException(status_code=404, detail="Could not find tune")
return templates.TemplateResponse(
"fragments/tema/editor/title.html",
{
"request": request,
"logged_in": logged_in,
"tema": tema,
}
)
def property_(request: Request, logged_in: bool, tema_id: int, property_id: int):
tema = temes_q.get_tema_by_id(tema_id)
if not tema:
raise HTTPException(status_code=404, detail="Could not find tune")
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 not tema:
raise HTTPException(status_code=404, detail="Could not find tune")
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,
}
)

View File

@@ -0,0 +1,88 @@
from fastapi import HTTPException, Request
from folkugat_web.model import temes as model
from folkugat_web.services.temes import query as temes_q
from folkugat_web.services.temes.links import guess_link_type
from folkugat_web.templates import templates
def link(request: Request, logged_in: bool, link: model.Link):
return templates.TemplateResponse(
"fragments/tema/link.html",
{
"request": request,
"logged_in": logged_in,
"link": link,
"LinkType": model.LinkType,
"ContentType": model.ContentType,
},
headers={
"HX-Trigger": f"reload-tema-{link.tema_id}-score"
}
)
def link_editor(request: Request, logged_in: bool, link: model.Link):
return templates.TemplateResponse(
"fragments/tema/editor/link.html",
{
"request": request,
"logged_in": logged_in,
"link": link,
"LinkType": model.LinkType,
"ContentType": model.ContentType,
}
)
def link_editor_url(request: Request, logged_in: bool, link: model.Link):
return templates.TemplateResponse(
"fragments/tema/editor/link_url.html",
{
"request": request,
"logged_in": logged_in,
"link": link,
"LinkType": model.LinkType,
"ContentType": model.ContentType,
}
)
def link_editor_file(request: Request, logged_in: bool, link: model.Link):
return templates.TemplateResponse(
"fragments/tema/editor/link_file.html",
{
"request": request,
"logged_in": logged_in,
"link": link,
"LinkType": model.LinkType,
"ContentType": model.ContentType,
}
)
def link_icon(request: Request, logged_in: bool, link: model.Link):
return templates.TemplateResponse(
"fragments/tema/link_icon.html",
{
"request": request,
"logged_in": logged_in,
"link": link,
"LinkType": model.LinkType,
"ContentType": model.ContentType,
}
)
def score(request: Request, logged_in: bool, tema_id: int):
tema = temes_q.get_tema_by_id(tema_id)
if not tema:
raise HTTPException(status_code=404, detail="Could not find tune")
return templates.TemplateResponse(
"fragments/tema/score.html",
{
"request": request,
"logged_in": logged_in,
"tema": tema,
"LinkType": model.LinkType,
}
)

View File

@@ -0,0 +1,25 @@
from fastapi import Request
from folkugat_web.model import temes as model
from folkugat_web.templates import templates
def lyric(request: Request, logged_in: bool, lyric: model.Lyrics):
return templates.TemplateResponse(
"fragments/tema/lyric.html",
{
"request": request,
"logged_in": logged_in,
"lyric": lyric,
}
)
def lyric_editor(request: Request, logged_in: bool, lyric: model.Lyrics):
return templates.TemplateResponse(
"fragments/tema/editor/lyric.html",
{
"request": request,
"logged_in": logged_in,
"lyric": lyric,
}
)

View File

@@ -0,0 +1,27 @@
from fastapi import Request
from folkugat_web.model import temes as model
from folkugat_web.templates import templates
def property_(request: Request, logged_in: bool, property: model.Property):
return templates.TemplateResponse(
"fragments/tema/property.html",
{
"request": request,
"logged_in": logged_in,
"property": property,
"PropertyField": model.PropertyField,
}
)
def property_editor(request: Request, logged_in: bool, property: model.Property):
return templates.TemplateResponse(
"fragments/tema/editor/property.html",
{
"request": request,
"logged_in": logged_in,
"property": property,
"PropertyField": model.PropertyField,
}
)

View File

@@ -1,10 +1,12 @@
from fastapi import HTTPException, Request from fastapi import Request
from folkugat_web.model import temes as model from folkugat_web.model import temes as model
from folkugat_web.model.pagines import Pages from folkugat_web.model.pagines import Pages
from folkugat_web.services import sessions as sessions_service from folkugat_web.services import sessions as sessions_service
from folkugat_web.services.temes import links as links_service
from folkugat_web.services.temes import query as temes_q from folkugat_web.services.temes import query as temes_q
from folkugat_web.services.temes import search as temes_s from folkugat_web.services.temes import search as temes_s
from folkugat_web.templates import templates from folkugat_web.templates import templates
from folkugat_web.utils import FnChain
def temes_pagina(request: Request, logged_in: bool, query: str): def temes_pagina(request: Request, logged_in: bool, query: str):
@@ -35,7 +37,12 @@ def temes_busca(request: Request, logged_in: bool, query: str, offset: int = 0,
if offset > 0: if offset > 0:
prev_offset = max(offset - limit, 0) prev_offset = max(offset - limit, 0)
temes = temes_q.temes_compute_stats(temes) temes = (
FnChain.transform(temes) |
temes_q.temes_compute_stats |
links_service.add_links_to_temes |
list
).result()
return templates.TemplateResponse( return templates.TemplateResponse(
"fragments/temes/results.html", "fragments/temes/results.html",
@@ -52,11 +59,7 @@ def temes_busca(request: Request, logged_in: bool, query: str, offset: int = 0,
) )
def tema(request: Request, tema_id: int, logged_in: bool): def tema(request: Request, logged_in: bool, tema: model.Tema):
tema = temes_q.get_tema_by_id(tema_id)
if not tema:
raise HTTPException(status_code=404, detail="Could not find tune")
tema = temes_q.tema_compute_stats(tema)
return templates.TemplateResponse( return templates.TemplateResponse(
"fragments/tema/pagina.html", "fragments/tema/pagina.html",
{ {

View File

@@ -1,3 +0,0 @@
from ._base import IndexedList
__all__ = ["IndexedList"]

View File

@@ -1,39 +0,0 @@
import dataclasses
from typing import TypeVar
from typing_extensions import override
@dataclasses.dataclass
class WithId:
id: int | None
T = TypeVar("T", bound=WithId)
class IndexedList(list[T]):
@override
def append(self, _item: T) -> None:
if _item.id is None:
_item.id = max((i.id or 0 for i in self), default=0) + 1
return super().append(_item)
def find_idx(self, _id: int) -> int:
try:
i, _ = next(filter(lambda it: it[1].id == _id, enumerate(self)))
except StopIteration:
raise ValueError(f"Could not find item with id {_id}")
return i
def get(self, _id: int) -> T:
i = self.find_idx(_id)
return self.__getitem__(i)
def delete(self, _id: int) -> None:
i = self.find_idx(_id)
return super().__delitem__(i)
def replace(self, _id: int, _obj: T) -> None:
i = self.find_idx(_id)
super().__setitem__(i, _obj)

View File

@@ -1,14 +1,11 @@
import dataclasses import dataclasses
import datetime import datetime
import enum import enum
from typing import Self
from folkugat_web.model.search import NGrams from folkugat_web.model.search import NGrams
from folkugat_web.model.sessions import Session from folkugat_web.model.sessions import Session
from folkugat_web.services import ngrams from folkugat_web.services import ngrams
from ._base import IndexedList, WithId
class ContentType(enum.Enum): class ContentType(enum.Enum):
PARTITURA = "partitura" PARTITURA = "partitura"
@@ -26,31 +23,14 @@ class LinkType(enum.Enum):
@dataclasses.dataclass @dataclasses.dataclass
class Link(WithId): class Link:
id: int | None
tema_id: int
content_type: ContentType content_type: ContentType
link_type: LinkType | None link_type: LinkType | None
url: str url: str
title: str = "" title: str = ""
def to_dict(self):
return dict(
id=self.id,
content_type=self.content_type.value,
link_type=self.link_type.value if self.link_type else None,
url=self.url,
title=self.title,
)
@classmethod
def from_dict(cls, d):
return cls(
id=d["id"],
content_type=ContentType(d["content_type"]),
link_type=LinkType(d["link_type"]) if d["link_type"] else None,
url=d["url"],
title=d["title"],
)
class PropertyField(enum.Enum): class PropertyField(enum.Enum):
AUTOR = "autor" AUTOR = "autor"
@@ -60,46 +40,20 @@ class PropertyField(enum.Enum):
@dataclasses.dataclass @dataclasses.dataclass
class Property(WithId): class Property:
id: int | None
tema_id: int
field: PropertyField field: PropertyField
value: str value: str
def to_dict(self):
return dict(
id=self.id,
field=self.field.value,
value=self.value,
)
@classmethod
def from_dict(cls, d):
return cls(
id=d["id"],
field=PropertyField(d["field"]),
value=d["value"],
)
@dataclasses.dataclass @dataclasses.dataclass
class Lyrics(WithId): class Lyrics:
id: int | None
tema_id: int
title: str title: str
content: str content: str
def to_dict(self):
return dict(
id=self.id,
title=self.title,
content=self.content,
)
@classmethod
def from_dict(cls, d) -> Self:
return cls(
id=d["id"],
title=d["title"],
content=d["content"],
)
@dataclasses.dataclass @dataclasses.dataclass
class Stats: class Stats:
@@ -112,12 +66,11 @@ class Tema:
id: int | None = None id: int | None = None
# Info # Info
title: str = "" title: str = ""
properties: IndexedList[Property] = dataclasses.field(default_factory=IndexedList) properties: list[Property] = dataclasses.field(default_factory=list)
links: IndexedList[Link] = dataclasses.field(default_factory=IndexedList) links: list[Link] = dataclasses.field(default_factory=list)
lyrics: IndexedList[Lyrics] = dataclasses.field(default_factory=IndexedList) lyrics: list[Lyrics] = dataclasses.field(default_factory=list)
# Search related # Search related
alternatives: list[str] = dataclasses.field(default_factory=list) alternatives: list[str] = dataclasses.field(default_factory=list)
ngrams: NGrams = dataclasses.field(default_factory=dict)
hidden: bool = True hidden: bool = True
# Other info # Other info
modification_date: datetime.datetime = dataclasses.field(default_factory=datetime.datetime.now) modification_date: datetime.datetime = dataclasses.field(default_factory=datetime.datetime.now)
@@ -125,12 +78,8 @@ class Tema:
# Stats # Stats
stats: Stats | None = None stats: Stats | None = None
def compute_ngrams(self): def ngrams(self) -> NGrams:
self.ngrams = ngrams.get_text_ngrams(self.title, *self.alternatives) return ngrams.get_text_ngrams(self.title, *self.alternatives)
def with_ngrams(self):
self.compute_ngrams()
return self
@staticmethod @staticmethod
def _is_score(link: Link) -> bool: def _is_score(link: Link) -> bool:

View File

@@ -48,7 +48,7 @@ async def store_file(tema_id: int, upload_file: UploadFile) -> str:
filepath = filedir / filename filepath = filedir / filename
with open(filepath, "wb") as f: with open(filepath, "wb") as f:
f.write(await upload_file.read()) _ = f.write(await upload_file.read())
return get_db_file_path(filepath) return get_db_file_path(filepath)

View File

@@ -1,6 +1,7 @@
import re import re
from typing import Optional from collections.abc import Iterable, Iterator
from folkugat_web.dal.sql.temes import links as links_dal
from folkugat_web.model import temes as model from folkugat_web.model import temes as model
IMAGE_FORMATS_RE = "|".join([ IMAGE_FORMATS_RE = "|".join([
@@ -30,3 +31,29 @@ def guess_link_type(url: str) -> model.LinkType | None:
if regex.match(url): if regex.match(url):
return link_type return link_type
return None return None
def add_links_to_tema(tema: model.Tema) -> model.Tema:
if tema.id is not None:
tema.links = list(links_dal.get_links(tema_id=tema.id))
return tema
def add_links_to_temes(temes: Iterable[model.Tema]) -> Iterator[model.Tema]:
return map(add_links_to_tema, temes)
def get_link_by_id(link_id: int, tema_id: int | None = None) -> model.Link | None:
return next(iter(links_dal.get_links(link_id=link_id, tema_id=tema_id)), None)
def update_link(link: model.Link):
links_dal.update_link(link=link)
def delete_link(link_id: int, tema_id: int | None = None):
links_dal.delete_link(link_id=link_id, tema_id=tema_id)
def create_link(tema_id: int) -> model.Link:
return links_dal.create_link(tema_id=tema_id)

View File

@@ -0,0 +1,30 @@
from collections.abc import Iterable, Iterator
from folkugat_web.dal.sql.temes import lyrics as lyrics_dal
from folkugat_web.model import temes as model
def add_lyrics_to_tema(tema: model.Tema) -> model.Tema:
if tema.id is not None:
tema.lyrics = list(lyrics_dal.get_lyrics(tema_id=tema.id))
return tema
def add_lyrics_to_temes(temes: Iterable[model.Tema]) -> Iterator[model.Tema]:
return map(add_lyrics_to_tema, temes)
def get_lyric_by_id(lyric_id: int, tema_id: int | None = None) -> model.Lyrics | None:
return next(iter(lyrics_dal.get_lyrics(lyric_id=lyric_id, tema_id=tema_id)), None)
def update_lyric(lyric: model.Lyrics):
lyrics_dal.update_lyric(lyric=lyric)
def delete_lyric(lyric_id: int, tema_id: int | None = None):
lyrics_dal.delete_lyric(lyric_id=lyric_id, tema_id=tema_id)
def create_lyric(tema_id: int) -> model.Lyrics:
return lyrics_dal.create_lyric(tema_id=tema_id)

View File

@@ -0,0 +1,30 @@
from collections.abc import Iterable, Iterator
from folkugat_web.dal.sql.temes import properties as properties_dal
from folkugat_web.model import temes as model
def add_properties_to_tema(tema: model.Tema) -> model.Tema:
if tema.id is not None:
tema.properties = list(properties_dal.get_properties(tema_id=tema.id))
return tema
def add_properties_to_temes(temes: Iterable[model.Tema]) -> Iterator[model.Tema]:
return map(add_properties_to_tema, temes)
def get_property_by_id(property_id: int, tema_id: int | None = None) -> model.Property | None:
return next(iter(properties_dal.get_properties(property_id=property_id, tema_id=tema_id)), None)
def update_property(property: model.Property):
properties_dal.update_property(property=property)
def delete_property(property_id: int, tema_id: int | None = None):
properties_dal.delete_property(property_id=property_id, tema_id=tema_id)
def create_property(tema_id: int) -> model.Property:
return properties_dal.create_property(tema_id=tema_id)

View File

@@ -1,7 +1,7 @@
import functools
import time import time
import typing from collections.abc import Iterable, Iterator
from collections.abc import Callable, Iterable from sqlite3 import Connection
from typing import Callable
import Levenshtein import Levenshtein
from folkugat_web.config import search as config from folkugat_web.config import search as config
@@ -10,6 +10,7 @@ from folkugat_web.dal.sql.temes import query as temes_q
from folkugat_web.log import logger from folkugat_web.log import logger
from folkugat_web.model import search as search_model from folkugat_web.model import search as search_model
from folkugat_web.model import temes as model from folkugat_web.model import temes as model
from folkugat_web.utils import FnChain
def get_query_word_similarity(query_word: str, text_ngrams: search_model.NGrams) -> search_model.SearchMatch: def get_query_word_similarity(query_word: str, text_ngrams: search_model.NGrams) -> search_model.SearchMatch:
@@ -33,40 +34,66 @@ def get_query_similarity(query: str, ngrams: search_model.NGrams) -> search_mode
return search_model.SearchMatch.combine_matches(word_matches) return search_model.SearchMatch.combine_matches(word_matches)
def build_result(query: str, entry: tuple[int, search_model.NGrams]) -> search_model.QueryResult: def _build_results_fn(query: str) -> Callable[[Iterable[tuple[int, search_model.NGrams]]],
if len(query) == 0: Iterator[search_model.QueryResult]]:
def build_result(entry: tuple[int, search_model.NGrams]) -> search_model.QueryResult:
if len(query) == 0:
return search_model.QueryResult(
id=entry[0],
distance=0,
ngram="",
)
match = get_query_similarity(query, entry[1])
return search_model.QueryResult( return search_model.QueryResult(
id=entry[0], id=entry[0],
distance=0, distance=match.distance,
ngram="", ngram=match.ngram,
) )
match = get_query_similarity(query, entry[1])
return search_model.QueryResult( def build_results(entries: Iterable[tuple[int, search_model.NGrams]]) -> Iterator[search_model.QueryResult]:
id=entry[0], return map(build_result, entries)
distance=match.distance,
ngram=match.ngram, return build_results
)
T = typing.TypeVar("T") def _filter_distance(qrs: Iterable[search_model.QueryResult]) -> Iterator[search_model.QueryResult]:
return filter(lambda qr: qr.distance <= config.SEARCH_DISTANCE_THRESHOLD, qrs)
@typing.no_type_check def _sort_by_distance(qrs: Iterable[search_model.QueryResult]) -> list[search_model.QueryResult]:
def _thread(it: Iterable[T], *funcs: Callable[[Iterable], Iterable]) -> Iterable: return sorted(qrs, key=lambda qr: qr.distance)
return functools.reduce(lambda i, fn: fn(i), funcs, it)
def _query_results_to_temes(con: Connection) -> Callable[[Iterable[search_model.QueryResult]], Iterator[model.Tema]]:
def fetch_temes(qrs: Iterable[search_model.QueryResult]) -> Iterator[model.Tema]:
return filter(None, map(lambda qr: temes_q.get_tema_by_id(tema_id=qr.id, con=con), qrs))
return fetch_temes
def _filter_hidden(hidden: bool) -> Callable[[Iterable[model.Tema]], Iterator[model.Tema]]:
def filter_hidden(temes: Iterable[model.Tema]) -> Iterator[model.Tema]:
return filter(lambda t: hidden or not t.hidden, temes)
return filter_hidden
def _apply_limit_offset(limit: int, offset: int) -> Callable[[Iterable[model.Tema]], list[model.Tema]]:
def apply_limit_offset(temes: Iterable[model.Tema]) -> list[model.Tema]:
return list(temes)[offset:offset + limit]
return apply_limit_offset
def busca_temes(query: str, hidden: bool = False, limit: int = 10, offset: int = 0) -> list[model.Tema]: def busca_temes(query: str, hidden: bool = False, limit: int = 10, offset: int = 0) -> list[model.Tema]:
t0 = time.time() t0 = time.time()
with get_connection() as con: with get_connection() as con:
result = _thread( result = (
temes_q.get_tema_id_to_ngrams(con).items(), FnChain.transform(temes_q.get_tema_id_to_ngrams(con).items()) |
lambda tema_id_to_ngrams: (build_result(query, entry) for entry in tema_id_to_ngrams), _build_results_fn(query) |
lambda results: filter(lambda qr: qr.distance <= config.SEARCH_DISTANCE_THRESHOLD, results), _filter_distance |
lambda results: sorted(results, key=lambda qr: qr.distance), _sort_by_distance |
lambda results: filter(None, map(lambda qr: temes_q.get_tema_by_id(qr.id, con), results)), _query_results_to_temes(con) |
lambda results: filter(lambda t: hidden or not t.hidden, results), _filter_hidden(hidden) |
) _apply_limit_offset(limit=limit, offset=offset)
result = list(result)[offset:offset + limit] ).result()
logger.info(f"Search time: { int((time.time() - t0) * 1000) } ms") logger.info(f"Search time: { int((time.time() - t0) * 1000) } ms")
return result return result

View File

@@ -7,8 +7,10 @@ from folkugat_web.model import temes as model
def create_tema(title: str = "") -> model.Tema: def create_tema(title: str = "") -> model.Tema:
new_tema = model.Tema(title=title, hidden=False).with_ngrams() with get_connection() as con:
return temes_w.insert_tema(tema=new_tema) new_tema = temes_w.insert_tema(tema=model.Tema(title=title, hidden=False), con=con)
temes_w.update_ngrams(tema=new_tema, con=con)
return new_tema
def delete_tema(tema_id: int) -> None: def delete_tema(tema_id: int) -> None:
@@ -21,131 +23,13 @@ def update_title(tema_id: int, title: str) -> model.Tema:
if tema is None: if tema is None:
raise ValueError(f"No tune found with tema_id = {tema_id}!") raise ValueError(f"No tune found with tema_id = {tema_id}!")
new_tema = dataclasses.replace(tema, title=title).with_ngrams() new_tema = dataclasses.replace(tema, title=title)
temes_w.update_tema(tema=new_tema, con=con) temes_w.update_tema(tema=new_tema, con=con)
temes_w.update_ngrams(tema=new_tema, con=con)
return new_tema return new_tema
def update_lyric(tema_id: int, lyric_id: int, lyric: model.Lyrics) -> 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.lyrics.replace(lyric_id, lyric)
temes_w.update_tema(tema=tema, con=con)
return tema
def delete_lyric(tema_id: int, lyric_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.lyrics.delete(lyric_id)
temes_w.update_tema(tema=tema, con=con)
return tema
def add_lyric(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.lyrics.append(model.Lyrics(
id=None,
title=tema.title,
content="",
))
temes_w.update_tema(tema=tema, con=con)
return tema
def update_link(tema_id: int, link_id: int, link: model.Link) -> 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.links.replace(link_id, link)
temes_w.update_tema(tema=tema, con=con)
return tema
def delete_link(tema_id: int, link_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.links.delete(link_id)
temes_w.update_tema(tema=tema, con=con)
return tema
def add_link(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.links.append(model.Link(
id=None,
title="",
url="",
content_type=model.ContentType.OTHER,
link_type=None,
))
temes_w.update_tema(tema=tema, con=con)
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: def set_visibility(tema_id: int, hidden: bool) -> 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)

View File

@@ -3,4 +3,4 @@ from typing import TypeVar
T = TypeVar("T") T = TypeVar("T")
ListOrValue = T | list[T] ListOrValue = T | list[T]
OptionalListOrValue = ListOrValue[T] | None OptionalListOrValue = T | list[T] | None

View File

@@ -1,6 +1,8 @@
from __future__ import annotations
import itertools import itertools
from collections.abc import Callable, Iterable, Iterator from collections.abc import Callable, Iterable, Iterator
from typing import Protocol, Self, TypeVar from typing import Generic, Protocol, Self, TypeVar
class SupportsLessThan(Protocol): class SupportsLessThan(Protocol):
@@ -20,3 +22,28 @@ def groupby(
) -> Iterator[tuple[KeyT, GroupT]]: ) -> Iterator[tuple[KeyT, GroupT]]:
for k, g in itertools.groupby(sorted(it, key=key_fn), key=key_fn): for k, g in itertools.groupby(sorted(it, key=key_fn), key=key_fn):
yield k, group_fn(g) yield k, group_fn(g)
U = TypeVar("U")
V = TypeVar("V")
def map_none(fn: Callable[[U], V], value: U | None) -> V | None:
if value is None:
return None
return fn(value)
class FnChain(Generic[U]):
def __init__(self, fn: Callable[[], U], /):
self._fn: Callable[[], U] = fn
def __or__(self, next_fn: Callable[[U], V], /) -> FnChain[V]:
return FnChain(lambda: next_fn(self._fn()))
def result(self) -> U:
return self._fn()
@classmethod
def transform(cls, x: V, /) -> FnChain[V]:
return FnChain(lambda: x)

View File

@@ -0,0 +1,53 @@
import json
from collections.abc import Iterable
from typing import TypedDict
from folkugat_web.dal.sql import get_connection
from folkugat_web.dal.sql.temes import links as links_dal
from folkugat_web.model.temes import ContentType, Link, LinkType
from folkugat_web.utils import map_none
query = """ SELECT id, links FROM temes """
LinkRowTuple = tuple[int, str]
class OldLinkRowDict(TypedDict):
content_type: str
link_type: str | None
title: str
url: str
def _build_link(tema_id: int, link_dict: OldLinkRowDict) -> Link:
return Link(
id=None,
tema_id=tema_id,
content_type=ContentType(link_dict["content_type"]),
link_type=map_none(LinkType, link_dict.get("link_type")),
url=link_dict["url"],
title=link_dict["title"],
)
def _build_links(row: LinkRowTuple) -> list[Link]:
tema_id = row[0]
link_dicts: list[OldLinkRowDict] = json.loads(row[1])
return [_build_link(tema_id, link_dict) for link_dict in link_dicts]
with get_connection() as con:
cur = con.cursor()
_ = cur.execute(query)
iter_rows: Iterable[LinkRowTuple] = cur.fetchall()
rows = list(map(_build_links, iter_rows))
_ = cur.execute("DELETE FROM tema_links")
for links in rows:
for link in links:
_ = links_dal.insert_link(link=link, con=con)
drop_query = """ ALTER TABLE temes DROP COLUMN links"""
_ = cur.execute(drop_query)
print("DONE!")

View File

@@ -0,0 +1,48 @@
import json
from collections.abc import Iterable
from typing import TypedDict
from folkugat_web.dal.sql import get_connection
from folkugat_web.dal.sql.temes import lyrics as lyrics_dal
from folkugat_web.model.temes import Lyrics
query = """ SELECT id, lyrics FROM temes """
LyricRowTuple = tuple[int, str]
class OldLyricRowDict(TypedDict):
title: str
content: str
def _build_lyric(tema_id: int, lyric_dict: OldLyricRowDict) -> Lyrics:
return Lyrics(
id=None,
tema_id=tema_id,
title=lyric_dict["title"],
content=lyric_dict["content"],
)
def _build_lyrics(row: LyricRowTuple) -> list[Lyrics]:
tema_id = row[0]
lyric_dicts: list[OldLyricRowDict] = json.loads(row[1])
return [_build_lyric(tema_id, lyric_dict) for lyric_dict in lyric_dicts]
with get_connection() as con:
cur = con.cursor()
_ = cur.execute(query)
iter_rows: Iterable[LyricRowTuple] = cur.fetchall()
rows = list(map(_build_lyrics, iter_rows))
_ = cur.execute("DELETE FROM tema_lyrics")
for lyrics in rows:
for lyric in lyrics:
_ = lyrics_dal.insert_lyric(lyric=lyric, con=con)
drop_query = """ ALTER TABLE temes DROP COLUMN lyrics"""
_ = cur.execute(drop_query)
print("DONE!")

View File

@@ -0,0 +1,48 @@
import json
from collections.abc import Iterable
from typing import TypedDict
from folkugat_web.dal.sql import get_connection
from folkugat_web.dal.sql.temes import properties as properties_dal
from folkugat_web.model.temes import Property, PropertyField
query = """ SELECT id, properties FROM temes """
PropertyRowTuple = tuple[int, str]
class OldPropertyRowDict(TypedDict):
field: str
value: str
def _build_property(tema_id: int, property_dict: OldPropertyRowDict) -> Property:
return Property(
id=None,
tema_id=tema_id,
field=PropertyField(property_dict["field"]),
value=property_dict["value"],
)
def _build_properties(row: PropertyRowTuple) -> list[Property]:
tema_id = row[0]
property_dicts: list[OldPropertyRowDict] = json.loads(row[1])
return [_build_property(tema_id, property_dict) for property_dict in property_dicts]
with get_connection() as con:
cur = con.cursor()
_ = cur.execute(query)
iter_rows: Iterable[PropertyRowTuple] = cur.fetchall()
rows = list(map(_build_properties, iter_rows))
_ = cur.execute("DELETE FROM tema_properties")
for properties in rows:
for property in properties:
_ = properties_dal.insert_property(property=property, con=con)
drop_query = """ ALTER TABLE temes DROP COLUMN properties"""
_ = cur.execute(drop_query)
print("DONE!")