From 0b9fa8d7c31e38ea0dae66500784a3e3b8bb4b16 Mon Sep 17 00:00:00 2001 From: mikiyas-stp Date: Sat, 27 Sep 2025 14:56:29 +0100 Subject: [PATCH 01/10] version of psycopg2 changed --- backend/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/requirements.txt b/backend/requirements.txt index e03836c..5e11b05 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -11,7 +11,7 @@ idna==3.10 itsdangerous==2.2.0 Jinja2==3.1.5 MarkupSafe==3.0.2 -psycopg2==2.9.10 +psycopg2-binary==2.9.10 pycparser==2.22 PyJWT==2.10.1 python-dotenv==1.0.1 From 7f952beaae05ad5c6694b9f756e3baeb139d0da6 Mon Sep 17 00:00:00 2001 From: Mikiyas-STP Date: Sat, 18 Oct 2025 16:35:53 +0100 Subject: [PATCH 02/10] =?UTF-8?q?Goal=20was=20to=20fix:=20Bug=20Report:=20?= =?UTF-8?q?Can't=20log=20in=20from=20profile=20page=20=F0=9F=94=97=20Clone?= =?UTF-8?q?=20Bug=20Report:=20Can't=20log=20in=20from=20profile=20page=20?= =?UTF-8?q?=F0=9F=94=97=20Steps=20to=20reproduce:?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Load site with sample data populated. Log in as sample / sosecret Click on a username (e.g. AS) in a bloom shown in the timeline Click logout Log in as sample / sosecret Expect to see: A logged in view of AS’s profile, with a “Logout” button in the top-right. Actually see: A 501 error page saying “Server does not support this operation.” :gear: bug DONE now --- front-end/views/profile.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/front-end/views/profile.mjs b/front-end/views/profile.mjs index dd2b92a..31139a9 100644 --- a/front-end/views/profile.mjs +++ b/front-end/views/profile.mjs @@ -39,8 +39,8 @@ function profileView(username) { createLogin ); document - .querySelector("[data-action='login']") - ?.addEventListener("click", handleLogin); + .querySelector("[data-form='login']") + ?.addEventListener("submit", handleLogin); const profileData = state.profiles.find((p) => p.username === username); if (profileData) { From ca53dc4c1999c99467eb7a7d702187bc001bab40 Mon Sep 17 00:00:00 2001 From: Mikiyas-STP Date: Sat, 18 Oct 2025 18:51:22 +0100 Subject: [PATCH 03/10] Bug Report: Extra long blooms? Blooms are meant to be limited to 280 characters! But AS has a bloom which is longer than that! Not fair! => This bug is fixed now --- backend/endpoints.py | 14 +++++++++++++- backend/populate.py | 3 +-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/backend/endpoints.py b/backend/endpoints.py index 0e177a0..1657625 100644 --- a/backend/endpoints.py +++ b/backend/endpoints.py @@ -157,8 +157,20 @@ def send_bloom(): return type_check_error user = get_current_user() + #Extract content safely + content = request.json.get("content", "") + # Limit content length + MAX_BLOOM_LENGTH = 280 + if len(content) > MAX_BLOOM_LENGTH: + return make_response( + { + "success": False, + "message": f"Bloom cannot exceed {MAX_BLOOM_LENGTH} characters", + }, + 400, + ) - blooms.add_bloom(sender=user, content=request.json["content"]) + blooms.add_bloom(sender=user, content=content) return jsonify( { diff --git a/backend/populate.py b/backend/populate.py index 414218b..8ca3293 100644 --- a/backend/populate.py +++ b/backend/populate.py @@ -64,8 +64,7 @@ def main(): writer_access_token = create_user("AS", "neverSt0pTalking") send_bloom( writer_access_token, - "In this essay I will convince you that my views are correct in ways you have never imagined. If it doesn't change your life, read it again. Marshmallows are magnificent. They have great squish, tasty good, and you can even toast them over a fire. Toast them just right until they have a tiny bit of crunch when you bite into them, and have just started melting in the middle.", - ) + "In this essay I will convince you that my views are correct in ways you have never imagined. Marshmallows are magnificent!" ) justsomeguy_access_token = create_user("JustSomeGuy", "mysterious") send_bloom(justsomeguy_access_token, "Hello.") From ab5a19463062773974a567f64e0df621a6edbcb1 Mon Sep 17 00:00:00 2001 From: Mikiyas-STP Date: Sat, 18 Oct 2025 19:04:13 +0100 Subject: [PATCH 04/10] =?UTF-8?q?swizbiz=20love=20regex=20debugging=20:=20?= =?UTF-8?q?for=20the=20bug=20report=20Bug=20Report:=20Hashtag=20link=20doe?= =?UTF-8?q?sn't=20work=20correctly,=20When=20loading=20the=20populated=20d?= =?UTF-8?q?ata,=20the=20user=20Swiz=E2=80=99s=20second=20bloom=20(which=20?= =?UTF-8?q?reads=20=E2=80=9CLet=E2=80=99s=20get=20some=20#SwizBiz=20love!!?= =?UTF-8?q?=E2=80=9D)=20has=20weird=20hashtag=20links.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If you click the word “#SwizBiz” it takes you to an empty page which doesn’t show any blooms. I would expect at least the bloom I clicked the link in to be shown, and also perhaps any others containing the hashtag. On the other hand, in their other bloom (“New album dropping at midnight! Pre-save now! #SwizBiz”), clicking on “#SwizBiz” does take you to exactly what I expect - a page showing blooms containing #SwizBiz including the one I clicked from. --- front-end/components/bloom.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front-end/components/bloom.mjs b/front-end/components/bloom.mjs index 0b4166c..8a75cc3 100644 --- a/front-end/components/bloom.mjs +++ b/front-end/components/bloom.mjs @@ -37,7 +37,7 @@ const createBloom = (template, bloom) => { function _formatHashtags(text) { if (!text) return text; return text.replace( - /\B#[^#]+/g, + /(^|\s)(#[\w]+)/g, (match) => `${match}` ); } From 679d9a067536a6a762ed217b0174e254fbc9b0f9 Mon Sep 17 00:00:00 2001 From: Mikiyas-STP Date: Sat, 18 Oct 2025 19:30:19 +0100 Subject: [PATCH 05/10] fix for Bug Report: Hashtag slowing down my browser: added a condition for checking if the current hashtag is there we dont need to fetch its own data. makes sense --- front-end/views/hashtag.mjs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/front-end/views/hashtag.mjs b/front-end/views/hashtag.mjs index 7b7e996..3e51bad 100644 --- a/front-end/views/hashtag.mjs +++ b/front-end/views/hashtag.mjs @@ -15,6 +15,13 @@ import {createHeading} from "../components/heading.mjs"; // Hashtag view: show all tweets containing this tag function hashtagView(hashtag) { + //only fetch data from the server and avoid the flashing on slow network + if (state.currentHashtag !== hashtag) { + state.currentHashtag = hashtag; + state.hashtagBlooms = []; + apiService.getBloomsByHashtag(hashtag); + } + //this one if statement fixes the flashing behavior by checking if the hashtag selected is already running or not destroy(); apiService.getBloomsByHashtag(hashtag); From 6a909f00270994289d886703b2d1c775ebb3ddec Mon Sep 17 00:00:00 2001 From: mikiyas-stp Date: Wed, 24 Dec 2025 19:12:31 +0000 Subject: [PATCH 06/10] endpoint: unfollow method --- backend/endpoints.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/backend/endpoints.py b/backend/endpoints.py index 1657625..67a075f 100644 --- a/backend/endpoints.py +++ b/backend/endpoints.py @@ -1,6 +1,6 @@ from typing import Dict, Union from data import blooms -from data.follows import follow, get_followed_usernames, get_inverse_followed_usernames +from data.follows import follow, get_followed_usernames, get_inverse_followed_usernames, unfollow from data.users import ( UserRegistrationError, get_suggested_follows, @@ -149,6 +149,22 @@ def do_follow(): } ) +@jwt_required() +def do_unfollow(unfollow_username): + current_user = get_current_user() + unfollow_user = get_user(unfollow_username) + + if unfollow_user is None: + return make_response( + (f"Cannot unfollow {unfollow_username} - user does not exist", 404) + ) + + unfollow(current_user, unfollow_user) + return jsonify( + { + "success": True, + } + ) @jwt_required() def send_bloom(): @@ -257,3 +273,4 @@ def verify_request_fields(names_to_types: Dict[str, type]) -> Union[Response, No ) ) return None + From a55043ebf61bb41608d7c0be0cf6891093526f7d Mon Sep 17 00:00:00 2001 From: mikiyas-stp Date: Wed, 24 Dec 2025 19:13:59 +0000 Subject: [PATCH 07/10] post endpoint for unfollow --- backend/data/follows.py | 7 +++++++ backend/main.py | 2 ++ front-end/components/profile.mjs | 36 ++++++++++++++++++++++++++++---- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/backend/data/follows.py b/backend/data/follows.py index a4b6314..93494f7 100644 --- a/backend/data/follows.py +++ b/backend/data/follows.py @@ -20,6 +20,13 @@ def follow(follower: User, followee: User): # Already following - treat as idempotent request. pass +def unfollow(follower: User, followee: User): + """Remove a follow relationship between two users.""" + with db_cursor() as cur: + cur.execute( + "DELETE FROM follows WHERE follower = %s AND followee = %s", + (follower.id, followee.id), + ) def get_followed_usernames(follower: User) -> List[str]: """get_followed_usernames returns a list of usernames followee follows.""" diff --git a/backend/main.py b/backend/main.py index 7ba155f..a69c9e9 100644 --- a/backend/main.py +++ b/backend/main.py @@ -4,6 +4,7 @@ from data.users import lookup_user from endpoints import ( do_follow, + do_unfollow, get_bloom, hashtag, home_timeline, @@ -54,6 +55,7 @@ def main(): app.add_url_rule("/profile", view_func=self_profile) app.add_url_rule("/profile/", view_func=other_profile) app.add_url_rule("/follow", methods=["POST"], view_func=do_follow) + app.add_url_rule("/unfollow/", methods=["POST"], view_func=do_unfollow) app.add_url_rule("/suggested-follows/", view_func=suggested_follows) app.add_url_rule("/bloom", methods=["POST"], view_func=send_bloom) diff --git a/front-end/components/profile.mjs b/front-end/components/profile.mjs index ec4f200..098493f 100644 --- a/front-end/components/profile.mjs +++ b/front-end/components/profile.mjs @@ -27,8 +27,16 @@ function createProfile(template, {profileData, whoToFollow, isLoggedIn}) { followerCountEl.textContent = profileData.followers?.length || 0; followingCountEl.textContent = profileData.follows?.length || 0; followButtonEl.setAttribute("data-username", profileData.username || ""); - followButtonEl.hidden = profileData.is_self || profileData.is_following; - followButtonEl.addEventListener("click", handleFollow); + + // Set button text and action based on follow status + if (profileData.is_following) { + followButtonEl.textContent = "Unfollow"; + followButtonEl.addEventListener("click", handleUnfollow); + } else { + followButtonEl.textContent = "Follow"; + followButtonEl.addEventListener("click", handleFollow); + } + if (!isLoggedIn) { followButtonEl.style.display = "none"; } @@ -43,7 +51,17 @@ function createProfile(template, {profileData, whoToFollow, isLoggedIn}) { usernameLink.setAttribute("href", `/profile/${userToFollow.username}`); const followButton = wtfElement.querySelector("button"); followButton.setAttribute("data-username", userToFollow.username); - followButton.addEventListener("click", handleFollow); + + if (userToFollow.is_following) { + followButton.textContent = "Unfollow"; + followButton.addEventListener("click", handleUnfollow); + } + else { + followButton.textContent = "Follow"; + followButton.addEventListener("click", handleFollow); + } + + if (!isLoggedIn) { followButton.style.display = "none"; } @@ -66,4 +84,14 @@ async function handleFollow(event) { await apiService.getWhoToFollow(); } -export {createProfile, handleFollow}; +async function handleUnfollow(event) { + const button = event.target; + const username = button.getAttribute("data-username"); + if (!username) return; + + await apiService.unfollowUser(username); + await apiService.getWhoToFollow(); +} + + +export {createProfile, handleFollow, handleUnfollow}; From 8f666a5cf4574bfcd1aaadf11380a8beb00cfd28 Mon Sep 17 00:00:00 2001 From: mikiyas-stp Date: Wed, 24 Dec 2025 19:18:48 +0000 Subject: [PATCH 08/10] handling unfollow --- front-end/components/profile.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front-end/components/profile.mjs b/front-end/components/profile.mjs index 098493f..f994f9f 100644 --- a/front-end/components/profile.mjs +++ b/front-end/components/profile.mjs @@ -83,7 +83,7 @@ async function handleFollow(event) { await apiService.followUser(username); await apiService.getWhoToFollow(); } - +//unfollow handler async function handleUnfollow(event) { const button = event.target; const username = button.getAttribute("data-username"); From 7de7acc21fc2d3811bf9f9be531a7d3347b29828 Mon Sep 17 00:00:00 2001 From: mikiyas-stp Date: Wed, 24 Dec 2025 19:19:50 +0000 Subject: [PATCH 09/10] follow button working based on status of following --- front-end/components/profile.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front-end/components/profile.mjs b/front-end/components/profile.mjs index f994f9f..da2d6ea 100644 --- a/front-end/components/profile.mjs +++ b/front-end/components/profile.mjs @@ -40,7 +40,7 @@ function createProfile(template, {profileData, whoToFollow, isLoggedIn}) { if (!isLoggedIn) { followButtonEl.style.display = "none"; } - +//follow button based on condition if (whoToFollow.length > 0) { const whoToFollowList = whoToFollowContainer.querySelector("[data-who-to-follow]"); const whoToFollowTemplate = document.querySelector("#who-to-follow-chip"); From 85bd4b8c6ced4ce922e1c17926f9cf132f0b7c49 Mon Sep 17 00:00:00 2001 From: mikiyas-stp Date: Wed, 24 Dec 2025 20:19:37 +0000 Subject: [PATCH 10/10] update rebloomcounter and rebloom endpoints/methods --- backend/endpoints.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/backend/endpoints.py b/backend/endpoints.py index 67a075f..2844a62 100644 --- a/backend/endpoints.py +++ b/backend/endpoints.py @@ -233,12 +233,40 @@ def home_timeline(): return jsonify(sorted_blooms) - def user_blooms(profile_username): user_blooms = blooms.get_blooms_for_user(profile_username) user_blooms.reverse() return jsonify(user_blooms) +def update_rebloom_counter(bloom_id): + try: + id_int = int(bloom_id) + except ValueError: + return make_response((f"Invalid bloom id", 400)) + blooms.update_rebloom_counter(id_int) + return jsonify( + { + "success": True, + } + ) + +@jwt_required() +def send_rebloom(): + user = get_current_user() + bloom_id = request.json["id"] + try: + id_int = int(bloom_id) + except ValueError: + return make_response((f"Invalid bloom id", 400)) + blooms.add_rebloom(sender=user, id=id_int) + + return jsonify( + { + "success": True, + } + ) + + @jwt_required() def suggested_follows(limit_str):