]> littlesong.place Git - littlesongplace.git/commitdiff
Add PFPs
authorChris Fulljames <christianfulljames@gmail.com>
Tue, 4 Feb 2025 12:22:34 +0000 (07:22 -0500)
committerChris Fulljames <christianfulljames@gmail.com>
Tue, 4 Feb 2025 12:23:38 +0000 (07:23 -0500)
.gitignore
main.py
static/styles.css
templates/profile.html
test/lsp_notes.png [new file with mode: 0644]
test/test_offline.py
todo.txt

index b9030d3d5b270060e8171fee0c4188f4f1bf0e93..154346671197612d1ed8c56017a1105c308ba437 100644 (file)
@@ -2,5 +2,6 @@ venv
 __pycache__
 database.db
 songs
+images
 app.log*
 
diff --git a/main.py b/main.py
index a8ff87887b4fabc0965f82efd886b54c08ff8e4c..a3497a6ad6956d2b4f52db80a1d0bb30d6a7cca6 100644 (file)
--- a/main.py
+++ b/main.py
@@ -165,18 +165,30 @@ def users_profile(profile_username):
             bio=profile_bio,
             song_list=render_template("song-list.html", songs=songs))
 
-@app.post("/update-bio")
-def update_bio():
+@app.post("/edit-profile")
+def edit_profile():
+    if not "userid" in session:
+        abort(401)
+
     query_db(
             "update users set bio = ? where userid = ?",
             [request.form["bio"], session["userid"]])
     get_db().commit()
-    flash("Bio updated successfully")
+
+    if request.files["pfp"]:
+        pfp_path = get_user_images_path(session["userid"]) / "pfp"
+        request.files["pfp"].save(pfp_path)
+
+    flash("Profile updated successfully")
 
     app.logger.info(f"{session['username']} updated bio")
 
     return redirect(f"/users/{session['username']}")
 
+@app.get("/pfp/<int:userid>")
+def pfp(userid):
+    return send_from_directory(DATA_DIR / "images" / str(userid), "pfp")
+
 @app.get("/edit-song")
 def edit_song():
     if not "userid" in session:
@@ -267,12 +279,18 @@ def validate_song_form():
 
     return error
 
-def get_user_path(userid):
+def get_user_songs_path(userid):
     userpath = DATA_DIR / "songs" / str(userid)
     if not userpath.exists():
         os.makedirs(userpath)
     return userpath
 
+def get_user_images_path(userid):
+    userpath = DATA_DIR / "images" / str(userid)
+    if not userpath.exists():
+        os.makedirs(userpath)
+    return userpath
+
 def update_song():
     songid = request.args["songid"]
     try:
@@ -300,7 +318,7 @@ def update_song():
 
             if passed:
                 # Move file to permanent location
-                filepath = get_user_path(session["userid"]) / (str(song_data["songid"]) + ".mp3")
+                filepath = get_user_songs_path(session["userid"]) / (str(song_data["songid"]) + ".mp3")
                 shutil.move(tmp_file.name, filepath)
             else:
                 error = True
@@ -345,7 +363,7 @@ def create_song():
                     "insert into songs (userid, title, description, created) values (?, ?, ?, ?) returning (songid)",
                     [session["userid"], title, description, timestamp], one=True)
             songid = song_data["songid"]
-            filepath = get_user_path(session["userid"]) / (str(song_data["songid"]) + ".mp3")
+            filepath = get_user_songs_path(session["userid"]) / (str(song_data["songid"]) + ".mp3")
 
             # Move file to permanent location
             shutil.move(tmp_file.name, filepath)
index 3500ea4e0ebc75b45520895d4588676bd18c3e70..3cb63a3ddc0a436489a69574c4faa0f2c726a822 100644 (file)
@@ -59,6 +59,7 @@ input[type=text], input[type=password] {
     font-family: sans-serif;
     font-size: 16px;
     font-weight: bold;
+    text-decoration: none;
     color: var(--yellow);
     background: var(--purple);
     border: 0px;
@@ -165,8 +166,26 @@ input[type=file] {
     font-size: 40px;
 }
 
+.big-pfp-container {
+    margin: 0 auto;
+    width: 200px;
+    max-width: 80%;
+    background: var(--purple);
+    padding: 5px;
+    border-radius: 10px;
+}
+
+.big-pfp {
+    width: 100%;
+    max-height: 200px;
+    margin: 0px;
+    padding: 0px;
+    display: block;
+    border-radius: 5px;
+}
+
 .profile-action {
-    margin: 10px;
+    margin-bottom: 20px;
 }
 
 .profile-edit-buttons {
index 3f8bceebf1dd666ba6ef1456bccdbecdd7717f62..308cb69a734b5fda5dea02a5541cfcf719772e7c 100644 (file)
@@ -4,19 +4,34 @@
 
 {% block body %}
 
+<!-- Username -->
 <h1 class="profile-name">{{ name }}</h1>
 
+<!-- Profile Picture -->
+<div class="big-pfp-container">
+    <img src="/pfp/{{ userid }}" onerror="hidePfp(this)" class="big-pfp">
+</div>
+
+<script>
+    function hidePfp(pfp) {
+        pfp.parentElement.hidden = true;
+    }
+</script>
+
 <!-- Bio -->
 <div class="profile-bio" id="profile-bio">{{ (bio.replace("\n", "<br>"))|safe }}</div>
 
-<!-- Bio edit form -->
+<!-- Profile edit form -->
 {% if session["userid"] == userid %}
 
 <div class="profile-action">
-    <a href="javascript:showEditForm();" id="profile-bio-edit-btn">Edit Bio</a>
+    <button class="button" onclick="showEditForm()" id="profile-bio-edit-btn">Edit Profile</button>
 </div>
 
-<form id="profile-edit-form" action="/update-bio" method="post" hidden>
+<form id="profile-edit-form" action="/edit-profile" method="post" enctype="multipart/form-data" hidden>
+    <h2> Profile Picture </h2>
+    <input type="file" name="pfp" accept="image/png, image/jpg, image/gif, image/svg" />
+
     <h2> Edit Bio </h2>
     <p>Common HTML tags (&lt;a&gt;, &lt;b&gt;, &lt;i&gt;, &lt;img&gt;, etc.) are allowed.</p>
     <p>Examples:</p>
@@ -35,7 +50,7 @@
     </div>
 </form>
 
-<!-- Show/hide bio edit form -->
+<!-- Show/hide profile edit form -->
 <script>
     function showEditForm() {
         document.getElementById("profile-bio").hidden = true;
@@ -56,7 +71,7 @@
 <!-- Upload New Song button -->
 {% if session["userid"] == userid %}
 <div class="profile-action">
-    <a href="/edit-song">Upload New Song</a>
+    <a class="button" href="/edit-song">Upload New Song</a>
 </div>
 {% endif %}
 
diff --git a/test/lsp_notes.png b/test/lsp_notes.png
new file mode 100644 (file)
index 0000000..97625d2
Binary files /dev/null and b/test/lsp_notes.png differ
index 6af50828ca23035d883c548d8b1988507b4e3ed1..6decc137bf8ffd6b975328d18777f6f36b4946a9 100644 (file)
@@ -165,13 +165,42 @@ def test_default_bio_empty(client):
 def test_update_bio(client):
     _create_user(client, "user", "password", login=True)
 
-    response = client.post("/update-bio", data={"bio": "this is the bio"})
+    response = client.post("/edit-profile", data={"bio": "this is the bio", "pfp": (b"", "", "aplication/octet-stream")})
     assert response.status_code == 302
     assert response.headers["Location"] == "/users/user"
 
     response = client.get("/users/user")
     assert b'<div class="profile-bio" id="profile-bio">this is the bio</div>' in response.data
 
+def test_upload_pfp(client):
+    _create_user(client, "user", "password", login=True)
+    response = client.post("/edit-profile", data={"bio": "", "pfp": open("lsp_notes.png", "rb")})
+    assert response.status_code == 302
+
+def test_edit_profile_not_logged_in(client):
+    response = client.post("/edit-profile", data={"bio": "", "pfp": open("lsp_notes.png", "rb")})
+    assert response.status_code == 401
+
+def test_get_pfp(client):
+    _create_user(client, "user", "password", login=True)
+    client.post("/edit-profile", data={"bio": "", "pfp": open("lsp_notes.png", "rb")})
+
+    response = client.get("/pfp/1")
+    assert response.status_code == 200
+    with open("lsp_notes.png", "rb") as expected:
+        assert expected.read() == response.data
+
+def test_get_pfp_no_file(client):
+    _create_user(client, "user", "password", login=True)
+    # User exists but doesn't have a pfp
+    response = client.get("/pfp/1")
+    assert response.status_code == 404
+
+def test_get_pfp_invalid_user(client):
+    response = client.get("/pfp/1")
+    # User doesn't exist
+    assert response.status_code == 404
+
 ################################################################################
 # Upload Song
 ################################################################################
index eeb02f525c5edd2c8bcae693a3078336ef10a255..2c6a49013b78bfb026b22bb44f04a570cfc9e976 100644 (file)
--- a/todo.txt
+++ b/todo.txt
@@ -1,8 +1,8 @@
 NOW
-- Profile pictures
+- PFP in song list
+- Additional song info in player (collabs, description, tags, pfp)
 - Dark mode/site color customization
 - YouTube importer
-- Additional song info in player (collabs, description, tags)
 
 SOON
 - Shuffle all