diff --git a/CHANGES-MADE.md b/CHANGES-MADE.md new file mode 100644 index 00000000..a3911ade --- /dev/null +++ b/CHANGES-MADE.md @@ -0,0 +1,39 @@ +# Project Log: PurpleForest - Un-follow Feature Implementation + +## 1. Feature Overview +Developed the "Un-follow" functionality to allow users to stop seeing blooms from specific accounts. This required building a "Reverse Path" that mirrors the "Follow" logic but executes a deletion in the database. + +--- + +## 2. Structural Changes + +### A. Database Layer (The Memory) +* **Logic:** Implemented the removal of data using the SQL `DELETE` command. +* **Precision:** Used a `WHERE` clause targeting both the `follower` and the `followee` IDs. +* **The "Why":** This ensures the operation only removes the specific relationship between two users without accidentally deleting all follows for a single user. + +### B. Backend Layer (The Brain) +* **Data Logic (`data/follows.py`):** + * Created the `unfollow` function. + * Used **parameterized queries** with a dictionary to safely pass user IDs to the database driver. +* **API Endpoint (`endpoints.py`):** + * Developed the `do_unfollow` controller. + * **Contract Matching:** Designed the function to accept `target_username` directly from the URL path to stay consistent with the existing Frontend API service. + * **Security:** Applied the `@jwt_required` middleware to ensure only the authenticated account owner can modify their following list. +* **Routing (`main.py`):** Registered the new `/unfollow/` route and imported the necessary logic from the endpoints module. + +### C. Frontend Layer (The Face) +* **Visibility Logic (`profile.mjs`):** + * Modified the `createProfile` component to prevent the button from being hidden when a user is already followed. + * **The Toggle:** Implemented an `if/else` block to dynamically change the button text between "Follow" and "Un-follow" based on the `is_following` state. +* **Event Handling (`profile.mjs`):** + * Upgraded the `handleFollow` function into a **"Smart Switch."** + * **The Logic:** The handler now inspects the current text of the button. If it says "Un-follow," it calls the `unfollowUser` API; otherwise, it calls the standard `followUser` API. + +--- + +## 4. Verification & Testing +* **Action Verification:** Confirmed that clicking "Follow" changes the UI to "Un-follow" and vice-versa. +* **Data Integrity:** Used **DBeaver** to verify that the corresponding row in the `follows` table is physically removed upon clicking "Un-follow." +* **Social Feed Test:** Verified that un-following a user immediately stops their posts from appearing in the Home timeline upon the next refresh. + diff --git a/backend/data/follows.py b/backend/data/follows.py index a4b6314e..5f31376a 100644 --- a/backend/data/follows.py +++ b/backend/data/follows.py @@ -20,6 +20,15 @@ def follow(follower: User, followee: User): # Already following - treat as idempotent request. pass +def unfollow(follower: User, followee: User): + with db_cursor() as cur: + cur.execute( + "DELETE FROM follows WHERE follower = %(follower_id)s AND followee = %(followee_id)s", + dict( + follower_id=follower.id, + followee_id=followee.id, + ), + ) def get_followed_usernames(follower: User) -> List[str]: """get_followed_usernames returns a list of usernames followee follows.""" @@ -41,3 +50,4 @@ def get_inverse_followed_usernames(followee: User) -> List[str]: ) rows = cur.fetchall() return [row[0] for row in rows] + diff --git a/backend/endpoints.py b/backend/endpoints.py index 0e177a07..efcc73b9 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, unfollow, get_followed_usernames, get_inverse_followed_usernames from data.users import ( UserRegistrationError, get_suggested_follows, @@ -149,6 +149,24 @@ def do_follow(): } ) +@jwt_required() +def do_unfollow(target_username): # Flask passes the name from the URL here + # 1. Identify the user who is logged in + current_user = get_current_user() + + # 2. Identify the user to be unfollowed + target_user = get_user(target_username) + + # 3. Handle if the target doesn't exist + if target_user is None: + return make_response( + (f"Cannot unfollow {target_username} - user does not exist", 404) + ) + + # 4. PERFORM THE ACTION (Calling your function from data/follows.py) + unfollow(current_user, target_user) + + return jsonify({"success": True}) @jwt_required() def send_bloom(): diff --git a/backend/main.py b/backend/main.py index 7ba155fa..3dde9f10 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 ec4f2009..4b768b3e 100644 --- a/front-end/components/profile.mjs +++ b/front-end/components/profile.mjs @@ -27,7 +27,11 @@ 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.hidden = profileData.is_self; + if (profileData.is_following) { + followButtonEl.textContent = "Un-follow" + } else {followButtonEl.textContent = "Follow"; + } followButtonEl.addEventListener("click", handleFollow); if (!isLoggedIn) { followButtonEl.style.display = "none"; @@ -62,7 +66,11 @@ async function handleFollow(event) { const username = button.getAttribute("data-username"); if (!username) return; - await apiService.followUser(username); + if (button.textContent === "Un-follow") { + await apiService.unfollowUser(username); + } else { + await apiService.followUser(username); + } await apiService.getWhoToFollow(); }