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 = ''
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,
):
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 fastapi import Request, UploadFile
from fastapi.params import File, Form, Param
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 import tema, temes
from folkugat_web.model import temes as model
from folkugat_web.services import auth, files
from folkugat_web.services import auth
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.links import guess_link_type
from folkugat_web.templates import templates
from folkugat_web.utils import FnChain
@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}")
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}")
@@ -62,190 +75,6 @@ def set_title(
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")
def set_visible(request: Request, logged_in: auth.RequireLogin, tema_id: int):
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>
<span><span id="page_num">?</span> / <span id="page_count">?</span></span>
</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>

View File

@@ -36,14 +36,14 @@
<div class="m-2 text-sm text-beige">
<button title="Desa els canvis"
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-swap="outerHTML">
<i class="fa fa-check" aria-hidden="true"></i>
</button>
<button title="Descarta els canvis"
class="mx-1"
hx-get="/api/tema/{{ tema.id }}/link/{{ link.id }}"
hx-get="/api/tema/{{ link.tema_id }}/link/{{ link.id }}"
hx-target="#tema-link-{{ link.id }}"
hx-swap="outerHTML">
<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">
<input type='file'
class="border border-beige focus:outline-none
@@ -7,8 +7,8 @@
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-get="/api/tema/{{ link.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>

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">
<input name="url"
placeholder="URL de l'enllaç"
@@ -6,7 +6,7 @@
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-get="/api/tema/{{ link.tema_id }}/link/{{ link.id }}/icon"
hx-trigger="keyup delay:500ms changed"
hx-target="#link-icon-{{ link.id }}"
hx-include="[name='content_type']"
@@ -14,8 +14,8 @@
/>
<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-get="/api/tema/{{ link.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>

View File

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

View File

@@ -30,14 +30,14 @@
</div>
<button title="Desa els canvis"
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-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-get="/api/tema/{{ property.tema_id }}/property/{{ property.id }}"
hx-target="#tema-property-{{ property.id }}"
hx-swap="outerHTML">
<i class="fa fa-times" aria-hidden="true"></i>

View File

@@ -14,14 +14,14 @@
<div class="m-2 text-sm text-beige">
<button title="Modifica l'enllaç"
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-swap="outerHTML">
<i class="fa fa-pencil" aria-hidden="true"></i>
</button>
<button title="Esborra l'enllaç"
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-swap="outerHTML">
<i class="fa fa-times" aria-hidden="true"></i>

View File

@@ -11,7 +11,7 @@
{% if logged_in %}
<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"
hx-post="/api/tema/{{ tema.id }}/link"
hx-target="#new-link-target"

View File

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

View File

@@ -11,14 +11,14 @@
<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-get="/api/tema/{{ property.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-delete="/api/tema/{{ property.tema_id }}/property/{{ property.id }}"
hx-target="#tema-property-{{ property.id }}"
hx-swap="outerHTML">
<i class="fa fa-times" aria-hidden="true"></i>

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,12 +4,9 @@ from folkugat_web.dal.sql import Connection, get_connection
def create_db(con: Connection | None = None):
with get_connection(con) as con:
create_temes_table(con)
def drop_temes_table(con: Connection):
query = "DROP TABLE IF EXISTS temes"
cur = con.cursor()
_ = cur.execute(query)
create_links_table(con)
create_lyrics_table(con)
create_properties_table(con)
def create_temes_table(con: Connection):
@@ -17,11 +14,7 @@ def create_temes_table(con: Connection):
CREATE TABLE IF NOT EXISTS temes (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
properties TEXT,
links TEXT,
lyrics TEXT,
alternatives TEXT,
ngrams TEXT,
creation_date TEXT NOT NULL,
modification_date TEXT NOT NULL,
hidden INTEGER NOT NULL
@@ -29,3 +22,47 @@ def create_temes_table(con: Connection):
"""
cur = con.cursor()
_ = 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:
query = """
SELECT
id, title, properties, links, lyrics, alternatives, ngrams,
creation_date, modification_date, hidden
id, title, alternatives, creation_date, modification_date, hidden
FROM temes
WHERE id = :id
"""

View File

@@ -1,4 +1,7 @@
import json
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 . 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:
query = """
INSERT INTO temes
(id, title, properties, links, lyrics, alternatives, ngrams,
creation_date, modification_date, hidden)
(id, title, alternatives, creation_date, modification_date, hidden)
VALUES
(:id, :title, :properties, :links, :lyrics, :alternatives, :ngrams,
:creation_date, :modification_date, :hidden)
RETURNING *
(:id, :title, :alternatives, :creation_date, :modification_date, :hidden)
RETURNING id, title, alternatives, creation_date, modification_date, hidden
"""
data = conversion.tema_to_row(tema)
with get_connection(con) as con:
@@ -28,13 +29,27 @@ def update_tema(tema: model.Tema, con: Connection | None = None):
query = """
UPDATE temes
SET
title = :title, properties = :properties, links = :links, lyrics = :lyrics,
alternatives = :alternatives, ngrams = :ngrams, creation_date = :creation_date,
title = :title, alternatives = :alternatives, creation_date = :creation_date,
modification_date = :modification_date, hidden = :hidden
WHERE
id = :id
"""
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:
cur = con.cursor()
_ = 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.pagines import Pages
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 search as temes_s
from folkugat_web.templates import templates
from folkugat_web.utils import FnChain
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:
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(
"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):
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)
def tema(request: Request, logged_in: bool, tema: model.Tema):
return templates.TemplateResponse(
"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 datetime
import enum
from typing import Self
from folkugat_web.model.search import NGrams
from folkugat_web.model.sessions import Session
from folkugat_web.services import ngrams
from ._base import IndexedList, WithId
class ContentType(enum.Enum):
PARTITURA = "partitura"
@@ -26,31 +23,14 @@ class LinkType(enum.Enum):
@dataclasses.dataclass
class Link(WithId):
class Link:
id: int | None
tema_id: int
content_type: ContentType
link_type: LinkType | None
url: 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):
AUTOR = "autor"
@@ -60,46 +40,20 @@ class PropertyField(enum.Enum):
@dataclasses.dataclass
class Property(WithId):
class Property:
id: int | None
tema_id: int
field: PropertyField
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
class Lyrics(WithId):
class Lyrics:
id: int | None
tema_id: int
title: 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
class Stats:
@@ -112,12 +66,11 @@ class Tema:
id: int | None = None
# Info
title: str = ""
properties: IndexedList[Property] = dataclasses.field(default_factory=IndexedList)
links: IndexedList[Link] = dataclasses.field(default_factory=IndexedList)
lyrics: IndexedList[Lyrics] = dataclasses.field(default_factory=IndexedList)
properties: list[Property] = dataclasses.field(default_factory=list)
links: list[Link] = dataclasses.field(default_factory=list)
lyrics: list[Lyrics] = dataclasses.field(default_factory=list)
# Search related
alternatives: list[str] = dataclasses.field(default_factory=list)
ngrams: NGrams = dataclasses.field(default_factory=dict)
hidden: bool = True
# Other info
modification_date: datetime.datetime = dataclasses.field(default_factory=datetime.datetime.now)
@@ -125,12 +78,8 @@ class Tema:
# Stats
stats: Stats | None = None
def compute_ngrams(self):
self.ngrams = ngrams.get_text_ngrams(self.title, *self.alternatives)
def with_ngrams(self):
self.compute_ngrams()
return self
def ngrams(self) -> NGrams:
return ngrams.get_text_ngrams(self.title, *self.alternatives)
@staticmethod
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
with open(filepath, "wb") as f:
f.write(await upload_file.read())
_ = f.write(await upload_file.read())
return get_db_file_path(filepath)

View File

@@ -1,6 +1,7 @@
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
IMAGE_FORMATS_RE = "|".join([
@@ -30,3 +31,29 @@ def guess_link_type(url: str) -> model.LinkType | None:
if regex.match(url):
return link_type
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 typing
from collections.abc import Callable, Iterable
from collections.abc import Iterable, Iterator
from sqlite3 import Connection
from typing import Callable
import Levenshtein
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.model import search as search_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:
@@ -33,40 +34,66 @@ def get_query_similarity(query: str, ngrams: search_model.NGrams) -> search_mode
return search_model.SearchMatch.combine_matches(word_matches)
def build_result(query: str, entry: tuple[int, search_model.NGrams]) -> search_model.QueryResult:
if len(query) == 0:
def _build_results_fn(query: str) -> Callable[[Iterable[tuple[int, search_model.NGrams]]],
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(
id=entry[0],
distance=0,
ngram="",
distance=match.distance,
ngram=match.ngram,
)
match = get_query_similarity(query, entry[1])
return search_model.QueryResult(
id=entry[0],
distance=match.distance,
ngram=match.ngram,
)
def build_results(entries: Iterable[tuple[int, search_model.NGrams]]) -> Iterator[search_model.QueryResult]:
return map(build_result, entries)
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 _thread(it: Iterable[T], *funcs: Callable[[Iterable], Iterable]) -> Iterable:
return functools.reduce(lambda i, fn: fn(i), funcs, it)
def _sort_by_distance(qrs: Iterable[search_model.QueryResult]) -> list[search_model.QueryResult]:
return sorted(qrs, key=lambda qr: qr.distance)
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]:
t0 = time.time()
with get_connection() as con:
result = _thread(
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),
lambda results: filter(lambda qr: qr.distance <= config.SEARCH_DISTANCE_THRESHOLD, results),
lambda results: sorted(results, key=lambda qr: qr.distance),
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]
result = (
FnChain.transform(temes_q.get_tema_id_to_ngrams(con).items()) |
_build_results_fn(query) |
_filter_distance |
_sort_by_distance |
_query_results_to_temes(con) |
_filter_hidden(hidden) |
_apply_limit_offset(limit=limit, offset=offset)
).result()
logger.info(f"Search time: { int((time.time() - t0) * 1000) } ms")
return result

View File

@@ -7,8 +7,10 @@ from folkugat_web.model import temes as model
def create_tema(title: str = "") -> model.Tema:
new_tema = model.Tema(title=title, hidden=False).with_ngrams()
return temes_w.insert_tema(tema=new_tema)
with get_connection() as con:
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:
@@ -21,131 +23,13 @@ def update_title(tema_id: int, title: str) -> model.Tema:
if tema is None:
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_ngrams(tema=new_tema, con=con)
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:
with get_connection() as 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")
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
from collections.abc import Callable, Iterable, Iterator
from typing import Protocol, Self, TypeVar
from typing import Generic, Protocol, Self, TypeVar
class SupportsLessThan(Protocol):
@@ -20,3 +22,28 @@ def groupby(
) -> Iterator[tuple[KeyT, GroupT]]:
for k, g in itertools.groupby(sorted(it, key=key_fn), key=key_fn):
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!")