]> littlesong.place Git - littlesongplace.git/commitdiff
Add comment threads to profile, playlist
authorChris Fulljames <christianfulljames@gmail.com>
Sat, 22 Mar 2025 01:12:35 +0000 (21:12 -0400)
committerChris Fulljames <christianfulljames@gmail.com>
Sat, 22 Mar 2025 01:12:35 +0000 (21:12 -0400)
main.py
schema_update.sql
templates/activity.html
templates/comment-thread.html [new file with mode: 0644]
templates/playlist.html
templates/profile.html
templates/song-macros.html
todo.txt

diff --git a/main.py b/main.py
index 68564e26c49788dc71e66816dab9cac4d14cf9e1..ff3b3ea411f733f4d2eb5779443bfe5ed1c23174 100644 (file)
--- a/main.py
+++ b/main.py
@@ -122,7 +122,12 @@ def signup_post():
 
     password = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
     timestamp = datetime.now(timezone.utc).isoformat()
-    query_db("insert into users (username, password, created) values (?, ?, ?)", [username, password, timestamp])
+
+    user_data = query_db("insert into users (username, password, created, threadid) values (?, ?, ?, ?) returning userid", [username, password, timestamp, threadid], one=True)
+
+    # Create profile comment thread
+    threadid = create_comment_thread(ThreadType.PROFILE, user_data["userid"])
+    query_db("update users set threadid = ? where userid = ?", [threadid, user_data["userid"]])
     get_db().commit()
 
     flash("User created.  Please sign in to continue.", "success")
@@ -185,6 +190,9 @@ def users_profile(profile_username):
     # Get songs for current profile
     songs = Song.get_all_for_userid(profile_userid)
 
+    # Get comments for current profile
+    comments = get_comments(profile_data["threadid"])
+
     # Sanitize bio
     profile_bio = ""
     if profile_data["bio"] is not None:
@@ -198,8 +206,9 @@ def users_profile(profile_username):
             **get_user_colors(profile_data),
             playlists=plist_data,
             songs=songs,
-            user_has_pfp=user_has_pfp(profile_userid),
-            is_profile_song_list=True)
+            comments=comments,
+            threadid=profile_data["threadid"],
+            user_has_pfp=user_has_pfp(profile_userid))
 
 @app.post("/edit-profile")
 def edit_profile():
@@ -430,11 +439,13 @@ def create_song():
         if not passed:
             return True
         else:
+            # Create comment thread
+            threadid = create_comment_thread(ThreadType.SONG, session["userid"])
             # Create song
             timestamp = datetime.now(timezone.utc).isoformat()
             song_data = query_db(
-                    "insert into songs (userid, title, description, created) values (?, ?, ?, ?) returning (songid)",
-                    [session["userid"], title, description, timestamp], one=True)
+                    "insert into songs (userid, title, description, created, threadid) values (?, ?, ?, ?, ?) returning (songid)",
+                    [session["userid"], title, description, timestamp, threadid], one=True)
             songid = song_data["songid"]
             filepath = get_user_songs_path(session["userid"]) / (str(song_data["songid"]) + ".mp3")
 
@@ -580,7 +591,7 @@ def comment():
     if not "threadid" in request.args:
         abort(400) # Must have threadid
 
-    thread = query_db("select * from comment_threads where threadid = ?", [threadid], one=True)
+    thread = query_db("select * from comment_threads where threadid = ?", [request.args["threadid"]], one=True)
     if not thread:
         abort(400) # Invalid threadid
 
@@ -634,9 +645,9 @@ def comment():
             # Add new comment
             timestamp = datetime.now(timezone.utc).isoformat()
             userid = session["userid"]
-            songid = request.args["songid"]
             replytoid = request.args.get("replytoid", None)
 
+            threadid = request.args["threadid"]
             comment = query_db(
                     "insert into comments (threadid, userid, replytoid, created, content) values (?, ?, ?, ?, ?) returning (commentid)",
                     args=[threadid, userid, replytoid, timestamp, content], one=True)
@@ -753,7 +764,7 @@ def new_activity():
         user_data = query_db("select activitytime from users where userid = ?", [session["userid"]], one=True)
         comment_data = query_db(
             """\
-            select sc.created from comment_notifications as cn
+            select c.created from comment_notifications as cn
             inner join comments as c on cn.commentid = c.commentid
             where cn.targetuserid = ?
             order by c.created desc
@@ -788,14 +799,17 @@ def create_playlist():
 
     private = request.form["type"] == "private"
 
+    threadid = create_comment_thread(ThreadType.PLAYLIST, session["userid"])
+
     query_db(
-        "insert into playlists (created, updated, userid, name, private) values (?, ?, ?, ?, ?)",
+        "insert into playlists (created, updated, userid, name, private, threadid) values (?, ?, ?, ?, ?, ?)",
         args=[
             timestamp,
             timestamp,
             session["userid"],
             name,
             private,
+            threadid
         ]
     )
     get_db().commit()
@@ -932,6 +946,9 @@ def playlists(playlistid):
     # Get songs
     songs = Song.get_for_playlist(playlistid)
 
+    # Get comments
+    comments = get_comments(plist_data["threadid"])
+
     # Show page
     return render_template(
             "playlist.html",
@@ -940,8 +957,10 @@ def playlists(playlistid):
             private=plist_data["private"],
             userid=plist_data["userid"],
             username=plist_data["username"],
+            threadid=plist_data["threadid"],
             **get_user_colors(plist_data),
-            songs=songs)
+            songs=songs,
+            comments=comments)
 
 def flash_and_log(msg, category=None):
     flash(msg, category)
@@ -975,6 +994,26 @@ def sanitize_user_text(text):
                 attributes=allowed_attributes,
                 css_sanitizer=css_sanitizer)
 
+def create_comment_thread(threadtype, userid):
+    thread = query_db("insert into comment_threads (threadtype, userid) values (?, ?) returning threadid", [threadtype, userid], one=True)
+    get_db().commit()
+    return thread["threadid"]
+
+def get_comments(threadid):
+    comments = query_db("select * from comments inner join users on comments.userid == users.userid where comments.threadid = ?", [threadid])
+    comments = [dict(c) for c in comments]
+    for c in comments:
+        c["content"] = sanitize_user_text(c["content"])
+
+    # Top-level comments
+    song_comments = sorted([dict(c) for c in comments if c["replytoid"] is None], key=lambda c: c["created"])
+    song_comments = list(reversed(song_comments))
+    # Replies (can only reply to top-level)
+    for comment in song_comments:
+        comment["replies"] = sorted([c for c in comments if c["replytoid"] == comment["commentid"]], key=lambda c: c["created"])
+
+    return song_comments
+
 def get_gif_data():
     gifs = []
     static_path = Path(__file__).parent / "static"
@@ -1082,7 +1121,7 @@ def get_db():
 def assign_thread_ids(db, table, id_col, threadtype):
     cur = db.execute(f"select * from {table}")
     for row in cur:
-        thread_cur = db.execute("insert into comment_threads (threadtype) values (?) returning threadid", [threadtype])
+        thread_cur = db.execute("insert into comment_threads (threadtype, userid) values (?, ?) returning threadid", [threadtype, row["userid"]])
         threadid = thread_cur.fetchone()[0]
         thread_cur.close()
 
@@ -1145,19 +1184,7 @@ class Song:
         return json.dumps(vars(self))
 
     def get_comments(self):
-        comments = query_db("select * from comments inner join users on comments.userid == users.userid where threadid = ?", [self.threadid])
-        comments = [dict(c) for c in comments]
-        for c in comments:
-            c["content"] = sanitize_user_text(c["content"])
-
-        # Top-level comments
-        song_comments = sorted([dict(c) for c in comments if c["replytoid"] is None], key=lambda c: c["created"])
-        song_comments = list(reversed(song_comments))
-        # Replies (can only reply to top-level)
-        for comment in song_comments:
-            comment["replies"] = sorted([c for c in comments if c["replytoid"] == comment["commentid"]], key=lambda c: c["created"])
-
-        return song_comments
+        return get_comments(self.threadid)
 
     @classmethod
     def by_id(cls, songid):
@@ -1231,7 +1258,7 @@ class Song:
             song_collabs = [c["name"] for c in collabs[sd["songid"]] if c["name"]]
             created = datetime.fromisoformat(sd["created"]).astimezone().strftime("%Y-%m-%d")
             has_pfp = user_has_pfp(sd["userid"])
-            songs.append(cls(sd["songid"], sd["userid"], sd["username"], sd["title"], sanitize_user_text(sd["description"]), created, song_tags, song_collabs, has_pfp))
+            songs.append(cls(sd["songid"], sd["userid"], sd["threadid"], sd["username"], sd["title"], sanitize_user_text(sd["description"]), created, song_tags, song_collabs, has_pfp))
         return songs
 
     @classmethod
index 12d6d7c6e7f0111f20775c289483ea9a03b8094a..b9cc5157a92da81bcc6da3f2f165b1da17c9c07b 100644 (file)
@@ -1,7 +1,7 @@
 -- Create new comment tables
 CREATE TABLE comment_threads (
     threadid INTEGER PRIMARY KEY,
-    threadtype INTEGER NOT NULL
+    threadtype INTEGER NOT NULL,
     userid INTEGER NOT NULL,
     FOREIGN KEY(userid) REFERENCES users(userid) ON DELETE CASCADE
 );
index 334a13716fdd70f05e3e26dd84f75ea6573372dd..ba80ffab6e9e5dbe025d5dcbff659f136bb1a534 100644 (file)
             commented
             {% endif %}
             on
-            <a href="/song/{{ comment['content_userid'] }}/{{ comment['songid'] }}?action=view">{{ comment['title'] }}</a>
-            -
+            {% if 'songid' in comment %}
+            <a href="/song/{{ comment['content_userid'] }}/{{ comment['songid'] }}?action=view">{{ comment['title'] }}</a> -
+            {# Nothing to do for user profile #}
+            {% elif 'playlistid' in comment %}
+            <a href="/playlists/{{ comment['playlistid'] }}?action=view">{{ comment['title'] }}</a> -
+            {% endif %}
             <a href="/users/{{ comment['content_username'] }}" class="profile-link">{{ comment['content_username'] }}</a>
             <div class="top-level-comment">
                 <a href="/users/{{ comment['comment_username'] }}" class="profile-link">{{ comment['comment_username'] }}</a>:
                 <div class="comment-button-container">
                     {% if comment['replytoid'] %}
                         <!-- Comment is already part of a thread; reply to the same thread -->
-                        <a href="/comment?songid={{ comment['songid'] }}&replytoid={{ comment['replytoid'] }}">Reply</a>
+                        <a href="/comment?threadid={{ comment['threadid'] }}&replytoid={{ comment['replytoid'] }}">Reply</a>
                     {% else %}
                         <!-- Comment is a top-level, reply to the comment -->
-                        <a href="/comment?songid={{ comment['songid'] }}&replytoid={{ comment['commentid'] }}">Reply</a>
+                        <a href="/comment?threadid={{ comment['threadid'] }}&replytoid={{ comment['commentid'] }}">Reply</a>
                     {% endif %}
                 </div>
             </div>
diff --git a/templates/comment-thread.html b/templates/comment-thread.html
new file mode 100644 (file)
index 0000000..6ac737d
--- /dev/null
@@ -0,0 +1,59 @@
+{% macro comment_thread(threadid, current_userid, thread_userid, comments) %}
+    <div class="comment-thread">
+        {% if current_userid %}
+        <a href="/comment?threadid={{ threadid }}" class="song-list-button" title="Add a Comment"><img class="lsp_btn_add02" /></a>
+        {% endif %}
+
+        {% for comment in comments %}
+        <div class="top-level-comment">
+
+            <a href="/users/{{ comment['username'] }}" class="profile-link">{{ comment['username'] }}</a>:
+            {{ (comment['content'].replace("\n", "<br>"))|safe }}
+
+            {% if current_userid == comment['userid'] or current_userid == thread_userid %}
+            <div class="comment-button-container">
+                <!-- Only commenter can edit comment -->
+                {% if current_userid == comment['userid'] %}
+                <a href="/comment?commentid={{ comment['commentid'] }}&threadid={{ threadid }}" class="song-list-button" title="Edit">
+                    <img class="lsp_btn_edit02" />
+                </a>
+                {% endif %}
+
+                <!-- Commenter and content owner can delete comment -->
+                <a href="/delete-comment/{{ comment['commentid'] }}" onclick="return confirm(&#34;Are you sure you want to delete this comment?&#34;)" class="song-list-button" title="Delete">
+                    <img class="lsp_btn_delete02" />
+                </a>
+            </div>
+            {% endif %}
+
+            {% for reply in comment['replies'] %}
+            <div class="reply-comment">
+
+                <a href="/users/{{ reply['username'] }}" class="profile-link">{{ reply['username'] }}</a>:
+                {{ reply['content'] }}
+
+                {% if current_userid == reply['userid'] or current_userid == thread_userid %}
+                <div class="comment-button-container">
+                    <!-- Only commenter can edit comment -->
+                    {% if current_userid == reply['userid'] %}
+                    <a href="/comment?commentid={{ reply['commentid'] }}&threadid={{ threadid }}&replytoid={{ comment['commentid'] }}" class="song-list-button" title="Edit">
+                        <img class="lsp_btn_edit02" />
+                    </a>
+                    {% endif %}
+
+                    <!-- Commenter and content owner can delete comment -->
+                    <a href="/delete-comment/{{ reply['commentid'] }}" onclick="return confirm(&#34;Are you sure you want to delete this comment?&#34;)" class="song-list-button" title="delete">
+                        <img class="lsp_btn_delete02" />
+                    </a>
+                </div>
+                {% endif %}
+            </div>
+            {% endfor %}
+
+            <div class="comment-button-container">
+                <a href="/comment?threadid={{ threadid }}&replytoid={{ comment['commentid'] }}">Reply</a>
+            </div>
+        </div>
+        {% endfor %}
+    </div>
+{% endmacro %}
index 20ec14311df4784047cc870310ec8587e2133bbb..f7dff237f20ed29c28dc387bc1efc9ab62506421 100644 (file)
@@ -1,3 +1,4 @@
+{% from "comment-thread.html" import comment_thread %}
 {% extends "base.html" %}
 
 {% block title %}{{ name }}{% endblock %}
@@ -151,4 +152,7 @@ function hidePlaylistEditor() {
 <p>This playlist doesn't have any songs yet.  To add songs to the playlist, expand song details and use the "Add to Playlist..." dropdown.</p>
 {%- endif %}
 
+<h2>Comments</h2>
+{{ comment_thread(threadid, session['userid'], userid, comments) }}
+
 {%- endblock %}
index f949463b03ed0f1272a6d38b799f0c7cd0a2e9ad..43747f935744d5ec4b87892d8c04e7f7ee0495db 100644 (file)
@@ -1,3 +1,4 @@
+{% from "comment-thread.html" import comment_thread %}
 {% extends "base.html" %}
 
 {% block title %}{{ name }}'s profile{% endblock %}
 
 {% endif %}
 
+<h2>Comments</h2>
+{{ comment_thread(threadid, session['userid'], userid, comments) }}
+
 {% endblock %}
 
index c71b5d9b0f49cc0c1b74ebb3bd8313fb220d7d5d..97114401aec657a804b966151fcd5878a04de52a 100644 (file)
@@ -1,3 +1,5 @@
+{% from "comment-thread.html" import comment_thread %}
+
 {% macro song_artist(song) %}
 <span class="song-artist">
     <a href="/users/{{ song.username }}" class="profile-link">{{ song.username }}</a>
     </div>
 
     <!-- Song Comments -->
-    <div class="song-comments">
-        Comments:<br>
-        {% if session['userid'] %}
-        <a href="/comment?songid={{ song.songid }}" class="song-list-button" title="Add a Comment"><img class="lsp_btn_add02" /></a>
-        {% endif %}
-
-        {% for comment in song.get_comments() %}
-        <div class="top-level-comment">
-
-            <a href="/users/{{ comment['username'] }}" class="profile-link">{{ comment['username'] }}</a>:
-            {{ (comment['content'].replace("\n", "<br>"))|safe }}
-
-            {% if session['userid'] == comment['userid'] or session['userid'] == song.userid %}
-            <div class="comment-button-container">
-            {% endif %}
-
-                <!-- Only commenter can edit comment -->
-                {% if session['userid'] == comment['userid'] %}
-                <a href="/comment?commentid={{ comment['commentid'] }}&songid={{ song.songid }}" class="song-list-button" title="Edit">
-                    <img class="lsp_btn_edit02" />
-                </a>
-                {% endif %}
-
-                <!-- Commenter and song owner can delete comment -->
-                {% if session['userid'] == comment['userid'] or session['userid'] == song.userid %}
-                <a href="/delete-comment/{{ comment['commentid'] }}" onclick="return confirm(&#34;Are you sure you want to delete this comment?&#34;)" class="song-list-button" title="Delete">
-                    <img class="lsp_btn_delete02" />
-                </a>
-                {% endif %}
-
-            {% if session['userid'] == comment['userid'] or session['userid'] == song.userid %}
-            </div>
-            {% endif %}
-
-            {% for reply in comment['replies'] %}
-            <div class="reply-comment">
-
-                <a href="/users/{{ reply['username'] }}" class="profile-link">{{ reply['username'] }}</a>:
-                {{ reply['content'] }}
-
-                {% if session['userid'] == reply['userid'] or session['userid'] == song.userid %}
-                <div class="comment-button-container">
-                {% endif %}
-
-                    <!-- Only commenter can edit comment -->
-                    {% if session['userid'] == reply['userid'] %}
-                    <a href="/comment?commentid={{ reply['commentid'] }}&songid={{ song.songid }}&replytoid={{ comment['commentid'] }}" class="song-list-button" title="Edit">
-                        <img class="lsp_btn_edit02" />
-                    </a>
-                    {% endif %}
-
-                    <!-- Commenter and song owner can delete comment -->
-                    {% if session['userid'] == reply['userid'] or session['userid'] == song.userid %}
-                    <a href="/delete-comment/{{ reply['commentid'] }}" onclick="return confirm(&#34;Are you sure you want to delete this comment?&#34;)" class="song-list-button" title="delete">
-                        <img class="lsp_btn_delete02" />
-                    </a>
-                    {% endif %}
-
-                {% if session['userid'] == reply['userid'] or session['userid'] == song.userid %}
-                </div>
-                {% endif %}
-            </div>
-            {% endfor %}
-
-            <div class="comment-button-container">
-                <a href="/comment?songid={{ song.songid }}&replytoid={{ comment['commentid'] }}">Reply</a>
-            </div>
-        </div>
-        {% endfor %}
-    </div>
+    <strong>Comments</strong>
+    {{ comment_thread(song.threadid, session["userid"], song.userid, song.get_comments()) }}
 </div>
 {% endmacro %}
index 88546a7656b73c78c0a4d62ddc3e8650b1c573df..ed33f2a2aa30a88170247bd7f8af560b357095fe 100644 (file)
--- a/todo.txt
+++ b/todo.txt
@@ -2,6 +2,7 @@ SOON
 - Comments on profile(?), playlists (schema is hard :( )
 - Generalize nofications
 - Userid in thread for owner? (to allow comment deletion)
+- Fix: refresh -> navigate -> back shows pre-refresh page
 - Player minimize button
 - Break up main.py, test_offline.py
 - Image support in comments, descriptions, bios, etc. (rich text?)