Added indexed lists and link edition in tunes
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1,3 @@
|
||||
.direnv
|
||||
|
||||
**/**.pyc
|
||||
|
||||
@@ -13,11 +13,31 @@ def title_editor(
|
||||
return tema.title_editor(request=request, logged_in=logged_in, tema_id=tema_id)
|
||||
|
||||
|
||||
@router.get("/api/tema/{tema_id}/editor/lyric/{lyric_idx}")
|
||||
@router.get("/api/tema/{tema_id}/editor/lyric/{lyric_id}")
|
||||
def lyric_editor(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
tema_id: int,
|
||||
lyric_idx: int,
|
||||
lyric_id: int,
|
||||
):
|
||||
return tema.lyric_editor(request=request, logged_in=logged_in, tema_id=tema_id, lyric_idx=lyric_idx)
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.params import Form
|
||||
from fastapi.params import Form, Param
|
||||
from fastapi.responses import HTMLResponse
|
||||
from folkugat_web.api import router
|
||||
from folkugat_web.fragments import tema, temes
|
||||
from folkugat_web.model import temes as model
|
||||
from folkugat_web.services import auth
|
||||
from folkugat_web.services.temes import write as temes_w
|
||||
from folkugat_web.services.temes.links import guess_link_type
|
||||
from folkugat_web.templates import templates
|
||||
|
||||
|
||||
@@ -45,23 +46,23 @@ def set_title(
|
||||
return tema.title(request=request, tema=new_tema, logged_in=logged_in)
|
||||
|
||||
|
||||
@router.get("/api/tema/{tema_id}/lyric/{lyric_idx}")
|
||||
def lyric(request: Request, logged_in: auth.LoggedIn, tema_id: int, lyric_idx: int):
|
||||
return tema.lyric(request=request, logged_in=logged_in, tema_id=tema_id, lyric_idx=lyric_idx)
|
||||
@router.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_idx}")
|
||||
@router.put("/api/tema/{tema_id}/lyric/{lyric_id}")
|
||||
def set_lyric(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
tema_id: int,
|
||||
lyric_idx: int,
|
||||
lyric_id: int,
|
||||
title: Annotated[str, Form()],
|
||||
lyric: Annotated[str, Form()],
|
||||
):
|
||||
new_lyric = model.Lyrics(title=title, content=lyric.strip())
|
||||
temes_w.update_lyric(tema_id=tema_id, lyric_idx=lyric_idx, lyric=new_lyric)
|
||||
return tema.lyric(request=request, logged_in=logged_in, tema_id=tema_id, lyric_idx=lyric_idx)
|
||||
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")
|
||||
@@ -71,14 +72,95 @@ def add_lyric(
|
||||
tema_id: int,
|
||||
):
|
||||
new_tema = temes_w.add_lyric(tema_id=tema_id)
|
||||
lyric_idx = len(new_tema.lyrics) - 1
|
||||
return tema.lyric_editor(request=request, logged_in=logged_in, tema_id=tema_id, lyric_idx=lyric_idx)
|
||||
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_idx}")
|
||||
@router.delete("/api/tema/{tema_id}/lyric/{lyric_id}")
|
||||
def delete_lyric(
|
||||
tema_id: int,
|
||||
lyric_idx: int,
|
||||
lyric_id: int,
|
||||
):
|
||||
temes_w.delete_lyric(tema_id=tema_id, lyric_idx=lyric_idx)
|
||||
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}")
|
||||
def set_link(
|
||||
request: Request,
|
||||
logged_in: auth.RequireLogin,
|
||||
tema_id: int,
|
||||
link_id: int,
|
||||
content_type: Annotated[model.ContentType, Form()],
|
||||
url: Annotated[str, Form()] = "",
|
||||
title: Annotated[str, Form()] = "",
|
||||
):
|
||||
link_type = guess_link_type(url)
|
||||
new_link = model.Link(
|
||||
id=link_id,
|
||||
content_type=content_type,
|
||||
link_type=link_type,
|
||||
url=url,
|
||||
title=title,
|
||||
)
|
||||
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,
|
||||
):
|
||||
temes_w.delete_link(tema_id=tema_id, link_id=link_id)
|
||||
return HTMLResponse()
|
||||
|
||||
|
||||
@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,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<li id="tema-link-{{ link.id }}">
|
||||
<form class="flex flex-row items-start">
|
||||
{% include "fragments/tema/link_icon.html" %}
|
||||
<div class="grow my-2">
|
||||
<p class="text-sm text-beige">
|
||||
<select id="tema-link-{{ link.id }}-content-type"
|
||||
name="content_type"
|
||||
value="{{ link.content_type.value.capitalize() }}"
|
||||
class="border border-yellow-50 focus:outline-none
|
||||
rounded
|
||||
text-yellow-50 text-center
|
||||
bg-brown p-0 m-0">
|
||||
<option value="{{ link.content_type.value }}">
|
||||
{{ link.content_type.value.capitalize() }}
|
||||
</option>
|
||||
{% for ct in ContentType %}
|
||||
{% if ct != link.content_type %}
|
||||
<option value="{{ ct.value }}">
|
||||
{{ ct.value.capitalize() }}
|
||||
</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
<input name="title"
|
||||
placeholder="Títol de l'enllaç"
|
||||
value="{{ link.title }}"
|
||||
class="border border-beige focus:outline-none
|
||||
rounded w-full
|
||||
bg-brown p-1 my-1"
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<input name="url"
|
||||
placeholder="URL de l'enllaç"
|
||||
value="{{ link.url }}"
|
||||
class="border border-beige focus:outline-none
|
||||
rounded w-full
|
||||
bg-brown p-1 my-1"
|
||||
hx-get="/api/tema/{{ tema.id }}/link/{{ link_id }}/icon"
|
||||
hx-trigger="keyup delay:500ms changed"
|
||||
hx-target="#link-icon-{{ link.id }}"
|
||||
hx-include="[name='content_type']"
|
||||
hx-swap="outerHTML"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div 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-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-target="#tema-link-{{ link.id }}"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
@@ -1,4 +1,4 @@
|
||||
<form id="tema-lyric-{{ lyric_idx }}">
|
||||
<form id="tema-lyric-{{ lyric.id }}">
|
||||
<h5 class="text-sm text-beige text-right">
|
||||
<input name="title"
|
||||
placeholder="Nom de la lletra"
|
||||
@@ -9,15 +9,15 @@
|
||||
/>
|
||||
<button title="Desa els canvis"
|
||||
class="mx-1"
|
||||
hx-put="/api/tema/{{ tema.id }}/lyric/{{ lyric_idx }}"
|
||||
hx-target="#tema-lyric-{{ lyric_idx }}"
|
||||
hx-put="/api/tema/{{ 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_idx }}"
|
||||
hx-target="#tema-lyric-{{ lyric_idx }}"
|
||||
hx-get="/api/tema/{{ tema.id }}/lyric/{{ lyric.id }}"
|
||||
hx-target="#tema-lyric-{{ lyric.id }}"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
<li class="flex flex-row items-start">
|
||||
<div class="p-2 m-2 text-beige border border-beige rounded-md">
|
||||
<a href="{{ link.url }}" target="_blank">
|
||||
{% if link.subtype == LinkSubtype.SPOTIFY %}
|
||||
{% include "icons/spotify.svg" %}
|
||||
{% elif link.subtype == LinkSubtype.YOUTUBE %}
|
||||
{% include "icons/youtube.svg" %}
|
||||
{% elif link.subtype == LinkSubtype.PDF %}
|
||||
{% include "icons/pdf.svg" %}
|
||||
{% elif link.type == LinkType.AUDIO %}
|
||||
{% include "icons/notes.svg" %}
|
||||
{% else %}
|
||||
{% include "icons/link.svg" %}
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
<li id="tema-link-{{ link.id }}"
|
||||
class="flex flex-row items-start">
|
||||
{% include "fragments/tema/link_icon.html" %}
|
||||
<div class="my-2">
|
||||
<p class="text-sm text-beige">Partitura</p>
|
||||
<p>Hola</p>
|
||||
<p class="text-sm text-beige">
|
||||
{{ link.content_type.value.capitalize() }}
|
||||
</p>
|
||||
{% if link.title %}
|
||||
<p>{{ link.title }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if logged_in %}
|
||||
<div class="grow"></div>
|
||||
<div class="m-2 text-sm text-beige">
|
||||
<button title="Modifica l'enllaç"
|
||||
hx-get="/api/tema/{{ 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ç"
|
||||
hx-delete="/api/tema/{{ tema.id }}/link/{{ link.id }}"
|
||||
hx-target="#tema-link-{{ link.id }}"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</li>
|
||||
|
||||
16
folkugat_web/assets/templates/fragments/tema/link_icon.html
Normal file
16
folkugat_web/assets/templates/fragments/tema/link_icon.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<div id="link-icon-{{ link.id }}"
|
||||
class="p-2 m-2 text-beige border border-beige rounded-md">
|
||||
<a href="{{ link.url }}" target="_blank">
|
||||
{% if link.link_type == LinkType.SPOTIFY %}
|
||||
{% include "icons/spotify.svg" %}
|
||||
{% elif link.link_type == LinkType.YOUTUBE %}
|
||||
{% include "icons/youtube.svg" %}
|
||||
{% elif link.link_type == LinkType.PDF %}
|
||||
{% include "icons/pdf.svg" %}
|
||||
{% elif link.content_type == ContentType.AUDIO %}
|
||||
{% include "icons/notes.svg" %}
|
||||
{% else %}
|
||||
{% include "icons/link.svg" %}
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
@@ -4,13 +4,12 @@
|
||||
{% endif %}
|
||||
|
||||
{% if tema.links %}
|
||||
{% for link in tema.links %}
|
||||
<ul class="flex flex-col justify-center"
|
||||
id="new-link-target">
|
||||
{% set link_idx = loop.index0 %}
|
||||
{% for link in tema.links %}
|
||||
{% include "fragments/tema/link.html" %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% if logged_in %}
|
||||
@@ -19,7 +18,7 @@
|
||||
class="text-sm text-beige text-right"
|
||||
hx-post="/api/tema/{{ tema.id }}/link"
|
||||
hx-target="#new-link-target"
|
||||
hx-swap="beforebegin">
|
||||
hx-swap="beforeend transition:true">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||
Afegeix un enllaç
|
||||
</button>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<div id="tema-lyric-{{ lyric_idx }}">
|
||||
<div id="tema-lyric-{{ lyric.id }}">
|
||||
<h5 class="text-sm text-beige text-right">
|
||||
{{ lyric.title }}
|
||||
{% if logged_in %}
|
||||
<button title="Modifica la lletra"
|
||||
class="mx-1"
|
||||
hx-get="/api/tema/{{ tema.id }}/editor/lyric/{{ lyric_idx }}"
|
||||
hx-target="#tema-lyric-{{ lyric_idx }}"
|
||||
hx-get="/api/tema/{{ 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_idx }}"
|
||||
hx-target="#tema-lyric-{{ lyric_idx }}"
|
||||
hx-delete="/api/tema/{{ tema.id }}/lyric/{{ lyric.id }}"
|
||||
hx-target="#tema-lyric-{{ lyric.id }}"
|
||||
hx-swap="outerHTML">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
{% if tema.lyrics %}
|
||||
{% for lyric in tema.lyrics %}
|
||||
{% set lyric_idx = loop.index0 %}
|
||||
{% include "fragments/tema/lyric.html" %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@@ -4,18 +4,6 @@
|
||||
{% include "fragments/tema/title.html" %}
|
||||
<div class="text-left">
|
||||
|
||||
<!-- {% if tema.scores() %} -->
|
||||
<!-- temes -->
|
||||
<!-- <h4 class="text-xl text-beige">Tema</h4> -->
|
||||
<!-- <hr class="h-px mt-1 mb-3 bg-beige border-0"> -->
|
||||
<!-- {% for file in tema.files %} -->
|
||||
<!-- {% if file.type == FileType.PDF %} -->
|
||||
<!-- {% set pdf_url = file.path %} -->
|
||||
<!-- {% include "fragments/pdf_viewer.html" %} -->
|
||||
<!-- {% endif %} -->
|
||||
<!-- {% endfor %} -->
|
||||
<!-- {% endif %} -->
|
||||
|
||||
{% include "fragments/tema/lyrics.html" %}
|
||||
{% include "fragments/tema/links.html" %}
|
||||
|
||||
|
||||
@@ -13,16 +13,16 @@
|
||||
{% for link in tema.links %}
|
||||
<li class="p-2 m-2 text-beige border border-beige rounded-md">
|
||||
<a href="{{ link.url }}" target="_blank">
|
||||
{% if link.type == LinkType.AUDIO %}
|
||||
{% if link.subtype == LinkSubtype.SPOTIFY %}
|
||||
{% if link.content_type == ContentType.AUDIO %}
|
||||
{% if link.link_type == LinkType.SPOTIFY %}
|
||||
{% include "icons/spotify.svg" %}
|
||||
{% elif link.subtype == LinkSubtype.YOUTUBE %}
|
||||
{% elif link.link_type == LinkType.YOUTUBE %}
|
||||
{% include "icons/youtube.svg" %}
|
||||
{% else %}
|
||||
{% include "icons/notes.svg" %}
|
||||
{% endif %}
|
||||
{% elif link.type == LinkType.SCORE %}
|
||||
{% if link.subtype == LinkSubtype.PDF %}
|
||||
{% elif link.content_type == ContentType.PARTITURA %}
|
||||
{% if link.link_type == LinkType.PDF %}
|
||||
{% include "icons/pdf.svg" %}
|
||||
{% else %}
|
||||
{% include "icons/link.svg" %}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import datetime
|
||||
import json
|
||||
|
||||
from folkugat_web.model import IndexedList
|
||||
from folkugat_web.model import temes as model
|
||||
|
||||
|
||||
@@ -27,9 +28,9 @@ def row_to_tema(row: tuple) -> model.Tema:
|
||||
return model.Tema(
|
||||
id=row[0],
|
||||
title=row[1],
|
||||
properties=list(map(model.Property.from_dict, json.loads(row[2]))),
|
||||
links=list(map(model.Link.from_dict, json.loads(row[3]))),
|
||||
lyrics=list(map(model.Lyrics.from_dict, json.loads(row[4]))),
|
||||
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]),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from folkugat_web.model import IndexedList
|
||||
from folkugat_web.model import temes as model
|
||||
|
||||
TEMES = [
|
||||
@@ -9,26 +10,29 @@ TEMES = [
|
||||
# ---
|
||||
model.Tema(
|
||||
title="Pasdoble de Muntanya (Cançó amb el nom molt llarg)",
|
||||
links=[
|
||||
links=IndexedList([
|
||||
model.Link(
|
||||
type=model.LinkType.AUDIO,
|
||||
subtype=model.LinkSubtype.SPOTIFY,
|
||||
id=0,
|
||||
content_type=model.ContentType.AUDIO,
|
||||
link_type=model.LinkType.SPOTIFY,
|
||||
url="https://open.spotify.com/track/4j9Krf19c5USmMvVUCoeWa?si=3023d1d83f814886",
|
||||
title="Versió de l'Orquestrina Trama",
|
||||
),
|
||||
],
|
||||
]),
|
||||
hidden=False,
|
||||
).with_ngrams(),
|
||||
# ---
|
||||
model.Tema(
|
||||
title="Astrid Waltz",
|
||||
alternatives=["vals"],
|
||||
links=[
|
||||
links=IndexedList([
|
||||
model.Link(
|
||||
type=model.LinkType.OTHER,
|
||||
subtype=None,
|
||||
id=0,
|
||||
content_type=model.ContentType.OTHER,
|
||||
link_type=None,
|
||||
url="https://marc.sastre.cat/folkugat",
|
||||
)
|
||||
],
|
||||
]),
|
||||
hidden=False,
|
||||
).with_ngrams(),
|
||||
# ---
|
||||
@@ -45,13 +49,14 @@ TEMES = [
|
||||
# ---
|
||||
model.Tema(
|
||||
title="El Gitano",
|
||||
links=[
|
||||
links=IndexedList([
|
||||
model.Link(
|
||||
type=model.LinkType.SCORE,
|
||||
subtype=model.LinkSubtype.PDF,
|
||||
id=0,
|
||||
content_type=model.ContentType.PARTITURA,
|
||||
link_type=model.LinkType.PDF,
|
||||
url="/db/temes/1/tema.pdf",
|
||||
)
|
||||
],
|
||||
]),
|
||||
hidden=False,
|
||||
).with_ngrams(),
|
||||
# ---
|
||||
@@ -68,8 +73,9 @@ TEMES = [
|
||||
# ---
|
||||
model.Tema(
|
||||
title="Malaguenya de Barxeta",
|
||||
lyrics=[
|
||||
lyrics=IndexedList([
|
||||
model.Lyrics(
|
||||
id=0,
|
||||
title="Malaguenya de Barxeta",
|
||||
content="""
|
||||
Mira si he corregut terres
|
||||
@@ -93,13 +99,14 @@ d'allà on renaix de les cendres
|
||||
el meu País Valencià.
|
||||
""".strip(),
|
||||
),
|
||||
],
|
||||
properties=[
|
||||
]),
|
||||
properties=IndexedList([
|
||||
model.Property(
|
||||
model.PropertyField.AUTOR,
|
||||
"Pep Jimeno 'Botifarra'"
|
||||
id=0,
|
||||
field=model.PropertyField.AUTOR,
|
||||
value="Pep Jimeno 'Botifarra'"
|
||||
)
|
||||
],
|
||||
]),
|
||||
hidden=False,
|
||||
).with_ngrams(),
|
||||
]
|
||||
|
||||
@@ -25,11 +25,10 @@ def footer(request, value, logged_in):
|
||||
|
||||
|
||||
def nota(request):
|
||||
response = templates.TemplateResponse(
|
||||
return templates.TemplateResponse(
|
||||
"fragments/nota/nota.html",
|
||||
{
|
||||
"request": request,
|
||||
}
|
||||
},
|
||||
headers={"HX-Refresh": "true"},
|
||||
)
|
||||
response.headers["HX-Refresh"] = "true"
|
||||
return response
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Optional
|
||||
from fastapi import Request
|
||||
from folkugat_web.model import temes as model
|
||||
from folkugat_web.services.temes import query as temes_q
|
||||
from folkugat_web.services.temes.links import guess_link_type
|
||||
from folkugat_web.templates import templates
|
||||
|
||||
|
||||
@@ -33,13 +34,11 @@ def title_editor(request: Request, logged_in: bool, tema_id: int):
|
||||
)
|
||||
|
||||
|
||||
def lyric(request: Request, logged_in: bool, tema_id: int, lyric_idx: int):
|
||||
def lyric(request: Request, logged_in: bool, tema_id: int, lyric_id: int):
|
||||
tema = temes_q.get_tema_by_id(tema_id)
|
||||
if tema is None:
|
||||
raise ValueError(f"No tune exists for tema_id: {tema_id}")
|
||||
if len(tema.lyrics) < lyric_idx:
|
||||
raise ValueError(f'Lyric index out of bounds')
|
||||
lyric = tema.lyrics[lyric_idx]
|
||||
lyric = tema.lyrics.get(lyric_id)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"fragments/tema/lyric.html",
|
||||
@@ -47,19 +46,17 @@ def lyric(request: Request, logged_in: bool, tema_id: int, lyric_idx: int):
|
||||
"request": request,
|
||||
"logged_in": logged_in,
|
||||
"tema": tema,
|
||||
"lyric_idx": lyric_idx,
|
||||
"lyric_id": lyric_id,
|
||||
"lyric": lyric,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def lyric_editor(request: Request, logged_in: bool, tema_id: int, lyric_idx: int):
|
||||
def lyric_editor(request: Request, logged_in: bool, tema_id: int, lyric_id: int):
|
||||
tema = temes_q.get_tema_by_id(tema_id)
|
||||
if tema is None:
|
||||
raise ValueError(f"No tune exists for tema_id: {tema_id}")
|
||||
if len(tema.lyrics) < lyric_idx:
|
||||
raise ValueError(f'Lyric index out of bounds')
|
||||
lyric = tema.lyrics[lyric_idx]
|
||||
lyric = tema.lyrics.get(lyric_id)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"fragments/tema/editor/lyric.html",
|
||||
@@ -67,7 +64,75 @@ def lyric_editor(request: Request, logged_in: bool, tema_id: int, lyric_idx: int
|
||||
"request": request,
|
||||
"logged_in": logged_in,
|
||||
"tema": tema,
|
||||
"lyric_idx": lyric_idx,
|
||||
"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 tema is None:
|
||||
raise ValueError(f"No tune exists for tema_id: {tema_id}")
|
||||
link = tema.links.get(link_id)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"fragments/tema/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(request: Request, logged_in: bool, tema_id: int, link_id: int):
|
||||
tema = temes_q.get_tema_by_id(tema_id)
|
||||
if tema is None:
|
||||
raise ValueError(f"No tune exists for tema_id: {tema_id}")
|
||||
link = tema.links.get(link_id)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"fragments/tema/editor/link.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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -25,8 +25,8 @@ def temes_busca_result(request: Request, tema: model.Tema, logged_in: bool):
|
||||
"request": request,
|
||||
"logged_in": logged_in,
|
||||
"tema": tema,
|
||||
"LinkSubtype": model.LinkSubtype,
|
||||
"LinkType": model.LinkType,
|
||||
"ContentType": model.ContentType,
|
||||
}
|
||||
).body.decode('utf-8')
|
||||
|
||||
@@ -47,8 +47,8 @@ def tema(request: Request, tema_id: int, logged_in: bool):
|
||||
"request": request,
|
||||
"logged_in": logged_in,
|
||||
"Pages": Pages,
|
||||
"LinkSubtype": model.LinkSubtype,
|
||||
"LinkType": model.LinkType,
|
||||
"ContentType": model.ContentType,
|
||||
"tema": tema,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from ._base import IndexedList
|
||||
|
||||
36
folkugat_web/model/_base.py
Normal file
36
folkugat_web/model/_base.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import dataclasses
|
||||
from typing import Optional, TypeVar
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class WithId:
|
||||
id: Optional[int]
|
||||
|
||||
|
||||
T = TypeVar("T", bound=WithId)
|
||||
|
||||
|
||||
class IndexedList(list[T]):
|
||||
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)
|
||||
@@ -5,16 +5,18 @@ from typing import Optional
|
||||
|
||||
from folkugat_web.services import ngrams
|
||||
|
||||
from ._base import IndexedList, WithId
|
||||
|
||||
NGrams = dict[int, list[str]]
|
||||
|
||||
|
||||
class ContentType(enum.Enum):
|
||||
PARTITURA = "partitura"
|
||||
AUDIO = "àudio"
|
||||
OTHER = "enllaç"
|
||||
|
||||
|
||||
class LinkType(enum.Enum):
|
||||
SCORE = "score"
|
||||
AUDIO = "audio"
|
||||
OTHER = "other"
|
||||
|
||||
|
||||
class LinkSubtype(enum.Enum):
|
||||
# Score
|
||||
PDF = "pdf"
|
||||
IMAGE = "image"
|
||||
@@ -24,16 +26,17 @@ class LinkSubtype(enum.Enum):
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Link:
|
||||
type: LinkType
|
||||
subtype: Optional[LinkSubtype]
|
||||
class Link(WithId):
|
||||
content_type: ContentType
|
||||
link_type: Optional[LinkType]
|
||||
url: str
|
||||
title: str = ""
|
||||
|
||||
def to_dict(self):
|
||||
return dict(
|
||||
type=self.type.value,
|
||||
subtype=self.subtype.value if self.subtype else None,
|
||||
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,
|
||||
)
|
||||
@@ -41,8 +44,9 @@ class Link:
|
||||
@classmethod
|
||||
def from_dict(cls, d):
|
||||
return cls(
|
||||
type=LinkType(d["type"]),
|
||||
subtype=LinkSubtype(d["subtype"]) if d["subtype"] else None,
|
||||
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"],
|
||||
)
|
||||
@@ -56,12 +60,13 @@ class PropertyField(enum.Enum):
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Property:
|
||||
class Property(WithId):
|
||||
field: PropertyField
|
||||
value: str
|
||||
|
||||
def to_dict(self):
|
||||
return dict(
|
||||
id=self.id,
|
||||
field=self.field.value,
|
||||
value=self.value,
|
||||
)
|
||||
@@ -69,18 +74,20 @@ class Property:
|
||||
@classmethod
|
||||
def from_dict(cls, d):
|
||||
return cls(
|
||||
id=d["id"],
|
||||
field=PropertyField(d["field"]),
|
||||
value=d["value"],
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Lyrics:
|
||||
class Lyrics(WithId):
|
||||
title: str
|
||||
content: str
|
||||
|
||||
def to_dict(self):
|
||||
return dict(
|
||||
id=self.id,
|
||||
title=self.title,
|
||||
content=self.content,
|
||||
)
|
||||
@@ -88,6 +95,7 @@ class Lyrics:
|
||||
@classmethod
|
||||
def from_dict(cls, d):
|
||||
return cls(
|
||||
id=d["id"],
|
||||
title=d["title"],
|
||||
content=d["content"],
|
||||
)
|
||||
@@ -98,9 +106,9 @@ class Tema:
|
||||
id: Optional[int] = None
|
||||
# Info
|
||||
title: str = ""
|
||||
properties: list[Property] = dataclasses.field(default_factory=list)
|
||||
links: list[Link] = dataclasses.field(default_factory=list)
|
||||
lyrics: list[Lyrics] = dataclasses.field(default_factory=list)
|
||||
properties: IndexedList[Property] = dataclasses.field(default_factory=IndexedList)
|
||||
links: IndexedList[Link] = dataclasses.field(default_factory=IndexedList)
|
||||
lyrics: IndexedList[Lyrics] = dataclasses.field(default_factory=IndexedList)
|
||||
# Search related
|
||||
alternatives: list[str] = dataclasses.field(default_factory=list)
|
||||
ngrams: NGrams = dataclasses.field(default_factory=dict)
|
||||
@@ -115,9 +123,3 @@ class Tema:
|
||||
def with_ngrams(self):
|
||||
self.compute_ngrams()
|
||||
return self
|
||||
|
||||
def scores(self):
|
||||
return [link for link in self.links if link.type is LinkType.SCORE]
|
||||
|
||||
def audios(self):
|
||||
return [link for link in self.links if link.type is LinkType.AUDIO]
|
||||
|
||||
32
folkugat_web/services/temes/links.py
Normal file
32
folkugat_web/services/temes/links.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
from folkugat_web.model import temes as model
|
||||
|
||||
IMAGE_FORMATS_RE = "|".join([
|
||||
'jpg', 'jpeg', 'png'
|
||||
])
|
||||
|
||||
LINK_RES = {
|
||||
model.LinkType.IMAGE: [
|
||||
re.compile(rf"^.*\.({IMAGE_FORMATS_RE})$")
|
||||
],
|
||||
model.LinkType.PDF: [
|
||||
re.compile(r"^.*\.pdf$")
|
||||
],
|
||||
model.LinkType.SPOTIFY: [
|
||||
re.compile(r"^.*spotify\.com.*$")
|
||||
],
|
||||
model.LinkType.YOUTUBE: [
|
||||
re.compile(r"^.*youtube\.com.*$"),
|
||||
re.compile(r"^.*youtu\.be.*$"),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def guess_link_type(url: str) -> Optional[model.LinkType]:
|
||||
for link_type, regexes in LINK_RES.items():
|
||||
for regex in regexes:
|
||||
if regex.match(url):
|
||||
return link_type
|
||||
return None
|
||||
@@ -20,29 +20,25 @@ def update_title(tema_id: int, title: str) -> model.Tema:
|
||||
return new_tema
|
||||
|
||||
|
||||
def update_lyric(tema_id: int, lyric_idx: int, lyric: model.Lyrics) -> model.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}!")
|
||||
if lyric_idx > len(tema.lyrics):
|
||||
raise ValueError(f'Lyric index out of bounds')
|
||||
|
||||
tema.lyrics[lyric_idx] = lyric
|
||||
tema.lyrics.replace(lyric_id, lyric)
|
||||
temes_w.update_tema(tema=tema, con=con)
|
||||
|
||||
return tema
|
||||
|
||||
|
||||
def delete_lyric(tema_id: int, lyric_idx: int) -> model.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}!")
|
||||
if lyric_idx > len(tema.lyrics):
|
||||
raise ValueError(f'Lyric index out of bounds')
|
||||
|
||||
del tema.lyrics[lyric_idx]
|
||||
tema.lyrics.delete(lyric_id)
|
||||
temes_w.update_tema(tema=tema, con=con)
|
||||
|
||||
return tema
|
||||
@@ -54,9 +50,51 @@ def add_lyric(tema_id: int) -> model.Tema:
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user