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
141 changes: 94 additions & 47 deletions backend/data/blooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,95 @@
@dataclass
class Bloom:
id: int
sender: User
sender: str
content: str
sent_timestamp: datetime.datetime
rebloom_of: int | None = None
original_sender: str | None = None
original_content: str | None = None
original_sent_timestamp: datetime.datetime | None = None
rebloom_count: int = 0

def to_dict(self) -> Dict[str, Any]:
return _make_bloom_dict(self)


def _make_bloom_dict(bloom: Bloom) -> Dict[str, Any]:
return {
"id": bloom.id,
"sender": bloom.sender,
"content": bloom.content,
"sent_timestamp": bloom.sent_timestamp,
"rebloom_of": bloom.rebloom_of,
"original_sender": bloom.original_sender,
"original_content": bloom.original_content,
"original_sent_timestamp": bloom.original_sent_timestamp,
"rebloom_count": bloom.rebloom_count,
}


def add_bloom(*, sender: User, content: str) -> Bloom:
def _row_to_bloom(row):
(
bloom_id,
sender_username,
content,
timestamp,
rebloom_of,
original_sender,
original_content,
original_timestamp,
rebloom_count,
) = row
return Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
rebloom_of=rebloom_of,
original_sender=original_sender,
original_content=original_content,
original_sent_timestamp=original_timestamp,
rebloom_count=rebloom_count or 0,
)


def _default_bloom_query():
return """SELECT
blooms.id,
users.username,
blooms.content,
blooms.send_timestamp,
blooms.rebloom_of,
original_users.username AS original_sender,
original_blooms.content AS original_content,
original_blooms.send_timestamp AS original_sent_timestamp,
COALESCE(rebloom_counts.num_rebloomed, 0) AS rebloom_count
FROM blooms
INNER JOIN users ON users.id = blooms.sender_id
LEFT JOIN blooms AS original_blooms ON original_blooms.id = blooms.rebloom_of
LEFT JOIN users AS original_users ON original_users.id = original_blooms.sender_id
LEFT JOIN (
SELECT rebloom_of, COUNT(*) AS num_rebloomed
FROM blooms
WHERE rebloom_of IS NOT NULL
GROUP BY rebloom_of
) rebloom_counts ON rebloom_counts.rebloom_of = blooms.id"""


def add_bloom(*, sender: User, content: str, rebloom_of=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_of) VALUES (%(bloom_id)s, %(sender_id)s, %(content)s, %(timestamp)s, %(rebloom_of)s)",
dict(
bloom_id=bloom_id,
sender_id=sender.id,
content=content,
timestamp=datetime.datetime.now(datetime.UTC),
rebloom_of=rebloom_of,
),
)
for hashtag in hashtags:
Expand All @@ -36,6 +107,14 @@ def add_bloom(*, sender: User, content: str) -> Bloom:
dict(hashtag=hashtag, bloom_id=bloom_id),
)

return Bloom(
id=bloom_id,
sender=sender.username,
content=content,
sent_timestamp=now,
rebloom_of=rebloom_of,
)


def get_blooms_for_user(
username: str, *, before: Optional[int] = None, limit: Optional[int] = None
Expand All @@ -45,57 +124,38 @@ def get_blooms_for_user(
"sender_username": username,
}
if before is not None:
before_clause = "AND send_timestamp < %(before_limit)s"
before_clause = "AND blooms.send_timestamp < %(before_limit)s"
kwargs["before_limit"] = before
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
f"""{_default_bloom_query()}
WHERE
username = %(sender_username)s
users.username = %(sender_username)s
{before_clause}
ORDER BY send_timestamp DESC
ORDER BY blooms.send_timestamp DESC
{limit_clause}
""",
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 [_row_to_bloom(row) for row in rows]


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",
f"""{_default_bloom_query()}
WHERE blooms.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 _row_to_bloom(row)


def get_blooms_with_hashtag(
Expand All @@ -107,30 +167,17 @@ def get_blooms_with_hashtag(
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
f"""{_default_bloom_query()}
INNER JOIN hashtags ON blooms.id = hashtags.bloom_id
WHERE
hashtag = %(hashtag_without_leading_hash)s
ORDER BY send_timestamp DESC
ORDER BY blooms.send_timestamp DESC
{limit_clause}
""",
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 [_row_to_bloom(row) for row in rows]


def make_limit_clause(limit: Optional[int], kwargs: Dict[Any, Any]) -> str:
Expand Down
36 changes: 31 additions & 5 deletions backend/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def other_profile(profile_username):
return jsonify(
{
"username": profile_username,
"recent_blooms": all_blooms[:10],
"recent_blooms": [bloom.to_dict() for bloom in all_blooms[:10]],
"follows": get_followed_usernames(profile_user),
"followers": list(followers),
"is_following": current_user is not None
Expand Down Expand Up @@ -175,7 +175,7 @@ def get_bloom(id_str):
bloom = blooms.get_bloom(id_int)
if bloom is None:
return make_response((f"Bloom not found", 404))
return jsonify(bloom)
return jsonify(bloom.to_dict())


@jwt_required()
Expand Down Expand Up @@ -203,13 +203,13 @@ def home_timeline():
sorted(all_blooms, key=lambda bloom: bloom.sent_timestamp, reverse=True)
)

return jsonify(sorted_blooms)
return jsonify([bloom.to_dict() for bloom in sorted_blooms])


def user_blooms(profile_username):
user_blooms = blooms.get_blooms_for_user(profile_username)
user_blooms.reverse()
return jsonify(user_blooms)
return jsonify([bloom.to_dict() for bloom in user_blooms])


@jwt_required()
Expand All @@ -229,7 +229,7 @@ def suggested_follows(limit_str):


def hashtag(hashtag):
return jsonify(blooms.get_blooms_with_hashtag(hashtag))
return jsonify([bloom.to_dict() for bloom in blooms.get_blooms_with_hashtag(hashtag)])


def verify_request_fields(names_to_types: Dict[str, type]) -> Union[Response, None]:
Expand All @@ -245,3 +245,29 @@ def verify_request_fields(names_to_types: Dict[str, type]) -> Union[Response, No
)
)
return None

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

original_bloom = blooms.get_bloom(bloom_id)

if original_bloom is None:
return make_response(("Bloom not found", 404))

current_user = get_current_user()

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

return jsonify(
{
"success": True,
}
)
6 changes: 6 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from custom_json_provider import CustomJsonProvider
from data.users import lookup_user
from endpoints import (
do_rebloom,
do_follow,
get_bloom,
hashtag,
Expand Down Expand Up @@ -60,6 +61,11 @@ 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(
"/rebloom/<bloom_id>",
methods=["POST"],
view_func=do_rebloom,
)

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

Expand Down
3 changes: 2 additions & 1 deletion db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ 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_of BIGINT REFERENCES blooms(id)
);

CREATE TABLE follows (
Expand Down
9 changes: 8 additions & 1 deletion front-end/components/bloom.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
* "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 +22,9 @@ 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-action='rebloom']"
);

bloomArticle.setAttribute("data-bloom-id", bloom.id);
bloomUsername.setAttribute("href", `/profile/${bloom.sender}`);
Expand All @@ -30,7 +35,9 @@ const createBloom = (template, bloom) => {
...bloomParser.parseFromString(_formatHashtags(bloom.content), "text/html")
.body.childNodes
);

rebloomButton?.addEventListener("click", () => {
apiService.rebloomBloom(bloom.id);
});
return bloomFrag;
};

Expand Down
11 changes: 10 additions & 1 deletion front-end/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,18 @@ <h2 id="bloom-form-title" class="bloom-form__title">Share a Bloom</h2>
<article class="bloom box" data-bloom data-bloom-id="">
<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>
<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__actions">
<button type="button" data-action="rebloom">
Re-bloom
</button>
</div>

</article>
</template>

Expand Down
Loading