songs2slides

A tool that automatically finds song lyrics and creates lyric slideshows
git clone https://git.ashermorgan.net/songs2slides/
Log | Files | Refs | README

commit d17fffd9a96f0bf745d9211be6e5085ca0fdcdd0
parent 540b3af694cecfa7a5c39718692a386fc0988667
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Thu, 28 Mar 2024 15:04:33 -0700

Move create steps to separate routes

This improves the behavior of the browser back button

Diffstat:
Msongs2slides/routes.py | 29++++++++++++++++++++++-------
Msongs2slides/static/create.js | 24+++++++++++-------------
Asongs2slides/templates/create-step-1.html | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asongs2slides/templates/create-step-2.html | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsongs2slides/templates/create.html | 199-------------------------------------------------------------------------------
Msongs2slides/templates/home.html | 2+-
Mtests/test_routes.py | 10+++++-----
7 files changed, 225 insertions(+), 225 deletions(-)

diff --git a/songs2slides/routes.py b/songs2slides/routes.py @@ -1,4 +1,5 @@ -from flask import abort, Blueprint, render_template, request, send_file +from flask import abort, Blueprint, redirect, render_template, request, \ + send_file, url_for import tempfile from songs2slides import core @@ -40,11 +41,20 @@ def home(): return render_template('home.html') @bp.get('/create/') -def create(): - return render_template('create.html', step=1, songs=[], missing=0) +def create_root(): + return redirect(url_for('.create_step_1'), 301) -@bp.post('/create/') -def get_lyrics(): +@bp.get('/create/step-1/') +def create_step_1(): + return render_template('create-step-1.html') + +@bp.get('/create/step-2/') +def create_step_2_get(): + # GET requests not allowed, redirect to step 1 + return redirect(url_for('.create_step_1'), 302) + +@bp.post('/create/step-2/') +def create_step_2(): # Parse form data songs = parse_form(request.form) @@ -61,10 +71,15 @@ def get_lyrics(): missing = sum([1 for x in songs if x.lyrics == None]) # Return song data - return render_template('create.html', step=2, songs=songs, missing=missing) + return render_template('create-step-2.html', songs=songs, missing=missing) + +@bp.get('/slides/') +def slides_get(): + # GET requests not allowed, redirect to home page + return redirect(url_for('.home'), 302) @bp.post('/slides/') -def create_slides(): +def slides(): # Parse form data songs = parse_form(request.form) title_slides = 'title-slides' in request.form diff --git a/songs2slides/static/create.js b/songs2slides/static/create.js @@ -1,13 +1,19 @@ addEventListener('submit', () => { - if (document.getElementById('step-1').hidden == false) { + if (document.getElementById('step-1')) { // Show step 1 spinner - document.getElementById('post-step-1').hidden = false + document.getElementById('post-submit').hidden = false } else if (document.querySelector('input[value=pptx]').checked) { - // Show step 2 downloading message + // Show step 2 downloading message and hide step 2 form + document.getElementById('post-submit').hidden = false document.getElementById('step-2').hidden = true - document.getElementById('post-step-2').hidden = false } -}); +}) + +addEventListener('pageshow', () => { + // Correct page state after returning via browser back button + document.getElementById('post-submit').hidden = true + document.getElementsByTagName('form')[0].hidden = false +}) /* step 1 functions */ function add_song() { @@ -31,11 +37,3 @@ function renumber_songs() { songs[i].children[3].children[0].onclick = () => remove_song(i) } } - -/* step 2 functions */ -function back() { - document.getElementById('step-1').hidden = false - document.getElementById('post-step-1').hidden = true - document.getElementById('step-2').hidden = true - document.getElementById('post-step-2').hidden = true -} diff --git a/songs2slides/templates/create-step-1.html b/songs2slides/templates/create-step-1.html @@ -0,0 +1,81 @@ +{% extends "layout.html" %} + +{% block head %} +<link rel="stylesheet" href="{{ url_for('static', filename='create.css') }}"/> +<script src="{{ url_for('static', filename='create.js') }}"></script> +{% endblock head %} + +{% block main %} +<form id="step-1" method="POST" action="{{ url_for('.create_step_2') }}"> + <h1>Step 1: Select Songs</h1> + + <p> + Select the songs to include in the slide show by their title and artist. + </p> + + <template id="row-template"> + <tr> + <td></td> + <td> + <input name="title-" placeholder="Song title" + aria-label="Song title" required/> + </td> + <td> + <input name="artist-" placeholder="Song artist" + aria-label="Song artist"/> + </td> + <td> + <button class="icon" type="button" title="Remove"> + <img src="{{ url_for('static', filename='trash.svg') }}" + alt="Remove icon"/> + </button> + </td> + </tr> + </template> + + <table> + <thead> + <tr> + <th></th> + <th>Title</th> + <th>Artist</th> + <th></th> + </tr> + </thead> + <tbody id="songs"> + <tr> + <td>1.</td> + <td> + <input type="text" name="title-1" placeholder="Song title" + aria-label="Song title" required/> + </td> + <td> + <input type="text" name="artist-1" + placeholder="Song artist" aria-label="Song title"/> + </td> + <td> + <button class="icon" type="button" onclick="remove_song(1)" + title="Remove"> + <img src="{{ url_for('static', filename='trash.svg') }}" + alt="Remove icon"/> + </button> + </td> + </tr> + </tbody> + </table> + + <div id="actions"> + <input type="button" value="Add song" onclick="add_song()"/> + <button type="submit"> + Next + </button> + </div> +</form> + +<div id="post-submit" class="loading-modal" hidden> + <div> + <p>Loading your song lyrics...</p> + <div class="spinner"></div> + </div> +</div> +{% endblock main %} diff --git a/songs2slides/templates/create-step-2.html b/songs2slides/templates/create-step-2.html @@ -0,0 +1,105 @@ +{% extends "layout.html" %} + +{% block head %} +<link rel="stylesheet" href="{{ url_for('static', filename='create.css') }}"/> +<script src="{{ url_for('static', filename='create.js') }}"></script> +{% endblock head %} + +{% set format_hint = +'One blank line represents the start of a new slide and three blank ' + +'lines represent an empty slide.' +%} + +{% block main %} +<form id="step-2" method="POST" action="{{ url_for('.slides') }}"> + <h1>Step 2: Preview Slides</h1> + <p> + Review the parsed song lyrics below and make any necessary corrections. + {{ format_hint }} + </p> + + {% if missing > 0 %} + <p> + Lyrics must be entered manually for + {{ missing }} {% if missing == 1 %} song. {% else %} songs. {% endif %} + </p> + {% endif %} + + <div> + {% for song in songs %} + <details + {% if missing == 0 and loop.index == 1 %} open {% endif %} + {% if not song.lyrics %} open class="missing" {% endif %}> + + <input hidden name="title-{{ loop.index }}" + value="{{ song.title }}"/> + <input hidden name="artist-{{ loop.index }}" + value="{{ song.artist }}"/> + + <summary> + <i>{{ song.title }}</i> + + {% if song.artist %} + ({{ song.artist }}) + {% endif %} + + {% if not song.lyrics %} + <span>lyrics not found</span> + {% endif %} + </summary> + + {% if song.lyrics %} + <textarea name="lyrics-{{ loop.index }}" placeholder="{{format_hint}}" + aria-label="{{ song.title }} Lyrics">{{ song.lyrics }}</textarea> + {% else %} + <textarea name="lyrics-{{ loop.index }}" placeholder="{{ + 'Lyrics not found, please enter them here manually.\n\n' + + format_hint }}" aria-label="{{ song.title }} Lyrics"></textarea> + {% endif %} + </details> + {% endfor %} + </div> + + <div> + <fieldset> + <legend>Extra slides:</legend> + <label> + <input type="checkbox" name="title-slides" checked/> + Include a title slide before each song + </label> + <label> + <input type="checkbox" name="blank-slides" checked/> + Include a blank slide between each song + </label> + </fieldset> + <fieldset> + <legend>Output type:</legend> + <label> + <input type="radio" name="output-type" value="html" checked/> + Web View + </label> + <label> + <input type="radio" name="output-type" value="pptx"/> + PowerPoint download + </label> + </fieldset> + </div> + + <div id="actions"> + <input onclick="history.back()" type="button" value="Back"/> + <button type="submit"> + Create Slide Show + </button> + </div> +</form> + +<div id="post-submit" hidden> + <p> + Your slide show is being downloaded, + thank you for using Songs2Slides. + </p> + <p> + <a href="{{ url_for('.create_step_1') }}">Create another slide show</a> + </p> +</div> +{% endblock main %} diff --git a/songs2slides/templates/create.html b/songs2slides/templates/create.html @@ -1,199 +0,0 @@ -{% extends "layout.html" %} - -{% block head %} -<link rel="stylesheet" href="{{ url_for('static', filename='create.css') }}"/> -<script src="{{ url_for('static', filename='create.js') }}"></script> -{% endblock head %} - -{% block main %} -<form id="step-1" method="POST" {% if step == 2 %} hidden {% endif %}> - <h1>Step 1: Select Songs</h1> - - <p> - Select the songs to include in the slide show by their title and artist. - </p> - - <template id="row-template"> - <tr> - <td></td> - <td> - <input name="title-" placeholder="Song title" required/> - </td> - <td> - <input name="artist-" placeholder="Song artist"/> - </td> - <td> - <button class="icon" type="button" title="Remove"> - <img src="{{ url_for('static', filename='trash.svg') }}" - alt="Remove icon"/> - </button> - </td> - </tr> - </template> - - <table> - <thead> - <tr> - <th></th> - <th>Title</th> - <th>Artist</th> - <th></th> - </tr> - </thead> - <tbody id="songs"> - {% if step == 1 %} - <tr> - <td>1.</td> - <td> - <input type="text" name="title-1" placeholder="Song title" required/> - </td> - <td> - <input type="text" name="artist-1" placeholder="Song artist"/> - </td> - <td> - <button class="icon" type="button" onclick="remove_song(1)" - title="Remove"> - <img src="{{ url_for('static', filename='trash.svg') }}" - alt="Remove icon"/> - </button> - </td> - </tr> - {% else %} - {% for song in songs %} - <tr> - <td>{{ loop.index }}.</td> - <td> - <input type="text" name="title-{{ loop.index }}" required - placeholder="Song title" value="{{ song.title }}"/> - </td> - <td> - <input type="text" name="artist-{{ loop.index }}" - placeholder="Song artist" value="{{ song.artist }}"/> - </td> - <td> - <button class="icon" type="button" title="Remove" - onclick="remove_song({{ loop.index }})"> - <img src="{{ url_for('static', filename='trash.svg') }}" - alt="Remove icon"/> - </button> - </td> - </tr> - {% endfor %} - {% endif %} - </tbody> - </table> - - <div id="actions"> - <input type="button" value="Add song" onclick="add_song()"/> - <button type="submit"> - Next - </button> - </div> -</form> - -<div id="post-step-1" class="loading-modal" hidden> - <div> - <p>Loading your song lyrics...</p> - <div class="spinner"></div> - </div> -</div> - -<form id="step-2" method="POST" {% if step == 1 %} hidden {% endif %} - action="{{ url_for('.create_slides') }}"> - - {% set format_hint = - 'One blank line represents the start of a new slide and three blank ' + - 'lines represent an empty slide.' - %} - - <h1>Step 2: Preview Slides</h1> - <p> - Review the parsed song lyrics below and make any necessary corrections. - {{ format_hint }} - </p> - - {% if missing > 0 %} - <p> - Lyrics must be entered manually for - {{ missing }} {% if missing == 1 %} song. {% else %} songs. {% endif %} - </p> - {% endif %} - - <div> - {% for song in songs %} - <details - {% if missing == 0 and loop.index == 1 %} open {% endif %} - {% if not song.lyrics %} open class="missing" {% endif %}> - - <input hidden name="title-{{ loop.index }}" - value="{{ song.title }}"/> - <input hidden name="artist-{{ loop.index }}" - value="{{ song.artist }}"/> - - <summary> - <i>{{ song.title }}</i> - - {% if song.artist %} - ({{ song.artist }}) - {% endif %} - - {% if not song.lyrics %} - <span>lyrics not found</span> - {% endif %} - </summary> - - {% if song.lyrics %} - <textarea name="lyrics-{{ loop.index }}" - placeholder="{{format_hint}}">{{ song.lyrics }}</textarea> - {% else %} - <textarea name="lyrics-{{ loop.index }}" placeholder="{{ - 'Lyrics not found, please enter them here manually.\n\n' + - format_hint }}"></textarea> - {% endif %} - </details> - {% endfor %} - </div> - - <div> - <fieldset> - <legend>Extra slides:</legend> - <label> - <input type="checkbox" name="title-slides" checked/> - Include a title slide before each song - </label> - <label> - <input type="checkbox" name="blank-slides" checked/> - Include a blank slide between each song - </label> - </fieldset> - <fieldset> - <legend>Output type:</legend> - <label> - <input type="radio" name="output-type" value="html" checked/> - Web View - </label> - <label> - <input type="radio" name="output-type" value="pptx"/> - PowerPoint download - </label> - </fieldset> - </div> - - <div id="actions"> - <input onclick="back()" type="button" value="Back"/> - <button type="submit"> - Create Slide Show - </button> - </div> -</form> - -<div id="post-step-2" hidden> - <p> - Your slide show is being downloaded, - thank you for using Songs2Slides. - </p> - <p> - <a href="{{ url_for('.create') }}">Create another slide show</a> - </p> -</div> -{% endblock main %} diff --git a/songs2slides/templates/home.html b/songs2slides/templates/home.html @@ -6,5 +6,5 @@ {% block main %} <p>Create lyric slide shows easily and quickly</p> -<a href="{{ url_for('.create') }} ">Get Started</a> +<a href="{{ url_for('.create_step_1') }} ">Get Started</a> {% endblock main %} diff --git a/tests/test_routes.py b/tests/test_routes.py @@ -22,7 +22,7 @@ class TestRoutes(unittest.TestCase): mocked_parse.side_effect = ['L1', 'L2'] # Send request - self.client.post('/create/', data={ + self.client.post('/create/step-2/', data={ 'title-1': 'T1', 'artist-1': 'A1', 'title-2': 'T2', @@ -32,7 +32,7 @@ class TestRoutes(unittest.TestCase): # Assert mocks called correctly mocked_get.assert_has_calls([call('T1', 'A1'), call('T2', 'A2')]) mocked_parse.assert_has_calls([call('L1', 4), call('L2', 4)]) - mocked_render.assert_called_with('create.html', step=2, songs=songs, missing=0) + mocked_render.assert_called_with('create-step-2.html', songs=songs, missing=0) def test_get_lyrics_one_error(self): with patch('songs2slides.core.get_song_data') as mocked_get, \ @@ -48,7 +48,7 @@ class TestRoutes(unittest.TestCase): mocked_parse.side_effect = ['L1', 'L2'] # Send request - self.client.post('/create/', data={ + self.client.post('/create/step-2/', data={ 'title-1': 'T1', 'artist-1': 'A1', 'title-2': 'T2', @@ -58,13 +58,13 @@ class TestRoutes(unittest.TestCase): # Assert mocks called correctly mocked_get.assert_has_calls([call('T1', 'A1'), call('T2', 'A2')]) mocked_parse.assert_has_calls([call('L2', 4)]) - mocked_render.assert_called_with('create.html', step=2, songs=songs, missing=1) + mocked_render.assert_called_with('create-step-2.html', songs=songs, missing=1) def test_get_lyrics_missing_artist(self): with patch('songs2slides.core.get_song_data') as mocked_get: # Send request - res = self.client.post('/create/', data={ + res = self.client.post('/create/step-2/', data={ 'title-1': 'T1', 'title-2': 'T2', 'artist-2': 'A2',