diff --git a/.gitignore b/.gitignore index e43b0f98..a0f3a019 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,11 @@ .DS_Store +# Virtual environments +venv/ +.venv/ +env/ +.env/ +ENV/ + +# Python cache files +__pycache__/ +*.pyc diff --git a/backend/data/blooms.py b/backend/data/blooms.py index 7e280cf3..c9eb7f29 100644 --- a/backend/data/blooms.py +++ b/backend/data/blooms.py @@ -15,6 +15,14 @@ class Bloom: sent_timestamp: datetime.datetime +class Rebloom: + id: int + resender: User + sender: User + content: str + sent_timestamp: datetime.datetime + rebloomed: int + def add_bloom(*, sender: User, content: str) -> Bloom: hashtags = [word[1:] for word in content.split(" ") if word.startswith("#")] @@ -37,6 +45,28 @@ def add_bloom(*, sender: User, content: str) -> Bloom: ) +def rebloom(*, rebloom_id: int, resender: User, sender: User, content: str, sent_timestamp: datetime.datetime) -> Rebloom: + + with db_cursor() as cur: + cur.execute( + "INSERT INTO reblooms (id, resender_id, original_sender_id, content, send_timestamp, times_rebloomed) VALUES (%(rebloom_id)s, %(resender_id)s, %(sender_id)s, %(content)s, %(timestamp)s, 1) ON CONFLICT (resender_id, original_sender_id, content) DO UPDATE SET times_rebloomed = times_rebloomed + 1", + dict( + rebloom_id=rebloom_id, + resender_name=resender.username, + sender_name=sender, + content=content, + timestamp=sent_timestamp + ), + ) + return Rebloom( + id=rebloom_id, + resender=resender, + sender=sender, + content=content, + sent_timestamp=sent_timestamp, + rebloomed=1 + ) + def get_blooms_for_user( username: str, *, before: Optional[int] = None, limit: Optional[int] = None ) -> List[Bloom]: @@ -50,7 +80,7 @@ def get_blooms_for_user( else: before_clause = "" - limit_clause = make_limit_clause(limit, kwargs) + #limit_clause = make_limit_clause(limit, kwargs) cur.execute( f"""SELECT @@ -60,8 +90,7 @@ def get_blooms_for_user( WHERE username = %(sender_username)s {before_clause} - ORDER BY send_timestamp DESC - {limit_clause} + """, kwargs, ) @@ -79,6 +108,33 @@ def get_blooms_for_user( ) return blooms +def get_reblooms_for_user(username: str) -> List[Rebloom]: + with db_cursor() as cur: + cur.execute( + """SELECT + reblooms.id, users.username, original_sender_name, content, send_timestamp, times_rebloomed + FROM + reblooms INNER JOIN users ON users.username = reblooms.resender_name + WHERE + users.username = %(username)s + """, + {"username": username}, + ) + rows = cur.fetchall() + reblooms = [] + for row in rows: + rebloom_id, resender_name, sender_name, content, timestamp, times_rebloomed = row + reblooms.append( + Rebloom( + id=rebloom_id, + resender=resender_name, + sender=sender_name, + content=content, + sent_timestamp=timestamp, + rebloomed=times_rebloomed + ) + ) + return reblooms def get_bloom(bloom_id: int) -> Optional[Bloom]: with db_cursor() as cur: diff --git a/backend/endpoints.py b/backend/endpoints.py index 0e177a07..7012a203 100644 --- a/backend/endpoints.py +++ b/backend/endpoints.py @@ -111,11 +111,14 @@ def other_profile(profile_username): followers = get_inverse_followed_usernames(profile_user) all_blooms = blooms.get_blooms_for_user(profile_username) - all_blooms.reverse() + all_reblooms = blooms.get_reblooms_for_user(profile_username) + + bloom_data = all_blooms + all_reblooms + bloom_data.reverse() return jsonify( { "username": profile_username, - "recent_blooms": all_blooms[:10], + "recent_blooms": bloom_data[:10], "follows": get_followed_usernames(profile_user), "followers": list(followers), "is_following": current_user is not None @@ -166,6 +169,26 @@ def send_bloom(): } ) +@jwt_required() +def send_rebloom(): + type_check_error = verify_request_fields({"content": str}) + if type_check_error is not None: + return type_check_error + + user = get_current_user() + + if user.username == request.json["sender"]: + return make_response((f"Cannot rebloom own bloom", 422)) + + print(request.json["sender"]) + + blooms.rebloom(rebloom_id=request.json["id"], resender=user, sender=request.json["sender"], content=request.json["content"], sent_timestamp=request.json["sent_timestamp"]) + + return jsonify( + { + "success": True, + } + ) def get_bloom(id_str): try: @@ -211,6 +234,10 @@ def user_blooms(profile_username): user_blooms.reverse() return jsonify(user_blooms) +def user_reblooms(profile_username): + user_reblooms = blooms.get_reblooms_for_user(profile_username) + user_reblooms.reverse() + return jsonify(user_reblooms) @jwt_required() def suggested_follows(limit_str): diff --git a/backend/main.py b/backend/main.py index 7ba155fa..6d2c5629 100644 --- a/backend/main.py +++ b/backend/main.py @@ -12,8 +12,10 @@ register, self_profile, send_bloom, + send_rebloom, suggested_follows, user_blooms, + user_reblooms, ) from dotenv import load_dotenv @@ -59,6 +61,8 @@ def main(): app.add_url_rule("/bloom", methods=["POST"], view_func=send_bloom) app.add_url_rule("/bloom/", methods=["GET"], view_func=get_bloom) app.add_url_rule("/blooms/", view_func=user_blooms) + app.add_url_rule("/rebloom", methods=["POST"], view_func=send_rebloom) + app.add_url_rule("/reblooms/", view_func=user_reblooms) app.add_url_rule("/hashtag/", view_func=hashtag) app.run(host="0.0.0.0", port="3000", debug=True) diff --git a/db/schema.sql b/db/schema.sql index 61e7580c..5b598cd3 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -13,6 +13,15 @@ CREATE TABLE blooms ( send_timestamp TIMESTAMP NOT NULL ); +CREATE TABLE reblooms ( + id BIGSERIAL NOT NULL PRIMARY KEY, + resender_name TEXT NOT NULL, + original_sender_name TEXT NOT NULL, + content TEXT NOT NULL, + send_timestamp TIMESTAMP NOT NULL, + times_rebloomed INT NOT NULL +); + CREATE TABLE follows ( id SERIAL PRIMARY KEY, follower INT NOT NULL REFERENCES users(id), diff --git a/front-end/components/bloom.mjs b/front-end/components/bloom.mjs index 0b4166c3..39f2c415 100644 --- a/front-end/components/bloom.mjs +++ b/front-end/components/bloom.mjs @@ -1,3 +1,6 @@ +import { apiService } from "../lib/api.mjs" +import { state } from "../lib/state.mjs" + /** * Create a bloom component * @param {string} template - The ID of the template to clone @@ -20,6 +23,8 @@ const createBloom = (template, bloom) => { const bloomTime = bloomFrag.querySelector("[data-time]"); const bloomTimeLink = bloomFrag.querySelector("a:has(> [data-time])"); const bloomContent = bloomFrag.querySelector("[data-content]"); + const rebloomButton = bloomFrag.querySelector("[data-rebloom]") + const timesRebloomedCounter = bloomFrag.querySelector("[data-times-rebloomed]") bloomArticle.setAttribute("data-bloom-id", bloom.id); bloomUsername.setAttribute("href", `/profile/${bloom.sender}`); @@ -30,7 +35,16 @@ const createBloom = (template, bloom) => { ...bloomParser.parseFromString(_formatHashtags(bloom.content), "text/html") .body.childNodes ); - + rebloomButton.hidden = state.currentUser === bloom.sender + rebloomButton.addEventListener("click", async () => { + try { + await apiService.postRebloom(bloom.id, bloom.sender, bloom.content, bloom.sent_timestamp); + } catch (error) { + throw error; + } + }); + timesRebloomedCounter.hidden = !bloom.rebloomed; + timesRebloomedCounter.textContent = `Times rebloomed: ${bloom.rebloomed}` return bloomFrag; }; @@ -84,4 +98,4 @@ function _formatTimestamp(timestamp) { } } -export {createBloom}; +export { createBloom }; diff --git a/front-end/components/error.mjs b/front-end/components/error.mjs index 3fc9eeee..80ef1946 100644 --- a/front-end/components/error.mjs +++ b/front-end/components/error.mjs @@ -7,6 +7,7 @@ const _STATUS_MESSAGES = { 404: "Not Found - The requested resource does not exist.", 405: "Not Allowed - The server knows the request method, but the target resource doesn't support this method.", 418: "I'm a teapot - Server refuses to brew coffee with a teapot.", + 422: "Invalid data - The request was well-formed but was unable to be followed due to semantic errors.", 500: "Internal Server Error - Something went wrong on the server.", }; diff --git a/front-end/index.html b/front-end/index.html index 89d6b130..dca15e76 100644 --- a/front-end/index.html +++ b/front-end/index.html @@ -239,6 +239,8 @@

Share a Bloom

+

+ diff --git a/front-end/lib/api.mjs b/front-end/lib/api.mjs index f4b5339b..e84acb7d 100644 --- a/front-end/lib/api.mjs +++ b/front-end/lib/api.mjs @@ -213,6 +213,24 @@ async function postBloom(content) { } // ======= USER methods +async function postRebloom(id, sender, content, sent_timestamp) { + try { + const data = await _apiRequest("/rebloom", { + method: "POST", + body: JSON.stringify({id, sender, content, sent_timestamp}), + }); + + if (data.success) { + await getBlooms(); + await getProfile(state.currentUser); + } + + return data; + } catch (error) { + + return {success: false}; + } +} async function getProfile(username) { const endpoint = username ? `/profile/${username}` : "/profile"; @@ -291,6 +309,7 @@ const apiService = { getBloom, getBlooms, postBloom, + postRebloom, getBloomsByHashtag, // User methods