Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 91 additions & 58 deletions backend/data/blooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,39 @@ class Bloom:
sender: User
content: str
sent_timestamp: datetime.datetime
rebloom_id: Optional[int] = None #adding rebloom_id to our class
original_sender: Optional[str] = None
rebloom_count: int = 0


def add_bloom(*, sender: User, content: str) -> Bloom:
def add_bloom(*, sender: User, content: str,rebloom_id:Optional[int]=None) -> Bloom:
hashtags = [word[1:] for word in content.split(" ") if word.startswith("#")]

now = datetime.datetime.now(tz=datetime.UTC)
bloom_id = int(now.timestamp() * 1000000)
with db_cursor() as cur:
cur.execute(
"INSERT INTO blooms (id, sender_id, content, send_timestamp) VALUES (%(bloom_id)s, %(sender_id)s, %(content)s, %(timestamp)s)",
"INSERT INTO blooms (id, sender_id, content, send_timestamp,rebloom_id) VALUES (%(bloom_id)s, %(sender_id)s, %(content)s, %(timestamp)s,%(rebloom_id)s)",#updating the query with rebloom_id
dict(
bloom_id=bloom_id,
sender_id=sender.id,
content=content,
timestamp=datetime.datetime.now(datetime.UTC),
rebloom_id=rebloom_id,
),
)
for hashtag in hashtags:
cur.execute(
"INSERT INTO hashtags (hashtag, bloom_id) VALUES (%(hashtag)s, %(bloom_id)s)",
dict(hashtag=hashtag, bloom_id=bloom_id),
)
return Bloom(
id=bloom_id,
sender=sender.username,
content=content,
sent_timestamp=now,
rebloom_id=rebloom_id
)


def get_blooms_for_user(
Expand All @@ -50,52 +61,56 @@ def get_blooms_for_user(
else:
before_clause = ""

limit_clause = make_limit_clause(limit, kwargs)


cur.execute(
f"""SELECT
blooms.id, users.username, content, send_timestamp
FROM
blooms INNER JOIN users ON users.id = blooms.sender_id
b.id,
u.username,
b.content,
b.send_timestamp,
b.rebloom_id,
ou.username as original_sender,
(SELECT COUNT(*) FROM blooms WHERE rebloom_id = COALESCE(b.rebloom_id, b.id)) as rebloom_count
FROM blooms b
INNER JOIN users u ON u.id = b.sender_id
LEFT JOIN blooms ob ON b.rebloom_id = ob.id
LEFT JOIN users ou ON ob.sender_id = ou.id
WHERE
username = %(sender_username)s
u.username = %(sender_username)s
{before_clause}
ORDER BY send_timestamp DESC
{limit_clause}
ORDER BY b.send_timestamp DESC

""",
kwargs,
)
rows = cur.fetchall()
blooms = []
for row in rows:
bloom_id, sender_username, content, timestamp = row
blooms.append(
Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
)
)
return blooms
return map_rows_to_blooms(cur.fetchall())#gets all rows from the SQL query as a list of tuples,Each tuple-> map_rows_to_blooms()-> Bloom object.


def get_bloom(bloom_id: int) -> Optional[Bloom]:
with db_cursor() as cur:
#Rebloom support built-in the query
cur.execute(
"SELECT blooms.id, users.username, content, send_timestamp FROM blooms INNER JOIN users ON users.id = blooms.sender_id WHERE blooms.id = %s",
"""SELECT
b.id,
u.username,
b.content,
b.send_timestamp,
b.rebloom_id,
ou.username as original_sender,
(SELECT COUNT(*) FROM blooms WHERE rebloom_id = COALESCE(b.rebloom_id, b.id)) as rebloom_count
FROM blooms b
INNER JOIN users u ON u.id = b.sender_id
LEFT JOIN blooms ob ON b.rebloom_id = ob.id
LEFT JOIN users ou ON ob.sender_id = ou.id
WHERE b.id = %s""",
(bloom_id,),
)

row = cur.fetchone()
if row is None:
return None
bloom_id, sender_username, content, timestamp = row
return Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
)
return map_rows_to_blooms([row])[0]



def get_blooms_with_hashtag(
Expand All @@ -104,39 +119,57 @@ def get_blooms_with_hashtag(
kwargs = {
"hashtag_without_leading_hash": hashtag_without_leading_hash,
}
limit_clause = make_limit_clause(limit, kwargs)

with db_cursor() as cur:
cur.execute(
f"""SELECT
blooms.id, users.username, content, send_timestamp
FROM
blooms INNER JOIN hashtags ON blooms.id = hashtags.bloom_id INNER JOIN users ON blooms.sender_id = users.id
b.id,
u.username,
b.content,
b.send_timestamp,
b.rebloom_id,
ou.username as original_sender,
(SELECT COUNT(*) FROM blooms WHERE rebloom_id = COALESCE(b.rebloom_id, b.id)) as rebloom_count
FROM blooms b
INNER JOIN hashtags h ON b.id = h.bloom_id
INNER JOIN users u ON b.sender_id = u.id
LEFT JOIN blooms ob ON b.rebloom_id = ob.id
LEFT JOIN users ou ON ob.sender_id = ou.id
WHERE
hashtag = %(hashtag_without_leading_hash)s
ORDER BY send_timestamp DESC
{limit_clause}
h.hashtag = %(hashtag_without_leading_hash)s
ORDER BY b.send_timestamp DESC

""",
kwargs,
)
rows = cur.fetchall()
blooms = []
for row in rows:
bloom_id, sender_username, content, timestamp = row
blooms.append(
Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
)
)
return blooms
return map_rows_to_blooms(cur.fetchall())



def make_limit_clause(limit: Optional[int], kwargs: Dict[Any, Any]) -> str:
if limit is not None:
limit_clause = "LIMIT %(limit)s"
kwargs["limit"] = limit
else:
limit_clause = ""
return limit_clause
def map_rows_to_blooms(rows) -> List[Bloom]: #helper to Converts any SQL row tuple into a Bloom object.
blooms = []
for row in rows:
bloom_id, sender_username, content, timestamp, rebloom_id, original_sender, rebloom_count = row

blooms.append(
Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
rebloom_id=rebloom_id,
original_sender=original_sender,
rebloom_count=rebloom_count
)
)
return blooms
def has_user_rebloomed(original_bloom_id: int, user_id: int) -> bool:
"""
Return True if the user has already rebloomed this bloom.
"""
with db_cursor() as cur:
cur.execute(
"SELECT 1 FROM blooms WHERE rebloom_id = %s AND sender_id = %s",
(original_bloom_id, user_id)
)
return cur.fetchone() is not None
30 changes: 30 additions & 0 deletions backend/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,3 +245,33 @@ def verify_request_fields(names_to_types: Dict[str, type]) -> Union[Response, No
)
)
return None
@jwt_required()
def do_rebloom(bloom_id_str):

try:
bloom_id = int(bloom_id_str)
except ValueError:
return make_response(("Invalid bloom id", 400))


original_bloom = blooms.get_bloom(bloom_id) # fetch originalbloom
if original_bloom is None:
return make_response(("Bloom not found", 404))

current_user = get_current_user()


if blooms.has_user_rebloomed(bloom_id, current_user.id): #ensuring each user can only rebloom a bloom once.
return make_response(("Already rebloomed", 400))

new_bloom = blooms.add_bloom(
sender=current_user,
content=original_bloom.content,
rebloom_id=bloom_id
)

return jsonify({
"success": True,
"rebloom_id": new_bloom.id,
"original_bloom_id": bloom_id
})
2 changes: 2 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from endpoints import (
do_follow,
get_bloom,
do_rebloom,
hashtag,
home_timeline,
login,
Expand Down Expand Up @@ -58,6 +59,7 @@ def main():

app.add_url_rule("/bloom", methods=["POST"], view_func=send_bloom)
app.add_url_rule("/bloom/<id_str>", methods=["GET"], view_func=get_bloom)
app.add_url_rule("/bloom/<bloom_id_str>/rebloom", methods=["POST"], view_func=do_rebloom)
app.add_url_rule("/blooms/<profile_username>", view_func=user_blooms)
app.add_url_rule("/hashtag/<hashtag>", view_func=hashtag)

Expand Down
4 changes: 3 additions & 1 deletion db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ CREATE TABLE blooms (
id BIGSERIAL NOT NULL PRIMARY KEY,
sender_id INT NOT NULL REFERENCES users(id),
content TEXT NOT NULL,
send_timestamp TIMESTAMP NOT NULL
send_timestamp TIMESTAMP NOT NULL,
rebloom_id BIGINT REFERENCES blooms(id)

);

CREATE TABLE follows (
Expand Down
50 changes: 47 additions & 3 deletions front-end/components/bloom.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { apiService } from "../index.mjs";
/**
* Create a bloom component
* Create a bloom component-> adding rebloom functionality in this component.
* @param {string} template - The ID of the template to clone
* @param {Object} bloom - The bloom data
* @returns {DocumentFragment} - The bloom fragment of UI, for items in the Timeline
Expand All @@ -20,20 +21,63 @@ 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 rebloomInfo = bloomFrag.querySelector("[data-rebloom-info]");
const rebloomSender = bloomFrag.querySelector("[data-rebloom-sender]");
const rebloomBtn = bloomFrag.querySelector("[data-action='rebloom']");
const rebloomCount = bloomFrag.querySelector("[data-rebloom-count]");

bloomArticle.setAttribute("data-bloom-id", bloom.id);
bloomUsername.setAttribute("href", `/profile/${bloom.sender}`);
bloomUsername.textContent = bloom.sender;

if (bloom.rebloom_id) { //checking if rebloom_id exists its a rebloom otherwise normal bloom
bloomUsername.setAttribute("href", `/profile/${bloom.original_sender}`);
bloomUsername.textContent = bloom.original_sender;

if (rebloomInfo) rebloomInfo.hidden = false;
if (rebloomSender) {
rebloomSender.textContent = bloom.sender;
rebloomSender.setAttribute("href", `/profile/${bloom.sender}`);
}
} else {
bloomUsername.setAttribute("href", `/profile/${bloom.sender}`);
bloomUsername.textContent = bloom.sender;

if (rebloomInfo) rebloomInfo.hidden = true;
}
if (rebloomBtn) rebloomBtn.setAttribute("data-bloom-id", bloom.id);
bloomTime.textContent = _formatTimestamp(bloom.sent_timestamp);
bloomTimeLink.setAttribute("href", `/bloom/${bloom.id}`);
bloomContent.replaceChildren(
...bloomParser.parseFromString(_formatHashtags(bloom.content), "text/html")
.body.childNodes
);
if (rebloomCount) {
rebloomCount.textContent = bloom.rebloom_count || 0;
}

if (rebloomBtn) {
const rebloomID = bloom.rebloom_id || bloom.id;
rebloomBtn.setAttribute("data-bloom-id", rebloomID);

rebloomBtn.addEventListener("click", handleRebloom);
}
return bloomFrag;
};


async function handleRebloom(event) {
const button = event.target.closest("button");
const bloomId = button.getAttribute("data-bloom-id"); //each rebloom button stores id of the respective bloom.

if (!bloomId) return;

button.disabled = true; //preventing users from doubleclikcing and double reblooms

await apiService.rebloom(bloomId); //POST request to server.

button.disabled = false; // so that user knows it has been rebloomed.
}


function _formatHashtags(text) {
if (!text) return text;
return text.replace(
Expand Down
8 changes: 8 additions & 0 deletions front-end/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -234,11 +234,19 @@ <h2 id="bloom-form-title" class="bloom-form__title">Share a Bloom</h2>
<!-- Bloom Template -->
<template id="bloom-template">
<article class="bloom box" data-bloom data-bloom-id="">
<div class="bloom__meta system-message" data-rebloom-info hidden>
<small> Rebloomed by <a href="#" data-rebloom-sender></a></small>
</div>
<div class="bloom__header flex">
<a href="#" class="bloom__username" data-username>Username</a>
<a href="#" class="bloom__time"><time class="bloom__time" data-time>2m</time></a>
</div>
<div class="bloom__content" data-content></div>
<footer class="bloom__footer">
<button type="button" data-action="rebloom" >
Rebloom (<span data-rebloom-count>0</span>)
</button>
</footer>
</article>
</template>

Expand Down
Loading