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
98 changes: 91 additions & 7 deletions backend/data/blooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from data.connection import db_cursor
from data.users import User
from psycopg2.errors import UniqueViolation


@dataclass
Expand All @@ -13,6 +14,8 @@ class Bloom:
sender: User
content: str
sent_timestamp: datetime.datetime
re_bloom_count: int = 0
re_bloomed_by: Optional[str] = None


def add_bloom(*, sender: User, content: str) -> Bloom:
Expand Down Expand Up @@ -54,7 +57,8 @@ def get_blooms_for_user(

cur.execute(
f"""SELECT
blooms.id, users.username, content, send_timestamp
blooms.id, users.username, content, send_timestamp,
(SELECT COUNT(*) FROM re_blooms WHERE re_blooms.bloom_id = blooms.id) AS re_bloom_count
FROM
blooms INNER JOIN users ON users.id = blooms.sender_id
WHERE
Expand All @@ -68,33 +72,48 @@ def get_blooms_for_user(
rows = cur.fetchall()
blooms = []
for row in rows:
bloom_id, sender_username, content, timestamp = row
bloom_id, sender_username, content, timestamp, re_bloom_count = row
blooms.append(
Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
re_bloom_count=re_bloom_count,
)
)
return blooms
re_blooms_list = get_re_blooms_for_user(username, limit=limit)
combined = blooms + re_blooms_list
combined.sort(key=lambda b: b.sent_timestamp, reverse=True)
if limit is not None:
combined = combined[:limit]


return combined


def get_bloom(bloom_id: int) -> Optional[Bloom]:
with db_cursor() as cur:
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
blooms.id, users.username, content, send_timestamp,
(SELECT COUNT(*) FROM re_blooms WHERE re_blooms.bloom_id = blooms.id) AS re_bloom_count
FROM
blooms INNER JOIN users ON users.id = blooms.sender_id
WHERE
blooms.id = %s""",
(bloom_id,),
)
row = cur.fetchone()
if row is None:
return None
bloom_id, sender_username, content, timestamp = row
bloom_id, sender_username, content, timestamp, re_bloom_count = row
return Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
re_bloom_count=re_bloom_count,
)


Expand All @@ -108,7 +127,8 @@ def get_blooms_with_hashtag(
with db_cursor() as cur:
cur.execute(
f"""SELECT
blooms.id, users.username, content, send_timestamp
blooms.id, users.username, content, send_timestamp,
(SELECT COUNT(*) FROM re_blooms WHERE re_blooms.bloom_id = blooms.id) AS re_bloom_count
FROM
blooms INNER JOIN hashtags ON blooms.id = hashtags.bloom_id INNER JOIN users ON blooms.sender_id = users.id
WHERE
Expand All @@ -121,13 +141,14 @@ def get_blooms_with_hashtag(
rows = cur.fetchall()
blooms = []
for row in rows:
bloom_id, sender_username, content, timestamp = row
bloom_id, sender_username, content, timestamp, re_bloom_count = row
blooms.append(
Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
re_bloom_count=re_bloom_count,
)
)
return blooms
Expand All @@ -140,3 +161,66 @@ def make_limit_clause(limit: Optional[int], kwargs: Dict[Any, Any]) -> str:
else:
limit_clause = ""
return limit_clause


def re_bloom(bloom_id: int, re_bloomer: User) -> bool:

bloom = get_bloom(bloom_id)

if bloom is None:
return False

now = datetime.datetime.now(tz=datetime.UTC)

with db_cursor() as cur:
try:
cur.execute(
"INSERT INTO re_blooms (bloom_id, re_bloomer_id, re_bloom_timestamp) VALUES (%(bloom_id)s, %(re_bloomer_id)s, %(timestamp)s)",
dict(
bloom_id=bloom_id,
re_bloomer_id=re_bloomer.id,
timestamp=now,
),
)

except UniqueViolation:
return False

return True;


def get_re_blooms_for_user(username: str, *, limit: Optional[int] = None) -> List[Bloom]:
kwargs = {"re_bloomer_username": username}
limit_clause = make_limit_clause(limit, kwargs)
with db_cursor() as cur:
cur.execute(
f"""SELECT
blooms.id, original_sender.username, blooms.content, re_blooms.re_bloom_timestamp,
(SELECT COUNT(*) FROM re_blooms WHERE re_blooms.bloom_id = blooms.id) AS re_bloom_count
FROM
re_blooms
INNER JOIN blooms ON blooms.id = re_blooms.bloom_id
INNER JOIN users AS original_sender ON original_sender.id = blooms.sender_id
INNER JOIN users AS re_bloomer ON re_bloomer.id = re_blooms.re_bloomer_id
WHERE
re_bloomer.username = %(re_bloomer_username)s
ORDER BY re_blooms.re_bloom_timestamp DESC
{limit_clause}
""",
kwargs,
)
rows = cur.fetchall()
re_blooms_list = []
for row in rows:
bloom_id, sender_username, content, re_bloom_timestamp, re_bloom_count = row
re_blooms_list.append(
Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=re_bloom_timestamp,
re_bloom_count=re_bloom_count,
re_bloomed_by=username,
)
)
return re_blooms_list
19 changes: 19 additions & 0 deletions backend/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,3 +245,22 @@ def verify_request_fields(names_to_types: Dict[str, type]) -> Union[Response, No
)
)
return None


@jwt_required()
def re_bloom_bloom(id_str):
try:
bloom_id = int(id_str)
except ValueError:
return make_response(("Invalid bloom id", 400))

user = get_current_user()
bloom = blooms.get_bloom(bloom_id)
if bloom is None:
return make_response(("Bloom not found", 404))

blooms.re_bloom(bloom_id, user)
return jsonify({
"success": True,
"message": f"Bloom {bloom_id} re-bloomed successfully"
})
2 changes: 2 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
send_bloom,
suggested_follows,
user_blooms,
re_bloom_bloom,
)

from dotenv import load_dotenv
Expand Down Expand Up @@ -60,6 +61,7 @@ def main():
app.add_url_rule("/bloom/<id_str>", methods=["GET"], view_func=get_bloom)
app.add_url_rule("/blooms/<profile_username>", view_func=user_blooms)
app.add_url_rule("/hashtag/<hashtag>", view_func=hashtag)
app.add_url_rule("/bloom/<id_str>/re-bloom", methods=["POST"], view_func=re_bloom_bloom)

app.run(host="0.0.0.0", port="3000", debug=True)

Expand Down
9 changes: 9 additions & 0 deletions db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,12 @@ CREATE TABLE hashtags (
bloom_id BIGINT NOT NULL REFERENCES blooms(id),
UNIQUE(hashtag, bloom_id)
);

CREATE TABLE re_blooms (
id SERIAL PRIMARY KEY,
bloom_id BIGINT NOT NULL REFERENCES blooms(id),
re_bloomer_id INT NOT NULL REFERENCES users(id),
re_bloom_timestamp TIMESTAMP NOT NULL,
UNIQUE(bloom_id, re_bloomer_id)
)

23 changes: 23 additions & 0 deletions front-end/components/bloom.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
* "sent_timestamp": "datetime as ISO 8601 formatted string"}

*/

import {apiService} from "../index.mjs"


const createBloom = (template, bloom) => {
if (!bloom) return;
const bloomFrag = document.getElementById(template).content.cloneNode(true);
Expand All @@ -20,6 +24,10 @@ 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 reBloomedByBanner = bloomFrag.querySelector("[data-rebloomed-by]");
const reBloomedByLink = bloomFrag.querySelector("[data-rebloomed-by-link]");
const reBloomBtn = bloomFrag.querySelector("[data-rebloom-btn]");
const reBloomCount = bloomFrag.querySelector("[data-rebloom-count]");

bloomArticle.setAttribute("data-bloom-id", bloom.id);
bloomUsername.setAttribute("href", `/profile/${bloom.sender}`);
Expand All @@ -31,6 +39,21 @@ const createBloom = (template, bloom) => {
.body.childNodes
);

if (bloom.re_bloomed_by) {
reBloomedByBanner.hidden = false;
reBloomedByLink.textContent = bloom.re_bloomed_by;
reBloomedByLink.setAttribute("href", `/profile/${bloom.re_bloomed_by}`);
}

if (bloom.re_bloom_count > 0) {
reBloomCount.hidden = false;
reBloomCount.textContent = `${bloom.re_bloom_count} re-blooms`;
}

reBloomBtn.addEventListener("click", () => {
apiService.reBloomBloom(bloom.id);
});

return bloomFrag;
};

Expand Down
9 changes: 9 additions & 0 deletions front-end/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -234,11 +234,20 @@ <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__rebloomed-by" data-rebloomed-by hidden>
re-bloomed by <a href="#" data-rebloomed-by-link></a>
</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>

<div class="bloom__footer">
<button class="bloom__rebloom-btn" data-rebloom-btn> Re-bloom</button>
<br><br>
<span data-rebloom-count hidden></span>
</div>
</article>
</template>

Expand Down
17 changes: 17 additions & 0 deletions front-end/lib/api.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,22 @@ async function unfollowUser(username) {
}
}

async function reBloomBloom(bloomId) {
try {
const data = await _apiRequest(`/bloom/${bloomId}/re-bloom`, {
method: "POST",
});

if (data.success) {
await getBlooms();
}

return data;
} catch (error) {
return {success: false};
}
}

const apiService = {
// Auth methods
login,
Expand All @@ -298,6 +314,7 @@ const apiService = {
followUser,
unfollowUser,
getWhoToFollow,
reBloomBloom,
};

export {apiService};