Compare commits

...

2 Commits

Author SHA1 Message Date
marc
23337f8ab3 Afegir cartells a les jams (i esquelet per a slow jams i notes) 2025-11-01 12:47:04 +01:00
marc
fbfedf4dac Fix set editor 2025-10-30 22:03:01 +01:00
28 changed files with 1657 additions and 213 deletions

View File

@@ -1,10 +1,12 @@
from folkugat_web.api.router import get_router from folkugat_web.api.router import get_router
from . import index, live, set_page from . import cartell, index, live, playlist, set_page
router = get_router() router = get_router()
router.include_router(cartell.router)
router.include_router(index.router) router.include_router(index.router)
router.include_router(live.router) router.include_router(live.router)
router.include_router(set_page.router) router.include_router(set_page.router)
router.include_router(playlist.router)
__all__ = ["router"] __all__ = ["router"]

View File

@@ -0,0 +1,71 @@
import dataclasses
from typing import Annotated
from fastapi import HTTPException, Request, UploadFile
from fastapi.params import File
from folkugat_web.api.router import get_router
from folkugat_web.fragments.sessio import cartell
from folkugat_web.services import auth
from folkugat_web.services import files as files_service
from folkugat_web.services import sessions as sessions_service
router = get_router()
@router.get("/api/sessio/{session_id}/cartell")
def get_cartell(
request: Request,
logged_in: auth.LoggedIn,
session_id: int,
):
return cartell.cartell(request, session_id, logged_in)
@router.get("/api/sessio/{session_id}/cartell/editor")
def get_cartell_editor(
request: Request,
_: auth.RequireLogin,
session_id: int,
):
return cartell.cartell_editor(request, session_id)
@router.put("/api/sessio/{session_id}/cartell")
async def set_cartell(
request: Request,
logged_in: auth.RequireLogin,
session_id: int,
upload_file: Annotated[UploadFile, File()],
):
session = sessions_service.get_session(session_id=session_id)
if not session:
raise HTTPException(status_code=404, detail="Could not find session")
url = await files_service.store_session_cartell(
session_id=session_id,
upload_file=upload_file,
)
new_session = dataclasses.replace(
session,
cartell_url=url,
)
sessions_service.set_session(new_session)
return cartell.cartell(request, session_id, logged_in)
@router.delete("/api/sessio/{session_id}/cartell")
async def delete_cartell(
request: Request,
logged_in: auth.RequireLogin,
session_id: int,
):
session = sessions_service.get_session(session_id=session_id)
if not session:
raise HTTPException(status_code=404, detail="Could not find session")
if session.cartell_url:
new_session = dataclasses.replace(
session,
cartell_url=None,
)
sessions_service.set_session(new_session)
files_service.clean_orphan_files()
return cartell.cartell(request, session_id, logged_in)

View File

@@ -2,7 +2,8 @@ from typing import Annotated
from fastapi import Form, Request from fastapi import Form, Request
from folkugat_web.api.router import get_router from folkugat_web.api.router import get_router
from folkugat_web.fragments import live, sessio from folkugat_web.fragments import live
from folkugat_web.fragments.sessio import page as sessio
from folkugat_web.services import auth from folkugat_web.services import auth
from folkugat_web.services.temes import write as temes_service from folkugat_web.services.temes import write as temes_service
from folkugat_web.templates import templates from folkugat_web.templates import templates
@@ -52,150 +53,3 @@ def stop_live(
session_id: int, session_id: int,
): ):
return live.stop_live_session(request=request, session_id=session_id) return live.stop_live_session(request=request, session_id=session_id)
@router.post("/api/sessio/{session_id}/set")
def add_set(
request: Request,
logged_in: auth.RequireLogin,
session_id: int,
):
return sessio.add_set(request=request, session_id=session_id, logged_in=logged_in)
@router.get("/api/sessio/{session_id}/set/{set_id}")
def get_set(
request: Request,
logged_in: auth.LoggedIn,
session_id: int,
set_id: int,
):
return sessio.get_set(request=request, session_id=session_id, set_id=set_id, logged_in=logged_in)
@router.delete("/api/sessio/{session_id}/set/{set_id}")
def delete_set(
_: auth.RequireLogin,
session_id: int,
set_id: int,
):
return sessio.delete_set(session_id=session_id, set_id=set_id)
@router.post("/api/sessio/{session_id}/set/{set_id}")
def add_tema(
request: Request,
logged_in: auth.RequireLogin,
session_id: int,
set_id: int,
):
return sessio.add_tema(request=request, session_id=session_id, set_id=set_id, logged_in=logged_in)
@router.get("/api/sessio/{session_id}/set/{set_id}/tema/{entry_id}")
def get_tema(
request: Request,
logged_in: auth.RequireLogin,
session_id: int,
set_id: int,
entry_id: int,
):
return sessio.get_tema(
request=request, session_id=session_id, set_id=set_id, entry_id=entry_id, logged_in=logged_in)
@router.get("/api/sessio/{session_id}/set/{set_id}/tema/{entry_id}/editor")
def get_tema_editor(
request: Request,
logged_in: auth.RequireLogin,
session_id: int,
set_id: int,
entry_id: int,
):
return sessio.get_tema_editor(
request=request, session_id=session_id, set_id=set_id, entry_id=entry_id, logged_in=logged_in)
@router.delete("/api/sessio/{session_id}/set/{set_id}/tema/{entry_id}")
def delete_tema(
_: auth.RequireLogin,
session_id: int,
set_id: int,
entry_id: int,
):
return sessio.delete_tema(session_id=session_id, set_id=set_id, entry_id=entry_id)
@router.get("/api/sessio/{session_id}/set/{set_id}/tema/{entry_id}/busca")
def busca_tema(
request: Request,
_: auth.RequireLogin,
session_id: int,
set_id: int,
entry_id: int,
query: str,
):
return sessio.busca_tema(
request=request,
session_id=session_id,
set_id=set_id,
entry_id=entry_id,
query=query,
)
@router.put("/api/sessio/{session_id}/set/{set_id}/tema/{entry_id}")
def set_tema(
request: Request,
logged_in: auth.RequireLogin,
session_id: int,
set_id: int,
entry_id: int,
tema_id: Annotated[int, Form()],
):
return sessio.set_tema(
request=request,
logged_in=logged_in,
session_id=session_id,
set_id=set_id,
entry_id=entry_id,
tema_id=tema_id,
)
@router.put("/api/sessio/{session_id}/set/{set_id}/tema/{entry_id}/unknown")
def set_tema_unknown(
request: Request,
logged_in: auth.RequireLogin,
session_id: int,
set_id: int,
entry_id: int,
):
return sessio.set_tema(
request=request,
logged_in=logged_in,
session_id=session_id,
set_id=set_id,
entry_id=entry_id,
tema_id=None,
)
@router.post("/api/sessio/{session_id}/set/{set_id}/tema/{entry_id}")
def set_tema_new(
request: Request,
logged_in: auth.RequireLogin,
session_id: int,
set_id: int,
entry_id: int,
title: Annotated[str, Form()],
):
new_tema = temes_service.create_tema(title=title)
return sessio.set_tema(
request=request,
logged_in=logged_in,
session_id=session_id,
set_id=set_id,
entry_id=entry_id,
tema_id=new_tema.id,
)

View File

@@ -0,0 +1,189 @@
from typing import Annotated
from fastapi import Form, Request
from folkugat_web.api.router import get_router
from folkugat_web.fragments import live
from folkugat_web.fragments.sessio import playlist
from folkugat_web.services import auth
from folkugat_web.services.temes import write as temes_service
from folkugat_web.templates import templates
router = get_router()
@router.post("/api/sessio/{session_id}/playlist/set")
def add_set(
request: Request,
logged_in: auth.RequireLogin,
session_id: int,
):
return playlist.add_set(
request=request,
session_id=session_id,
logged_in=logged_in,
)
@router.get("/api/sessio/{session_id}/playlist/set/{set_id}")
def get_set(
request: Request,
logged_in: auth.LoggedIn,
session_id: int,
set_id: int,
):
return playlist.get_set(
request=request,
session_id=session_id,
set_id=set_id,
logged_in=logged_in,
)
@router.delete("/api/sessio/{session_id}/playlist/set/{set_id}")
def delete_set(
_: auth.RequireLogin,
session_id: int,
set_id: int,
):
return playlist.delete_set(
session_id=session_id,
set_id=set_id,
)
@router.post("/api/sessio/{session_id}/playlist/set/{set_id}")
def add_tema(
request: Request,
logged_in: auth.RequireLogin,
session_id: int,
set_id: int,
):
return playlist.add_tema(
request=request,
session_id=session_id,
set_id=set_id,
logged_in=logged_in,
)
@router.get("/api/sessio/{session_id}/playlist/set/{set_id}/tema/{entry_id}")
def get_tema(
request: Request,
logged_in: auth.RequireLogin,
session_id: int,
set_id: int,
entry_id: int,
):
return playlist.get_tema(
request=request,
session_id=session_id,
set_id=set_id,
entry_id=entry_id,
logged_in=logged_in,
)
@router.get("/api/sessio/{session_id}/playlist/set/{set_id}/tema/{entry_id}/editor")
def get_tema_editor(
request: Request,
logged_in: auth.RequireLogin,
session_id: int,
set_id: int,
entry_id: int,
):
return playlist.get_tema_editor(
request=request,
session_id=session_id,
set_id=set_id,
entry_id=entry_id,
logged_in=logged_in,
)
@router.delete("/api/sessio/{session_id}/playlist/set/{set_id}/tema/{entry_id}")
def delete_tema(
_: auth.RequireLogin,
session_id: int,
set_id: int,
entry_id: int,
):
return playlist.delete_tema(
session_id=session_id,
set_id=set_id,
entry_id=entry_id,
)
@router.get("/api/sessio/{session_id}/playlist/set/{set_id}/tema/{entry_id}/busca")
def busca_tema(
request: Request,
_: auth.RequireLogin,
session_id: int,
set_id: int,
entry_id: int,
query: str,
):
return playlist.busca_tema(
request=request,
session_id=session_id,
set_id=set_id,
entry_id=entry_id,
query=query,
)
@router.put("/api/sessio/{session_id}/playlist/set/{set_id}/tema/{entry_id}")
def set_tema(
request: Request,
logged_in: auth.RequireLogin,
session_id: int,
set_id: int,
entry_id: int,
tema_id: Annotated[int, Form()],
):
return playlist.set_tema(
request=request,
logged_in=logged_in,
session_id=session_id,
set_id=set_id,
entry_id=entry_id,
tema_id=tema_id,
)
@router.put("/api/sessio/{session_id}/playlist/set/{set_id}/tema/{entry_id}/unknown")
def set_tema_unknown(
request: Request,
logged_in: auth.RequireLogin,
session_id: int,
set_id: int,
entry_id: int,
):
return playlist.set_tema(
request=request,
logged_in=logged_in,
session_id=session_id,
set_id=set_id,
entry_id=entry_id,
tema_id=None,
)
@router.post("/api/sessio/{session_id}/playlist/set/{set_id}/tema/{entry_id}")
def set_tema_new(
request: Request,
logged_in: auth.RequireLogin,
session_id: int,
set_id: int,
entry_id: int,
title: Annotated[str, Form()],
):
new_tema = temes_service.create_tema(title=title)
return playlist.set_tema(
request=request,
logged_in=logged_in,
session_id=session_id,
set_id=set_id,
entry_id=entry_id,
tema_id=new_tema.id,
)

View File

@@ -34,7 +34,7 @@ async def set_link(
upload_file: Annotated[UploadFile | None, File()] = None, upload_file: Annotated[UploadFile | None, File()] = None,
): ):
if upload_file: if upload_file:
url = await files_service.store_file(tema_id=tema_id, upload_file=upload_file) url = await files_service.store_tema_file(tema_id=tema_id, upload_file=upload_file)
link_type = links_service.guess_link_type(url or '') link_type = links_service.guess_link_type(url or '')
new_link = model.Link( new_link = model.Link(
@@ -65,7 +65,7 @@ def create_link(
@router.delete("/api/tema/{tema_id}/link/{link_id}") @router.delete("/api/tema/{tema_id}/link/{link_id}")
def delete_link( def delete_link(
_logged_in: auth.RequireLogin, _: auth.RequireLogin,
tema_id: int, tema_id: int,
link_id: int, link_id: int,
): ):

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,36 @@
<div id="cartell"
class="flex flex-col items-center">
{% if session.cartell_url %}
<div class="max-w-[655px] w-full
flex flex-col items-center">
{% if logged_in %}
<div class="my-2 w-full text-beige text-right">
<button title="Modifica el cartell"
class="mx-1"
hx-get="/api/sessio/{{ session_id }}/cartell/editor"
hx-target="#cartell"
hx-swap="outerHTML">
<i class="fa fa-pencil" aria-hidden="true"></i>
</button>
<button title="Esborra el cartell"
class="mx-1"
hx-delete="/api/sessio/{{ session_id }}/cartell"
hx-target="#cartell"
hx-swap="outerHTML">
<i class="fa fa-times" aria-hidden="true"></i>
</button>
</div>
{% endif %}
<img class="w-full h-auto"
src="{{ session.cartell_url }}"/>
</div>
{% elif logged_in %}
<button class="text-beige mt-2"
hx-get="/api/sessio/{{ session_id }}/cartell/editor"
hx-target="#cartell"
hx-swap="outerHTML">
<i class="fa fa-plus" aria-hidden="true"></i>
Afegeix cartell
</button>
{% endif %}
</div>

View File

@@ -0,0 +1,23 @@
<form id="cartell-editor"
class="flex flex-row gap-2"
hx-encoding="multipart/form-data">
<input type='file'
class="border border-beige focus:outline-none
rounded grow
bg-brown p-1 my-1"
name='upload_file'/>
<button title="Desa els canvis"
class="mx-1 text-beige"
hx-put="/api/sessio/{{ session_id }}/cartell"
hx-target="#cartell-editor"
hx-swap="outerHTML">
<i class="fa fa-check" aria-hidden="true"></i>
</button>
<button title="Descarta els canvis"
class="mx-1 text-beige"
hx-get="/api/sessio/{{ session_id }}/cartell"
hx-target="#cartell-editor"
hx-swap="outerHTML">
<i class="fa fa-times" aria-hidden="true"></i>
</button>
</form>

View File

@@ -12,6 +12,7 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
</h3> </h3>
{% include "fragments/sessio/cartell.html" %}
<div class="text-left"> <div class="text-left">
<h4 class="pt-4 text-xl text-beige">Horari i lloc</h4> <h4 class="pt-4 text-xl text-beige">Horari i lloc</h4>
De {{ session.start_time.strftime("%H:%M") }} De {{ session.start_time.strftime("%H:%M") }}
@@ -29,6 +30,17 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
{% if logged_in or session.notes %}
<div class="text-left">
<h4 class="py-4 text-xl text-beige">Notes</h4>
{% if session.notes %}{{ session.notes }}{% endif %}
</div>
{% endif %}
{% if logged_in or False %}
<div class="text-left">
<h4 class="py-4 text-xl text-beige">Slow Jam</h4>
</div>
{% endif %}
{% if logged_in or playlist.sets %} {% if logged_in or playlist.sets %}
<div class="text-left"> <div class="text-left">
<h4 class="py-4 text-xl text-beige mt-2">Temes tocats</h4> <h4 class="py-4 text-xl text-beige mt-2">Temes tocats</h4>

View File

@@ -7,7 +7,7 @@
{% if logged_in %} {% if logged_in %}
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<button class="text-beige mt-2" <button class="text-beige mt-2"
hx-post="/api/sessio/{{ session.id }}/set" hx-post="/api/sessio/{{ session.id }}/playlist/set"
hx-target="#playlist-{{ session.id }}" hx-target="#playlist-{{ session.id }}"
hx-swap="beforeend transition:true"> hx-swap="beforeend transition:true">
<i class="fa fa-plus" aria-hidden="true"></i> <i class="fa fa-plus" aria-hidden="true"></i>

View File

@@ -2,18 +2,22 @@
m-4 rounded-lg bg-white m-4 rounded-lg bg-white
px-2 py-1 my-1" px-2 py-1 my-1"
id="set-entry-{{ set_id }}" id="set-entry-{{ set_id }}"
hx-get="/api/sessio/{{ session_id }}/set/{{ set_id }}" hx-get="/api/sessio/{{ session_id }}/playlist/set/{{ set_id }}"
hx-target="#set-entry-{{ set_id }}" hx-target="#set-entry-{{ set_id }}"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-trigger="reload-set-{{ set_id }}"> hx-trigger="reload-set-{{ set_id }}">
{% if set_entry.temes | length > 1 or not set_entry.temes[0] %} {% if set_entry.temes | length > 1 or not set_entry.temes[0].tema %}
{% set set_url = "/sessio/%d/set/%d" | format(session_id, set_id) %} {% set set_url = "/sessio/%d/set/%d" | format(session_id, set_id) %}
{% else %} {% else %}
{% set set_url = "/tema/%d" | format(set_entry.temes[0].tema_id) %} {% set set_url = "/tema/%d" | format(set_entry.temes[0].tema_id) %}
{% endif %} {% endif %}
{% if logged_in %}
<div class="flex flex-row grow items-center">
{% else %}
<a href="{{ set_url }}" <a href="{{ set_url }}"
class="flex flex-row grow items-center cursor-pointer" class="flex flex-row grow items-center cursor-pointer"
title="Mostra els temes"> title="Mostra els temes">
{% endif %}
<div class="flex-1"></div> <div class="flex-1"></div>
<div class="flex flex-col <div class="flex flex-col
items-start items-start
@@ -21,7 +25,7 @@
w-full max-w-[655px]"> w-full max-w-[655px]">
{% if logged_in %} {% if logged_in %}
<button class="text-beige w-full" <button class="text-beige w-full"
hx-delete="/api/sessio/{{ session_id }}/set/{{ set_id }}" hx-delete="/api/sessio/{{ session_id }}/playlist/set/{{ set_id }}"
hx-target="#set-entry-{{ set_id }}" hx-target="#set-entry-{{ set_id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">
<i class="fa fa-times" aria-hidden="true"></i> <i class="fa fa-times" aria-hidden="true"></i>
@@ -40,7 +44,7 @@
</ol> </ol>
{% if logged_in %} {% if logged_in %}
<button class="text-beige mt-2 w-full" <button class="text-beige mt-2 w-full"
hx-post="/api/sessio/{{ session_id }}/set/{{ set_id }}" hx-post="/api/sessio/{{ session_id }}/playlist/set/{{ set_id }}"
hx-target="#set-entry-{{ set_id }}-list" hx-target="#set-entry-{{ set_id }}-list"
hx-swap="beforeend transition:true"> hx-swap="beforeend transition:true">
<i class="fa fa-plus" aria-hidden="true"></i> <i class="fa fa-plus" aria-hidden="true"></i>
@@ -49,7 +53,9 @@
{% endif %} {% endif %}
</div> </div>
<div class="flex-1 text-beige"> <div class="flex-1 text-beige">
{% if logged_in %}<a href="{{ set_url }}" class="text-beige">{% endif %}
<i class="fa fa-chevron-right" aria-hidden="true"></i> <i class="fa fa-chevron-right" aria-hidden="true"></i>
{% if logged_in %}</a>{% endif %}
</div> </div>
</a> {% if logged_in %}</div>{% else %}</a>{% endif %}
</li> </li>

View File

@@ -12,13 +12,13 @@
class="border border-beige focus:outline-none class="border border-beige focus:outline-none
rounded text-center text-black rounded text-center text-black
p-1 m-1" p-1 m-1"
hx-get="/api/sessio/{{ session_id }}/set/{{ set_id }}/tema/{{ tema_entry.id }}/busca" hx-get="/api/sessio/{{ session_id }}/playlist/set/{{ set_id }}/tema/{{ tema_entry.id }}/busca"
hx-trigger="revealed, keyup delay:500ms changed" hx-trigger="revealed, keyup delay:500ms changed"
hx-target="#tune-entry-{{ tema_entry.id }}-search-results" hx-target="#tune-entry-{{ tema_entry.id }}-search-results"
hx-swap="outerHTML"/> hx-swap="outerHTML"/>
<button title="Descarta els canvis" <button title="Descarta els canvis"
class="text-beige mx-1" class="text-beige mx-1"
hx-get="/api/sessio/{{ session_id }}/set/{{ set_id }}/tema/{{ tema_entry.id }}" hx-get="/api/sessio/{{ session_id }}/playlist/set/{{ set_id }}/tema/{{ tema_entry.id }}"
hx-target="#tune-entry-{{ tema_entry.id }}" hx-target="#tune-entry-{{ tema_entry.id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">
<i class="fa fa-times" aria-hidden="true"></i> <i class="fa fa-times" aria-hidden="true"></i>

View File

@@ -15,14 +15,14 @@
<div class="flex-none flex flex-row shrink-0"> <div class="flex-none flex flex-row shrink-0">
<button title="Edita el tema" <button title="Edita el tema"
class="text-beige mx-1" class="text-beige mx-1"
hx-get="/api/sessio/{{ session_id }}/set/{{ set_id }}/tema/{{ tema_entry.id }}/editor" hx-get="/api/sessio/{{ session_id }}/playlist/set/{{ set_id }}/tema/{{ tema_entry.id }}/editor"
hx-target="#tune-entry-{{ tema_entry.id }}" hx-target="#tune-entry-{{ tema_entry.id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">
<i class="fa fa-pencil" aria-hidden="true"></i> <i class="fa fa-pencil" aria-hidden="true"></i>
</button> </button>
<button title="Esborra el tema" <button title="Esborra el tema"
class="text-beige mx-1" class="text-beige mx-1"
hx-delete="/api/sessio/{{ session_id }}/set/{{ set_id }}/tema/{{ tema_entry.id }}" hx-delete="/api/sessio/{{ session_id }}/playlist/set/{{ set_id }}/tema/{{ tema_entry.id }}"
hx-target="#tune-entry-{{ tema_entry.id }}" hx-target="#tune-entry-{{ tema_entry.id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">
<i class="fa fa-times" aria-hidden="true"></i> <i class="fa fa-times" aria-hidden="true"></i>

View File

@@ -4,7 +4,7 @@
<li> <li>
<button class="bg-beige text-brown rounded <button class="bg-beige text-brown rounded
m-1 px-2" m-1 px-2"
hx-put="/api/sessio/{{ session_id }}/set/{{ set_id }}/tema/{{ entry_id }}" hx-put="/api/sessio/{{ session_id }}/playlist/set/{{ set_id }}/tema/{{ entry_id }}"
hx-vals='{"tema_id": "{{ tema.id }}"}' hx-vals='{"tema_id": "{{ tema.id }}"}'
hx-target="#tune-entry-{{ entry_id }}" hx-target="#tune-entry-{{ entry_id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">
@@ -15,7 +15,7 @@
<li> <li>
<button class="border border-beige text-beige rounded <button class="border border-beige text-beige rounded
m-1 px-2" m-1 px-2"
hx-put="/api/sessio/{{ session_id }}/set/{{ set_id }}/tema/{{ entry_id }}/unknown" hx-put="/api/sessio/{{ session_id }}/playlist/set/{{ set_id }}/tema/{{ entry_id }}/unknown"
hx-target="#tune-entry-{{ entry_id }}" hx-target="#tune-entry-{{ entry_id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">
<i class="fa fa-question" aria-hidden="true"></i> <i class="fa fa-question" aria-hidden="true"></i>
@@ -25,7 +25,7 @@
<li> <li>
<button class="border border-beige text-beige rounded <button class="border border-beige text-beige rounded
m-1 px-2" m-1 px-2"
hx-post="/api/sessio/{{ session_id }}/set/{{ set_id }}/tema/{{ entry_id }}" hx-post="/api/sessio/{{ session_id }}/playlist/set/{{ set_id }}/tema/{{ entry_id }}"
hx-vals='{"title": "{{ query }}"}' hx-vals='{"title": "{{ query }}"}'
hx-target="#tune-entry-{{ entry_id }}" hx-target="#tune-entry-{{ entry_id }}"
hx-swap="outerHTML"> hx-swap="outerHTML">

View File

@@ -4,7 +4,7 @@
class="border border-beige focus:outline-none class="border border-beige focus:outline-none
rounded grow rounded grow
bg-brown p-1 my-1" bg-brown p-1 my-1"
name='upload_file'> name='upload_file'/>
<button title="Afegeix un enllaç" <button title="Afegeix un enllaç"
class="border border-beige rounded px-2 py-1 my-1" class="border border-beige rounded px-2 py-1 my-1"
hx-get="/api/tema/{{ link.tema_id }}/editor/link/{{ link.id }}/url" hx-get="/api/tema/{{ link.tema_id }}/editor/link/{{ link.id }}/url"

View File

@@ -12,6 +12,7 @@ logger.info(f"Using DB_DIR: {DB_DIR}")
DB_FILES_DIR = DB_DIR / "fitxer" DB_FILES_DIR = DB_DIR / "fitxer"
DB_FILES_TEMA_DIR = DB_FILES_DIR / "tema" DB_FILES_TEMA_DIR = DB_FILES_DIR / "tema"
DB_FILES_SESSION_DIR = DB_FILES_DIR / "sessio"
DB_FILES_SET_DIR = DB_FILES_DIR / "set" DB_FILES_SET_DIR = DB_FILES_DIR / "set"
DB_FILES_TMP_DIR = DB_FILES_DIR / "tmp" DB_FILES_TMP_DIR = DB_FILES_DIR / "tmp"

View File

@@ -59,13 +59,15 @@ def get_playlist_entries(
return map(conversion.row_to_playlist_entry, cur.fetchall()) return map(conversion.row_to_playlist_entry, cur.fetchall())
GetTuneSessionsRow = tuple[int, int, str, str, str, str | None, str | None, bool] GetTuneSessionsRow = tuple[int, int, str, str, str, str | None, str | None, str | None, str | None, bool]
def get_tune_sessions(tema_ids: list[int], con: Connection | None = None) -> dict[int, list[Session]]: def get_tune_sessions(tema_ids: list[int], con: Connection | None = None) -> dict[int, list[Session]]:
placeholders = ", ".join(["?" for _ in tema_ids]) placeholders = ", ".join(["?" for _ in tema_ids])
query = f""" query = f"""
SELECT p.tema_id, s.id, s.date, s.start_time, s.end_time, s.venue_name, s.venue_url, s.is_live SELECT
p.tema_id, s.id, s.date, s.start_time, s.end_time, s.venue_name,
s.venue_url, s.notes, s.cartell_url, s.is_live
FROM playlists p JOIN sessions s ON p.session_id = s.id FROM playlists p JOIN sessions s ON p.session_id = s.id
WHERE p.tema_id IN ({placeholders}) WHERE p.tema_id IN ({placeholders})
""" """

View File

@@ -3,7 +3,17 @@ from typing import TypedDict
from folkugat_web.model import sessions as model from folkugat_web.model import sessions as model
SessionRowTuple = tuple[int, str, str, str, str | None, str | None, bool] SessionRowTuple = tuple[
int, # id
str, # date
str, # start_time
str, # end_time
str | None, # venue_name
str | None, # venue_url
str | None, # notes
str | None, # cartell_url
bool, # is_live
]
class SessionRowDict(TypedDict): class SessionRowDict(TypedDict):
@@ -13,6 +23,8 @@ class SessionRowDict(TypedDict):
end_time: str end_time: str
venue_name: str | None venue_name: str | None
venue_url: str | None venue_url: str | None
notes: str | None
cartell_url: str | None
is_live: bool is_live: bool
@@ -24,6 +36,8 @@ def session_to_row(sessio: model.Session) -> SessionRowDict:
'end_time': sessio.end_time.isoformat(), 'end_time': sessio.end_time.isoformat(),
'venue_name': sessio.venue.name, 'venue_name': sessio.venue.name,
'venue_url': sessio.venue.url, 'venue_url': sessio.venue.url,
'notes': sessio.notes,
'cartell_url': sessio.cartell_url,
'is_live': sessio.is_live, 'is_live': sessio.is_live,
} }
@@ -38,5 +52,7 @@ def row_to_session(row: SessionRowTuple) -> model.Session:
name=row[4], name=row[4],
url=row[5], url=row[5],
), ),
is_live=row[6], notes=row[6],
cartell_url=row[7],
is_live=row[8],
) )

View File

@@ -15,6 +15,8 @@ def create_sessions_table(con: Connection):
end_time TEXT NOT NULL, end_time TEXT NOT NULL,
venue_name TEXT, venue_name TEXT,
venue_url TEXT, venue_url TEXT,
notes TEXT,
cartell_url TEXT,
is_live BOOLEAN DEFAULT false is_live BOOLEAN DEFAULT false
) )
""" """

View File

@@ -82,7 +82,7 @@ def get_sessions(session_id: int | None = None,
clauses_str = " ".join(clauses) clauses_str = " ".join(clauses)
query = f""" query = f"""
SELECT id, date, start_time, end_time, venue_name, venue_url, is_live SELECT id, date, start_time, end_time, venue_name, venue_url, notes, cartell_url, is_live
FROM sessions FROM sessions
{clauses_str} {clauses_str}
""" """

View File

@@ -7,9 +7,9 @@ from . import conversion
def insert_session(session: model.Session, con: Connection | None = None): def insert_session(session: model.Session, con: Connection | None = None):
query = """ query = """
INSERT INTO sessions INSERT INTO sessions
(id, date, start_time, end_time, venue_name, venue_url, is_live) (id, date, start_time, end_time, venue_name, venue_url, notes, cartell_url, is_live)
VALUES VALUES
(:id, :date, :start_time, :end_time, :venue_name, :venue_url, :is_live) (:id, :date, :start_time, :end_time, :venue_name, :venue_url, :notes, :cartell_url, :is_live)
RETURNING * RETURNING *
""" """
data = conversion.session_to_row(session) data = conversion.session_to_row(session)
@@ -24,7 +24,8 @@ def update_session(session: model.Session, con: Connection | None = None):
query = """ query = """
UPDATE sessions SET UPDATE sessions SET
date = :date, start_time = :start_time, end_time = :end_time, date = :date, start_time = :start_time, end_time = :end_time,
venue_name = :venue_name, venue_url = :venue_url, is_live = :is_live venue_name = :venue_name, venue_url = :venue_url,
notes = :notes, cartell_url = :cartell_url, is_live = :is_live
WHERE id = :id WHERE id = :id
""" """
data = conversion.session_to_row(session) data = conversion.session_to_row(session)

View File

@@ -0,0 +1,31 @@
from fastapi import Request
from fastapi.responses import HTMLResponse
from folkugat_web.model.pagines import Pages
from folkugat_web.services import playlists as playlists_service
from folkugat_web.services import sessions as sessions_service
from folkugat_web.services.temes import query as query_service
from folkugat_web.services.temes import search as search_service
from folkugat_web.templates import templates
def cartell(request: Request, session_id: int, logged_in: bool):
session = sessions_service.get_session(session_id=session_id)
return templates.TemplateResponse(
"fragments/sessio/cartell.html",
{
"request": request,
"logged_in": logged_in,
"session_id": session_id,
"session": session,
}
)
def cartell_editor(request: Request, session_id: int):
return templates.TemplateResponse(
"fragments/sessio/cartell_editor.html",
{
"request": request,
"session_id": session_id,
}
)

View File

@@ -0,0 +1,25 @@
from fastapi import HTTPException, Request
from folkugat_web.model.pagines import Pages
from folkugat_web.services import playlists as playlists_service
from folkugat_web.services import sessions as sessions_service
from folkugat_web.templates import templates
def pagina(request: Request, session_id: int, logged_in: bool):
session = sessions_service.get_session(session_id=session_id)
if not session:
raise HTTPException(status_code=404, detail="Could not find session")
playlist = playlists_service.get_playlist(session_id=session_id)
playlist = playlists_service.add_temes_to_playlist(playlist)
return templates.TemplateResponse(
"fragments/sessio/pagina.html",
{
"request": request,
"logged_in": logged_in,
"Pages": Pages,
"session_id": session_id,
"session": session,
"playlist": playlist,
"date_names": sessions_service.get_date_names,
}
)

View File

@@ -8,24 +8,6 @@ from folkugat_web.services.temes import search as search_service
from folkugat_web.templates import templates from folkugat_web.templates import templates
def pagina(request: Request, session_id: int, logged_in: bool):
session = sessions_service.get_session(session_id=session_id)
playlist = playlists_service.get_playlist(session_id=session_id)
playlist = playlists_service.add_temes_to_playlist(playlist)
return templates.TemplateResponse(
"fragments/sessio/pagina.html",
{
"request": request,
"logged_in": logged_in,
"Pages": Pages,
"session_id": session_id,
"session": session,
"playlist": playlist,
"date_names": sessions_service.get_date_names,
}
)
def add_set(request: Request, session_id: int, logged_in: bool): def add_set(request: Request, session_id: int, logged_in: bool):
new_set = playlists_service.add_set(session_id=session_id) new_set = playlists_service.add_set(session_id=session_id)
return templates.TemplateResponse( return templates.TemplateResponse(

View File

@@ -19,6 +19,8 @@ class Session:
start_time: datetime.time = DEFAULT_START_TIME start_time: datetime.time = DEFAULT_START_TIME
end_time: datetime.time = DEFAULT_END_TIME end_time: datetime.time = DEFAULT_END_TIME
venue: SessionVenue = dataclasses.field(default_factory=SessionVenue) venue: SessionVenue = dataclasses.field(default_factory=SessionVenue)
notes: str | None = None
cartell_url: str | None = None
is_live: bool = False is_live: bool = False

View File

@@ -3,7 +3,7 @@ import mimetypes
import os import os
import re import re
import uuid import uuid
from collections.abc import Iterator from collections.abc import Iterable, Iterator
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from pathlib import Path from pathlib import Path
@@ -11,6 +11,7 @@ import aiofiles
import magic import magic
from fastapi import HTTPException, UploadFile from fastapi import HTTPException, UploadFile
from folkugat_web.config import db from folkugat_web.config import db
from folkugat_web.dal.sql.sessions import query as sessions_dal
from folkugat_web.dal.sql.temes import links as links_dal from folkugat_web.dal.sql.temes import links as links_dal
from folkugat_web.dal.sql.temes import scores as scores_dal from folkugat_web.dal.sql.temes import scores as scores_dal
from folkugat_web.log import logger from folkugat_web.log import logger
@@ -22,22 +23,16 @@ async def get_mimetype(upload_file: UploadFile) -> str:
return info.mime_type return info.mime_type
ACCEPTED_MIMETYPES = [ IMAGE_MIMETYPE = re.compile(r"image/.+")
re.compile(r"image/.+"), PDF_MIMETYPE = re.compile(r".+/pdf")
re.compile(r".+/pdf"),
]
def check_mimetype(mimetype: str) -> None: def check_mimetype(mimetype: str, accepted_mimetypes: Iterable[re.Pattern[str]]) -> None:
if not any(regex.match(mimetype) for regex in ACCEPTED_MIMETYPES): if not any(regex.match(mimetype) for regex in accepted_mimetypes):
raise HTTPException(status_code=400, detail=f"Unsupported file type: {mimetype}") raise HTTPException(status_code=400, detail=f"Unsupported file type: {mimetype}")
def get_db_file_path(filepath: Path) -> str: def check_upload_file_size(upload_file: UploadFile) -> None:
return f"{db.DB_FILES_URL}/{filepath.relative_to(db.DB_FILES_DIR)}"
async def store_file(tema_id: int, upload_file: UploadFile) -> str:
if not upload_file.size: if not upload_file.size:
raise HTTPException(status_code=400, detail="Couldn't find out the size of the file") raise HTTPException(status_code=400, detail="Couldn't find out the size of the file")
if upload_file.size > db.FILE_MAX_SIZE: if upload_file.size > db.FILE_MAX_SIZE:
@@ -46,8 +41,15 @@ async def store_file(tema_id: int, upload_file: UploadFile) -> str:
detail=f"The uploaded file is too big (max size = {db.FILE_MAX_SIZE} bytes)", detail=f"The uploaded file is too big (max size = {db.FILE_MAX_SIZE} bytes)",
) )
def get_db_file_path(filepath: Path) -> str:
return f"{db.DB_FILES_URL}/{filepath.relative_to(db.DB_FILES_DIR)}"
async def store_tema_file(tema_id: int, upload_file: UploadFile) -> str:
check_upload_file_size(upload_file)
mimetype = await get_mimetype(upload_file) mimetype = await get_mimetype(upload_file)
check_mimetype(mimetype) check_mimetype(mimetype, [IMAGE_MIMETYPE, PDF_MIMETYPE])
extension = mimetypes.guess_extension(mimetype) or "" extension = mimetypes.guess_extension(mimetype) or ""
filepath = create_tema_filename(tema_id=tema_id, extension=extension) filepath = create_tema_filename(tema_id=tema_id, extension=extension)
@@ -58,6 +60,20 @@ async def store_file(tema_id: int, upload_file: UploadFile) -> str:
return get_db_file_path(filepath) return get_db_file_path(filepath)
async def store_session_cartell(session_id: int, upload_file: UploadFile) -> str:
check_upload_file_size(upload_file)
mimetype = await get_mimetype(upload_file)
check_mimetype(mimetype, [IMAGE_MIMETYPE])
extension = mimetypes.guess_extension(mimetype) or ""
filepath = create_cartell_filename(session_id=session_id, extension=extension)
with open(filepath, "wb") as f:
_ = f.write(await upload_file.read())
return get_db_file_path(filepath)
def create_tema_filename(tema_id: int, extension: str = "") -> Path: def create_tema_filename(tema_id: int, extension: str = "") -> Path:
filename = str(uuid.uuid4().hex) + extension filename = str(uuid.uuid4().hex) + extension
filedir = db.DB_FILES_TEMA_DIR / str(tema_id) filedir = db.DB_FILES_TEMA_DIR / str(tema_id)
@@ -66,6 +82,14 @@ def create_tema_filename(tema_id: int, extension: str = "") -> Path:
return filepath return filepath
def create_cartell_filename(session_id: int, extension: str = "") -> Path:
filename = str(uuid.uuid4().hex) + extension
filedir = db.DB_FILES_SESSION_DIR / str(session_id) / "cartell"
filedir.mkdir(parents=True, exist_ok=True)
filepath = filedir / filename
return filepath
def create_tmp_filename(extension: str = "") -> Path: def create_tmp_filename(extension: str = "") -> Path:
filename = str(uuid.uuid4().hex) + extension filename = str(uuid.uuid4().hex) + extension
filepath = db.DB_FILES_TMP_DIR / filename filepath = db.DB_FILES_TMP_DIR / filename
@@ -94,11 +118,13 @@ def list_files(tema_id: str) -> list[str]:
def get_orphan_files() -> Iterator[Path]: def get_orphan_files() -> Iterator[Path]:
link_urls = {link.url for link in links_dal.get_links()} alive_urls = (
score_pdf_urls = {score.pdf_url for score in scores_dal.get_scores() if score.pdf_url is not None} {link.url for link in links_dal.get_links()}
score_img_urls = {score.img_url for score in scores_dal.get_scores() if score.img_url is not None} | {score.pdf_url for score in scores_dal.get_scores() if score.pdf_url is not None}
score_preview_urls = {score.preview_url for score in scores_dal.get_scores() if score.preview_url is not None} | {score.img_url for score in scores_dal.get_scores() if score.img_url is not None}
alive_urls = link_urls | score_pdf_urls | score_img_urls | score_preview_urls | {score.preview_url for score in scores_dal.get_scores() if score.preview_url is not None}
| {session.cartell_url for session in sessions_dal.get_sessions() if session.cartell_url}
)
return filter( return filter(
lambda p: p.is_file() and get_db_file_path(p) not in alive_urls, lambda p: p.is_file() and get_db_file_path(p) not in alive_urls,
itertools.chain( itertools.chain(

View File

@@ -0,0 +1,10 @@
from folkugat_web.dal.sql import get_connection
with get_connection() as con:
cur = con.cursor()
alter_query_1 = """ ALTER TABLE sessions ADD COLUMN notes TEXT """
alter_query_2 = """ ALTER TABLE sessions ADD COLUMN cartell_url TEXT """
_ = cur.execute(alter_query_1)
_ = cur.execute(alter_query_2)
print("DONE!")